#
tokens: 48010/50000 10/393 files (page 10/16)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 10 of 16. Use http://codebase.md/cameroncooke/xcodebuildmcp?lines=true&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
│       ├── README.md
│       ├── release.yml
│       ├── sentry.yml
│       └── stale.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
├── docs
│   ├── CONFIGURATION.md
│   ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│   ├── DEBUGGING_ARCHITECTURE.md
│   ├── DEMOS.md
│   ├── dev
│   │   ├── ARCHITECTURE.md
│   │   ├── CODE_QUALITY.md
│   │   ├── CONTRIBUTING.md
│   │   ├── ESLINT_TYPE_SAFETY.md
│   │   ├── MANUAL_TESTING.md
│   │   ├── NODEJS_2025.md
│   │   ├── PLUGIN_DEVELOPMENT.md
│   │   ├── README.md
│   │   ├── RELEASE_PROCESS.md
│   │   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   │   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   │   ├── RELOADEROO.md
│   │   ├── session_management_plan.md
│   │   ├── session-aware-migration-todo.md
│   │   ├── SMITHERY.md
│   │   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   │   ├── TESTING.md
│   │   └── ZOD_MIGRATION_GUIDE.md
│   ├── DEVICE_CODE_SIGNING.md
│   ├── GETTING_STARTED.md
│   ├── investigations
│   │   ├── issue-154-screenshot-downscaling.md
│   │   ├── issue-163.md
│   │   ├── issue-debugger-attach-stopped.md
│   │   └── issue-describe-ui-empty-after-debugger-resume.md
│   ├── OVERVIEW.md
│   ├── PRIVACY.md
│   ├── README.md
│   ├── SESSION_DEFAULTS.md
│   ├── TOOLS.md
│   └── TROUBLESHOOTING.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
│   │   ├── .gitignore
│   │   ├── 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
│   │   └── MCPTestTests
│   │       └── MCPTestTests.swift
│   └── 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
│   ├── generate-loaders.ts
│   ├── generate-version.ts
│   ├── release.sh
│   ├── tools-cli.ts
│   ├── update-tools-docs.ts
│   └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── generated-plugins.ts
│   │   ├── generated-resources.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   ├── session-status.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   ├── session-status.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── debugging
│   │       │   ├── debug_attach_sim.ts
│   │       │   ├── debug_breakpoint_add.ts
│   │       │   ├── debug_breakpoint_remove.ts
│   │       │   ├── debug_continue.ts
│   │       │   ├── debug_detach.ts
│   │       │   ├── debug_lldb_command.ts
│   │       │   ├── debug_stack.ts
│   │       │   ├── debug_variables.ts
│   │       │   └── index.ts
│   │       ├── 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
│   │       ├── 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
│   │   ├── bootstrap.ts
│   │   └── server.ts
│   ├── smithery.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   ├── utils
│   │   ├── __tests__
│   │   │   ├── build-utils-suppress-warnings.test.ts
│   │   │   ├── build-utils.test.ts
│   │   │   ├── debugger-simctl.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
│   │   │   └── workflow-selection.test.ts
│   │   ├── axe
│   │   │   └── index.ts
│   │   ├── axe-helpers.ts
│   │   ├── build
│   │   │   └── index.ts
│   │   ├── build-utils.ts
│   │   ├── capabilities.ts
│   │   ├── command.ts
│   │   ├── CommandExecutor.ts
│   │   ├── debugger
│   │   │   ├── __tests__
│   │   │   │   └── debugger-manager-dap.test.ts
│   │   │   ├── backends
│   │   │   │   ├── __tests__
│   │   │   │   │   └── dap-backend.test.ts
│   │   │   │   ├── dap-backend.ts
│   │   │   │   ├── DebuggerBackend.ts
│   │   │   │   └── lldb-cli-backend.ts
│   │   │   ├── dap
│   │   │   │   ├── __tests__
│   │   │   │   │   └── transport-framing.test.ts
│   │   │   │   ├── adapter-discovery.ts
│   │   │   │   ├── transport.ts
│   │   │   │   └── types.ts
│   │   │   ├── debugger-manager.ts
│   │   │   ├── index.ts
│   │   │   ├── simctl.ts
│   │   │   ├── tool-context.ts
│   │   │   ├── types.ts
│   │   │   └── ui-automation-guard.ts
│   │   ├── environment.ts
│   │   ├── errors.ts
│   │   ├── execution
│   │   │   ├── index.ts
│   │   │   └── interactive-process.ts
│   │   ├── FileSystemExecutor.ts
│   │   ├── log_capture.ts
│   │   ├── log-capture
│   │   │   ├── device-log-sessions.ts
│   │   │   └── index.ts
│   │   ├── logger.ts
│   │   ├── logging
│   │   │   └── index.ts
│   │   ├── plugin-registry
│   │   │   └── index.ts
│   │   ├── responses
│   │   │   └── index.ts
│   │   ├── runtime-registry.ts
│   │   ├── schema-helpers.ts
│   │   ├── sentry.ts
│   │   ├── session-status.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
│   │   ├── workflow-selection.ts
│   │   ├── xcode.ts
│   │   ├── xcodemake
│   │   │   └── index.ts
│   │   └── xcodemake.ts
│   └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```

# Files

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_stop plugin
  3 |  * Following CLAUDE.md testing standards with pure dependency injection
  4 |  * No vitest mocking - using dependency injection pattern
  5 |  */
  6 | 
  7 | import { describe, it, expect } from 'vitest';
  8 | import * as z from 'zod';
  9 | import swiftPackageStop, {
 10 |   createMockProcessManager,
 11 |   swift_package_stopLogic,
 12 |   type ProcessManager,
 13 | } from '../swift_package_stop.ts';
 14 | 
 15 | /**
 16 |  * Mock process implementation for testing
 17 |  */
 18 | class MockProcess {
 19 |   public killed = false;
 20 |   public killSignal: string | undefined;
 21 |   public exitCallback: (() => void) | undefined;
 22 |   public shouldThrowOnKill = false;
 23 |   public killError: Error | string | undefined;
 24 |   public pid: number;
 25 | 
 26 |   constructor(pid: number) {
 27 |     this.pid = pid;
 28 |   }
 29 | 
 30 |   kill(signal?: string): void {
 31 |     if (this.shouldThrowOnKill) {
 32 |       throw this.killError ?? new Error('Process kill failed');
 33 |     }
 34 |     this.killed = true;
 35 |     this.killSignal = signal;
 36 |   }
 37 | 
 38 |   on(event: string, callback: () => void): void {
 39 |     if (event === 'exit') {
 40 |       this.exitCallback = callback;
 41 |     }
 42 |   }
 43 | 
 44 |   // Simulate immediate exit for test control
 45 |   simulateExit(): void {
 46 |     if (this.exitCallback) {
 47 |       this.exitCallback();
 48 |     }
 49 |   }
 50 | }
 51 | 
 52 | describe('swift_package_stop plugin', () => {
 53 |   describe('Export Field Validation (Literal)', () => {
 54 |     it('should have correct name', () => {
 55 |       expect(swiftPackageStop.name).toBe('swift_package_stop');
 56 |     });
 57 | 
 58 |     it('should have correct description', () => {
 59 |       expect(swiftPackageStop.description).toBe(
 60 |         'Stops a running Swift Package executable started with swift_package_run',
 61 |       );
 62 |     });
 63 | 
 64 |     it('should have handler function', () => {
 65 |       expect(typeof swiftPackageStop.handler).toBe('function');
 66 |     });
 67 | 
 68 |     it('should validate schema correctly', () => {
 69 |       // Test valid inputs
 70 |       expect(swiftPackageStop.schema.pid.safeParse(12345).success).toBe(true);
 71 |       expect(swiftPackageStop.schema.pid.safeParse(0).success).toBe(true);
 72 |       expect(swiftPackageStop.schema.pid.safeParse(-1).success).toBe(true);
 73 | 
 74 |       // Test invalid inputs
 75 |       expect(swiftPackageStop.schema.pid.safeParse('not-a-number').success).toBe(false);
 76 |       expect(swiftPackageStop.schema.pid.safeParse(null).success).toBe(false);
 77 |       expect(swiftPackageStop.schema.pid.safeParse(undefined).success).toBe(false);
 78 |       expect(swiftPackageStop.schema.pid.safeParse({}).success).toBe(false);
 79 |       expect(swiftPackageStop.schema.pid.safeParse([]).success).toBe(false);
 80 |     });
 81 |   });
 82 | 
 83 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 84 |     it('should return exact error for process not found', async () => {
 85 |       const mockProcessManager = createMockProcessManager({
 86 |         getProcess: () => undefined,
 87 |       });
 88 | 
 89 |       const result = await swift_package_stopLogic({ pid: 99999 }, mockProcessManager);
 90 | 
 91 |       expect(result).toEqual({
 92 |         content: [
 93 |           {
 94 |             type: 'text',
 95 |             text: '⚠️ No running process found with PID 99999. Use swift_package_run to check active processes.',
 96 |           },
 97 |         ],
 98 |         isError: true,
 99 |       });
100 |     });
101 | 
102 |     it('should successfully stop a process that exits gracefully', async () => {
103 |       const mockProcess = new MockProcess(12345);
104 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
105 | 
106 |       const mockProcessManager = createMockProcessManager({
107 |         getProcess: (pid: number) =>
108 |           pid === 12345
109 |             ? {
110 |                 process: mockProcess,
111 |                 startedAt: startedAt,
112 |               }
113 |             : undefined,
114 |         removeProcess: () => true,
115 |       });
116 | 
117 |       // Set up the process to exit immediately when exit handler is registered
118 |       const originalOn = mockProcess.on.bind(mockProcess);
119 |       mockProcess.on = (event: string, callback: () => void) => {
120 |         originalOn(event, callback);
121 |         if (event === 'exit') {
122 |           // Simulate immediate graceful exit
123 |           setImmediate(() => callback());
124 |         }
125 |       };
126 | 
127 |       const result = await swift_package_stopLogic(
128 |         { pid: 12345 },
129 |         mockProcessManager,
130 |         10, // Very short timeout for testing
131 |       );
132 | 
133 |       expect(mockProcess.killed).toBe(true);
134 |       expect(mockProcess.killSignal).toBe('SIGTERM');
135 |       expect(result).toEqual({
136 |         content: [
137 |           {
138 |             type: 'text',
139 |             text: '✅ Stopped executable (was running since 2023-01-01T10:00:00.000Z)',
140 |           },
141 |           {
142 |             type: 'text',
143 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
144 |           },
145 |         ],
146 |       });
147 |     });
148 | 
149 |     it('should force kill process if graceful termination fails', async () => {
150 |       const mockProcess = new MockProcess(67890);
151 |       const startedAt = new Date('2023-02-15T14:30:00.000Z');
152 | 
153 |       const mockProcessManager = createMockProcessManager({
154 |         getProcess: (pid: number) =>
155 |           pid === 67890
156 |             ? {
157 |                 process: mockProcess,
158 |                 startedAt: startedAt,
159 |               }
160 |             : undefined,
161 |         removeProcess: () => true,
162 |       });
163 | 
164 |       // Mock the process to NOT exit gracefully (no callback invocation)
165 |       const killCalls: string[] = [];
166 |       const originalKill = mockProcess.kill.bind(mockProcess);
167 |       mockProcess.kill = (signal?: string) => {
168 |         killCalls.push(signal ?? 'default');
169 |         originalKill(signal);
170 |       };
171 | 
172 |       // Set up timeout to trigger SIGKILL after SIGTERM
173 |       const originalOn = mockProcess.on.bind(mockProcess);
174 |       mockProcess.on = (event: string, callback: () => void) => {
175 |         originalOn(event, callback);
176 |         // Do NOT call the callback to simulate hanging process
177 |       };
178 | 
179 |       const result = await swift_package_stopLogic(
180 |         { pid: 67890 },
181 |         mockProcessManager,
182 |         10, // Very short timeout for testing
183 |       );
184 | 
185 |       expect(killCalls).toEqual(['SIGTERM', 'SIGKILL']);
186 |       expect(result).toEqual({
187 |         content: [
188 |           {
189 |             type: 'text',
190 |             text: '✅ Stopped executable (was running since 2023-02-15T14:30:00.000Z)',
191 |           },
192 |           {
193 |             type: 'text',
194 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
195 |           },
196 |         ],
197 |       });
198 |     });
199 | 
200 |     it('should handle process kill error and return error response', async () => {
201 |       const mockProcess = new MockProcess(54321);
202 |       const startedAt = new Date('2023-03-20T09:15:00.000Z');
203 | 
204 |       // Configure process to throw error on kill
205 |       mockProcess.shouldThrowOnKill = true;
206 |       mockProcess.killError = new Error('ESRCH: No such process');
207 | 
208 |       const mockProcessManager = createMockProcessManager({
209 |         getProcess: (pid: number) =>
210 |           pid === 54321
211 |             ? {
212 |                 process: mockProcess,
213 |                 startedAt: startedAt,
214 |               }
215 |             : undefined,
216 |       });
217 | 
218 |       const result = await swift_package_stopLogic({ pid: 54321 }, mockProcessManager);
219 | 
220 |       expect(result).toEqual({
221 |         content: [
222 |           {
223 |             type: 'text',
224 |             text: 'Error: Failed to stop process\nDetails: ESRCH: No such process',
225 |           },
226 |         ],
227 |         isError: true,
228 |       });
229 |     });
230 | 
231 |     it('should handle non-Error exception in catch block', async () => {
232 |       const mockProcess = new MockProcess(11111);
233 |       const startedAt = new Date('2023-04-10T16:45:00.000Z');
234 | 
235 |       // Configure process to throw non-Error object
236 |       mockProcess.shouldThrowOnKill = true;
237 |       mockProcess.killError = 'Process termination failed';
238 | 
239 |       const mockProcessManager = createMockProcessManager({
240 |         getProcess: (pid: number) =>
241 |           pid === 11111
242 |             ? {
243 |                 process: mockProcess,
244 |                 startedAt: startedAt,
245 |               }
246 |             : undefined,
247 |       });
248 | 
249 |       const result = await swift_package_stopLogic({ pid: 11111 }, mockProcessManager);
250 | 
251 |       expect(result).toEqual({
252 |         content: [
253 |           {
254 |             type: 'text',
255 |             text: 'Error: Failed to stop process\nDetails: Process termination failed',
256 |           },
257 |         ],
258 |         isError: true,
259 |       });
260 |     });
261 | 
262 |     it('should handle process found but exit event never fires and timeout occurs', async () => {
263 |       const mockProcess = new MockProcess(22222);
264 |       const startedAt = new Date('2023-05-05T12:00:00.000Z');
265 | 
266 |       const mockProcessManager = createMockProcessManager({
267 |         getProcess: (pid: number) =>
268 |           pid === 22222
269 |             ? {
270 |                 process: mockProcess,
271 |                 startedAt: startedAt,
272 |               }
273 |             : undefined,
274 |         removeProcess: () => true,
275 |       });
276 | 
277 |       const killCalls: string[] = [];
278 |       const originalKill = mockProcess.kill.bind(mockProcess);
279 |       mockProcess.kill = (signal?: string) => {
280 |         killCalls.push(signal ?? 'default');
281 |         originalKill(signal);
282 |       };
283 | 
284 |       // Mock process.on to register the exit handler but never call it (timeout scenario)
285 |       const originalOn = mockProcess.on.bind(mockProcess);
286 |       mockProcess.on = (event: string, callback: () => void) => {
287 |         originalOn(event, callback);
288 |         // Handler is registered but callback never called (simulates hanging process)
289 |       };
290 | 
291 |       const result = await swift_package_stopLogic(
292 |         { pid: 22222 },
293 |         mockProcessManager,
294 |         10, // Very short timeout for testing
295 |       );
296 | 
297 |       expect(killCalls).toEqual(['SIGTERM', 'SIGKILL']);
298 |       expect(result).toEqual({
299 |         content: [
300 |           {
301 |             type: 'text',
302 |             text: '✅ Stopped executable (was running since 2023-05-05T12:00:00.000Z)',
303 |           },
304 |           {
305 |             type: 'text',
306 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
307 |           },
308 |         ],
309 |       });
310 |     });
311 | 
312 |     it('should handle edge case with pid 0', async () => {
313 |       const mockProcessManager = createMockProcessManager({
314 |         getProcess: () => undefined,
315 |       });
316 | 
317 |       const result = await swift_package_stopLogic({ pid: 0 }, mockProcessManager);
318 | 
319 |       expect(result).toEqual({
320 |         content: [
321 |           {
322 |             type: 'text',
323 |             text: '⚠️ No running process found with PID 0. Use swift_package_run to check active processes.',
324 |           },
325 |         ],
326 |         isError: true,
327 |       });
328 |     });
329 | 
330 |     it('should handle edge case with negative pid', async () => {
331 |       const mockProcessManager = createMockProcessManager({
332 |         getProcess: () => undefined,
333 |       });
334 | 
335 |       const result = await swift_package_stopLogic({ pid: -1 }, mockProcessManager);
336 | 
337 |       expect(result).toEqual({
338 |         content: [
339 |           {
340 |             type: 'text',
341 |             text: '⚠️ No running process found with PID -1. Use swift_package_run to check active processes.',
342 |           },
343 |         ],
344 |         isError: true,
345 |       });
346 |     });
347 | 
348 |     it('should handle process that exits after first SIGTERM call', async () => {
349 |       const mockProcess = new MockProcess(33333);
350 |       const startedAt = new Date('2023-06-01T08:30:00.000Z');
351 | 
352 |       const mockProcessManager = createMockProcessManager({
353 |         getProcess: (pid: number) =>
354 |           pid === 33333
355 |             ? {
356 |                 process: mockProcess,
357 |                 startedAt: startedAt,
358 |               }
359 |             : undefined,
360 |         removeProcess: () => true,
361 |       });
362 | 
363 |       const killCalls: string[] = [];
364 |       const originalKill = mockProcess.kill.bind(mockProcess);
365 |       mockProcess.kill = (signal?: string) => {
366 |         killCalls.push(signal ?? 'default');
367 |         originalKill(signal);
368 |       };
369 | 
370 |       // Set up the process to exit immediately when exit handler is registered
371 |       const originalOn = mockProcess.on.bind(mockProcess);
372 |       mockProcess.on = (event: string, callback: () => void) => {
373 |         originalOn(event, callback);
374 |         if (event === 'exit') {
375 |           // Simulate immediate graceful exit
376 |           setImmediate(() => callback());
377 |         }
378 |       };
379 | 
380 |       const result = await swift_package_stopLogic(
381 |         { pid: 33333 },
382 |         mockProcessManager,
383 |         10, // Very short timeout for testing
384 |       );
385 | 
386 |       expect(killCalls).toEqual(['SIGTERM']); // Should not call SIGKILL
387 |       expect(result).toEqual({
388 |         content: [
389 |           {
390 |             type: 'text',
391 |             text: '✅ Stopped executable (was running since 2023-06-01T08:30:00.000Z)',
392 |           },
393 |           {
394 |             type: 'text',
395 |             text: '💡 Process terminated. You can now run swift_package_run again if needed.',
396 |           },
397 |         ],
398 |       });
399 |     });
400 | 
401 |     it('should handle undefined pid parameter', async () => {
402 |       const mockProcessManager = createMockProcessManager({
403 |         getProcess: () => undefined,
404 |       });
405 | 
406 |       const result = await swift_package_stopLogic({} as any, mockProcessManager);
407 | 
408 |       expect(result).toEqual({
409 |         content: [
410 |           {
411 |             type: 'text',
412 |             text: '⚠️ No running process found with PID undefined. Use swift_package_run to check active processes.',
413 |           },
414 |         ],
415 |         isError: true,
416 |       });
417 |     });
418 |   });
419 | });
420 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for get_device_app_path plugin (unified)
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import * as z from 'zod';
  9 | import {
 10 |   createMockCommandResponse,
 11 |   createMockExecutor,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import getDeviceAppPath, { get_device_app_pathLogic } from '../get_device_app_path.ts';
 14 | import { sessionStore } from '../../../../utils/session-store.ts';
 15 | 
 16 | describe('get_device_app_path plugin', () => {
 17 |   beforeEach(() => {
 18 |     sessionStore.clear();
 19 |   });
 20 | 
 21 |   describe('Export Field Validation (Literal)', () => {
 22 |     it('should have correct name', () => {
 23 |       expect(getDeviceAppPath.name).toBe('get_device_app_path');
 24 |     });
 25 | 
 26 |     it('should have correct description', () => {
 27 |       expect(getDeviceAppPath.description).toBe(
 28 |         'Retrieves the built app path for a connected device.',
 29 |       );
 30 |     });
 31 | 
 32 |     it('should have handler function', () => {
 33 |       expect(typeof getDeviceAppPath.handler).toBe('function');
 34 |     });
 35 | 
 36 |     it('should expose only platform in public schema', () => {
 37 |       const schema = z.strictObject(getDeviceAppPath.schema);
 38 |       expect(schema.safeParse({}).success).toBe(true);
 39 |       expect(schema.safeParse({ platform: 'iOS' }).success).toBe(true);
 40 |       expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false);
 41 | 
 42 |       const schemaKeys = Object.keys(getDeviceAppPath.schema).sort();
 43 |       expect(schemaKeys).toEqual(['platform']);
 44 |     });
 45 |   });
 46 | 
 47 |   describe('XOR Validation', () => {
 48 |     it('should error when neither projectPath nor workspacePath provided', async () => {
 49 |       const result = await getDeviceAppPath.handler({
 50 |         scheme: 'MyScheme',
 51 |       });
 52 |       expect(result.isError).toBe(true);
 53 |       expect(result.content[0].text).toContain('Missing required session defaults');
 54 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 55 |     });
 56 | 
 57 |     it('should error when both projectPath and workspacePath provided', async () => {
 58 |       const result = await getDeviceAppPath.handler({
 59 |         projectPath: '/path/to/project.xcodeproj',
 60 |         workspacePath: '/path/to/workspace.xcworkspace',
 61 |         scheme: 'MyScheme',
 62 |       });
 63 |       expect(result.isError).toBe(true);
 64 |       expect(result.content[0].text).toContain('Parameter validation failed');
 65 |       expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
 66 |     });
 67 |   });
 68 | 
 69 |   describe('Handler Requirements', () => {
 70 |     it('should require scheme when missing', async () => {
 71 |       const result = await getDeviceAppPath.handler({
 72 |         projectPath: '/path/to/project.xcodeproj',
 73 |       });
 74 |       expect(result.isError).toBe(true);
 75 |       expect(result.content[0].text).toContain('Missing required session defaults');
 76 |       expect(result.content[0].text).toContain('scheme is required');
 77 |     });
 78 | 
 79 |     it('should require project or workspace when scheme default exists', async () => {
 80 |       sessionStore.setDefaults({ scheme: 'MyScheme' });
 81 | 
 82 |       const result = await getDeviceAppPath.handler({});
 83 |       expect(result.isError).toBe(true);
 84 |       expect(result.content[0].text).toContain('Provide a project or workspace');
 85 |     });
 86 |   });
 87 | 
 88 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 89 |     // Note: Parameter validation is now handled by Zod schema validation in createTypedTool,
 90 |     // so invalid parameters never reach the logic function. Schema validation is tested above.
 91 | 
 92 |     it('should generate correct xcodebuild command for iOS', async () => {
 93 |       const calls: Array<{
 94 |         args: string[];
 95 |         logPrefix?: string;
 96 |         useShell?: boolean;
 97 |         opts?: { cwd?: string };
 98 |       }> = [];
 99 | 
100 |       const mockExecutor = (
101 |         args: string[],
102 |         logPrefix?: string,
103 |         useShell?: boolean,
104 |         opts?: { cwd?: string },
105 |         _detached?: boolean,
106 |       ) => {
107 |         calls.push({ args, logPrefix, useShell, opts });
108 |         return Promise.resolve(
109 |           createMockCommandResponse({
110 |             success: true,
111 |             output:
112 |               'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
113 |             error: undefined,
114 |           }),
115 |         );
116 |       };
117 | 
118 |       await get_device_app_pathLogic(
119 |         {
120 |           projectPath: '/path/to/project.xcodeproj',
121 |           scheme: 'MyScheme',
122 |         },
123 |         mockExecutor,
124 |       );
125 | 
126 |       expect(calls).toHaveLength(1);
127 |       expect(calls[0]).toEqual({
128 |         args: [
129 |           'xcodebuild',
130 |           '-showBuildSettings',
131 |           '-project',
132 |           '/path/to/project.xcodeproj',
133 |           '-scheme',
134 |           'MyScheme',
135 |           '-configuration',
136 |           'Debug',
137 |           '-destination',
138 |           'generic/platform=iOS',
139 |         ],
140 |         logPrefix: 'Get App Path',
141 |         useShell: true,
142 |         opts: undefined,
143 |       });
144 |     });
145 | 
146 |     it('should generate correct xcodebuild command for watchOS', async () => {
147 |       const calls: Array<{
148 |         args: string[];
149 |         logPrefix?: string;
150 |         useShell?: boolean;
151 |         opts?: { cwd?: string };
152 |       }> = [];
153 | 
154 |       const mockExecutor = (
155 |         args: string[],
156 |         logPrefix?: string,
157 |         useShell?: boolean,
158 |         opts?: { cwd?: string },
159 |         _detached?: boolean,
160 |       ) => {
161 |         calls.push({ args, logPrefix, useShell, opts });
162 |         return Promise.resolve(
163 |           createMockCommandResponse({
164 |             success: true,
165 |             output:
166 |               'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-watchos\nFULL_PRODUCT_NAME = MyApp.app\n',
167 |             error: undefined,
168 |           }),
169 |         );
170 |       };
171 | 
172 |       await get_device_app_pathLogic(
173 |         {
174 |           projectPath: '/path/to/project.xcodeproj',
175 |           scheme: 'MyScheme',
176 |           platform: 'watchOS',
177 |         },
178 |         mockExecutor,
179 |       );
180 | 
181 |       expect(calls).toHaveLength(1);
182 |       expect(calls[0]).toEqual({
183 |         args: [
184 |           'xcodebuild',
185 |           '-showBuildSettings',
186 |           '-project',
187 |           '/path/to/project.xcodeproj',
188 |           '-scheme',
189 |           'MyScheme',
190 |           '-configuration',
191 |           'Debug',
192 |           '-destination',
193 |           'generic/platform=watchOS',
194 |         ],
195 |         logPrefix: 'Get App Path',
196 |         useShell: true,
197 |         opts: undefined,
198 |       });
199 |     });
200 | 
201 |     it('should generate correct xcodebuild command for workspace with iOS', async () => {
202 |       const calls: Array<{
203 |         args: string[];
204 |         logPrefix?: string;
205 |         useShell?: boolean;
206 |         opts?: { cwd?: string };
207 |       }> = [];
208 | 
209 |       const mockExecutor = (
210 |         args: string[],
211 |         logPrefix?: string,
212 |         useShell?: boolean,
213 |         opts?: { cwd?: string },
214 |         _detached?: boolean,
215 |       ) => {
216 |         calls.push({ args, logPrefix, useShell, opts });
217 |         return Promise.resolve(
218 |           createMockCommandResponse({
219 |             success: true,
220 |             output:
221 |               'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
222 |             error: undefined,
223 |           }),
224 |         );
225 |       };
226 | 
227 |       await get_device_app_pathLogic(
228 |         {
229 |           workspacePath: '/path/to/workspace.xcworkspace',
230 |           scheme: 'MyScheme',
231 |         },
232 |         mockExecutor,
233 |       );
234 | 
235 |       expect(calls).toHaveLength(1);
236 |       expect(calls[0]).toEqual({
237 |         args: [
238 |           'xcodebuild',
239 |           '-showBuildSettings',
240 |           '-workspace',
241 |           '/path/to/workspace.xcworkspace',
242 |           '-scheme',
243 |           'MyScheme',
244 |           '-configuration',
245 |           'Debug',
246 |           '-destination',
247 |           'generic/platform=iOS',
248 |         ],
249 |         logPrefix: 'Get App Path',
250 |         useShell: true,
251 |         opts: undefined,
252 |       });
253 |     });
254 | 
255 |     it('should return exact successful app path retrieval response', async () => {
256 |       const mockExecutor = createMockExecutor({
257 |         success: true,
258 |         output:
259 |           'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
260 |       });
261 | 
262 |       const result = await get_device_app_pathLogic(
263 |         {
264 |           projectPath: '/path/to/project.xcodeproj',
265 |           scheme: 'MyScheme',
266 |         },
267 |         mockExecutor,
268 |       );
269 | 
270 |       expect(result).toEqual({
271 |         content: [
272 |           {
273 |             type: 'text',
274 |             text: '✅ App path retrieved successfully: /path/to/build/Debug-iphoneos/MyApp.app',
275 |           },
276 |           {
277 |             type: 'text',
278 |             text: 'Next Steps:\n1. Get bundle ID: get_app_bundle_id({ appPath: "/path/to/build/Debug-iphoneos/MyApp.app" })\n2. Install app on device: install_app_device({ deviceId: "DEVICE_UDID", appPath: "/path/to/build/Debug-iphoneos/MyApp.app" })\n3. Launch app on device: launch_app_device({ deviceId: "DEVICE_UDID", bundleId: "BUNDLE_ID" })',
279 |           },
280 |         ],
281 |       });
282 |     });
283 | 
284 |     it('should return exact command failure response', async () => {
285 |       const mockExecutor = createMockExecutor({
286 |         success: false,
287 |         error: 'xcodebuild: error: The project does not exist.',
288 |       });
289 | 
290 |       const result = await get_device_app_pathLogic(
291 |         {
292 |           projectPath: '/path/to/nonexistent.xcodeproj',
293 |           scheme: 'MyScheme',
294 |         },
295 |         mockExecutor,
296 |       );
297 | 
298 |       expect(result).toEqual({
299 |         content: [
300 |           {
301 |             type: 'text',
302 |             text: 'Failed to get app path: xcodebuild: error: The project does not exist.',
303 |           },
304 |         ],
305 |         isError: true,
306 |       });
307 |     });
308 | 
309 |     it('should return exact parse failure response', async () => {
310 |       const mockExecutor = createMockExecutor({
311 |         success: true,
312 |         output: 'Build settings without required fields',
313 |       });
314 | 
315 |       const result = await get_device_app_pathLogic(
316 |         {
317 |           projectPath: '/path/to/project.xcodeproj',
318 |           scheme: 'MyScheme',
319 |         },
320 |         mockExecutor,
321 |       );
322 | 
323 |       expect(result).toEqual({
324 |         content: [
325 |           {
326 |             type: 'text',
327 |             text: 'Failed to extract app path from build settings. Make sure the app has been built first.',
328 |           },
329 |         ],
330 |         isError: true,
331 |       });
332 |     });
333 | 
334 |     it('should include optional configuration parameter in command', async () => {
335 |       const calls: Array<{
336 |         args: string[];
337 |         logPrefix?: string;
338 |         useShell?: boolean;
339 |         opts?: { cwd?: string };
340 |       }> = [];
341 | 
342 |       const mockExecutor = (
343 |         args: string[],
344 |         logPrefix?: string,
345 |         useShell?: boolean,
346 |         opts?: { cwd?: string },
347 |         _detached?: boolean,
348 |       ) => {
349 |         calls.push({ args, logPrefix, useShell, opts });
350 |         return Promise.resolve(
351 |           createMockCommandResponse({
352 |             success: true,
353 |             output:
354 |               'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Release-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
355 |             error: undefined,
356 |           }),
357 |         );
358 |       };
359 | 
360 |       await get_device_app_pathLogic(
361 |         {
362 |           projectPath: '/path/to/project.xcodeproj',
363 |           scheme: 'MyScheme',
364 |           configuration: 'Release',
365 |         },
366 |         mockExecutor,
367 |       );
368 | 
369 |       expect(calls).toHaveLength(1);
370 |       expect(calls[0]).toEqual({
371 |         args: [
372 |           'xcodebuild',
373 |           '-showBuildSettings',
374 |           '-project',
375 |           '/path/to/project.xcodeproj',
376 |           '-scheme',
377 |           'MyScheme',
378 |           '-configuration',
379 |           'Release',
380 |           '-destination',
381 |           'generic/platform=iOS',
382 |         ],
383 |         logPrefix: 'Get App Path',
384 |         useShell: true,
385 |         opts: undefined,
386 |       });
387 |     });
388 | 
389 |     it('should return exact exception handling response', async () => {
390 |       const mockExecutor = (
391 |         _args: string[],
392 |         _logPrefix?: string,
393 |         _useShell?: boolean,
394 |         _opts?: { cwd?: string },
395 |         _detached?: boolean,
396 |       ) => {
397 |         return Promise.reject(new Error('Network error'));
398 |       };
399 | 
400 |       const result = await get_device_app_pathLogic(
401 |         {
402 |           projectPath: '/path/to/project.xcodeproj',
403 |           scheme: 'MyScheme',
404 |         },
405 |         mockExecutor,
406 |       );
407 | 
408 |       expect(result).toEqual({
409 |         content: [
410 |           {
411 |             type: 'text',
412 |             text: 'Error retrieving app path: Network error',
413 |           },
414 |         ],
415 |         isError: true,
416 |       });
417 |     });
418 | 
419 |     it('should return exact string error handling response', async () => {
420 |       const mockExecutor = () => {
421 |         return Promise.reject('String error');
422 |       };
423 | 
424 |       const result = await get_device_app_pathLogic(
425 |         {
426 |           projectPath: '/path/to/project.xcodeproj',
427 |           scheme: 'MyScheme',
428 |         },
429 |         mockExecutor,
430 |       );
431 | 
432 |       expect(result).toEqual({
433 |         content: [
434 |           {
435 |             type: 'text',
436 |             text: 'Error retrieving app path: String error',
437 |           },
438 |         ],
439 |         isError: true,
440 |       });
441 |     });
442 |   });
443 | });
444 | 
```

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

```typescript
  1 | /**
  2 |  * Tests for screenshot tool plugin
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import * as z from 'zod';
  7 | import {
  8 |   createMockExecutor,
  9 |   createMockFileSystemExecutor,
 10 |   createNoopExecutor,
 11 |   mockProcess,
 12 | } from '../../../../test-utils/mock-executors.ts';
 13 | import { SystemError } from '../../../../utils/responses/index.ts';
 14 | import { sessionStore } from '../../../../utils/session-store.ts';
 15 | import screenshotPlugin, { screenshotLogic } from '../screenshot.ts';
 16 | 
 17 | describe('Screenshot Plugin', () => {
 18 |   beforeEach(() => {
 19 |     sessionStore.clear();
 20 |   });
 21 | 
 22 |   describe('Export Field Validation (Literal)', () => {
 23 |     it('should have correct name', () => {
 24 |       expect(screenshotPlugin.name).toBe('screenshot');
 25 |     });
 26 | 
 27 |     it('should have correct description', () => {
 28 |       expect(screenshotPlugin.description).toBe(
 29 |         "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).",
 30 |       );
 31 |     });
 32 | 
 33 |     it('should have handler function', () => {
 34 |       expect(typeof screenshotPlugin.handler).toBe('function');
 35 |     });
 36 | 
 37 |     it('should validate schema fields with safeParse', () => {
 38 |       const schema = z.object(screenshotPlugin.schema);
 39 | 
 40 |       // Public schema is empty; ensure extra fields are stripped
 41 |       expect(schema.safeParse({}).success).toBe(true);
 42 | 
 43 |       const withSimId = schema.safeParse({
 44 |         simulatorId: '12345678-1234-4234-8234-123456789012',
 45 |       });
 46 |       expect(withSimId.success).toBe(true);
 47 |       expect('simulatorId' in (withSimId.data as Record<string, unknown>)).toBe(false);
 48 |     });
 49 |   });
 50 | 
 51 |   describe('Plugin Handler Validation', () => {
 52 |     it('should require simulatorId session default when not provided', async () => {
 53 |       const result = await screenshotPlugin.handler({});
 54 | 
 55 |       expect(result.isError).toBe(true);
 56 |       const message = result.content[0].text;
 57 |       expect(message).toContain('Missing required session defaults');
 58 |       expect(message).toContain('simulatorId is required');
 59 |       expect(message).toContain('session-set-defaults');
 60 |     });
 61 | 
 62 |     it('should validate inline simulatorId overrides', async () => {
 63 |       const result = await screenshotPlugin.handler({
 64 |         simulatorId: 'invalid-uuid',
 65 |       });
 66 | 
 67 |       expect(result.isError).toBe(true);
 68 |       const message = result.content[0].text;
 69 |       expect(message).toContain('Parameter validation failed');
 70 |       expect(message).toContain('simulatorId: Invalid Simulator UUID format');
 71 |     });
 72 |   });
 73 | 
 74 |   describe('Command Generation', () => {
 75 |     it('should generate correct xcrun simctl command for basic screenshot', async () => {
 76 |       const capturedCommands: string[][] = [];
 77 |       const trackingExecutor = async (command: string[]) => {
 78 |         capturedCommands.push(command);
 79 |         return {
 80 |           success: true,
 81 |           output: 'Screenshot saved',
 82 |           error: undefined,
 83 |           process: mockProcess,
 84 |         };
 85 |       };
 86 | 
 87 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
 88 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
 89 |         readFile: async () => mockImageBuffer.toString('utf8'),
 90 |       });
 91 | 
 92 |       await screenshotLogic(
 93 |         {
 94 |           simulatorId: '12345678-1234-4234-8234-123456789012',
 95 |         },
 96 |         trackingExecutor,
 97 |         mockFileSystemExecutor,
 98 |         { tmpdir: () => '/tmp', join: (...paths) => paths.join('/') },
 99 |         { v4: () => 'test-uuid' },
100 |       );
101 | 
102 |       // Should capture the screenshot command first
103 |       expect(capturedCommands[0]).toEqual([
104 |         'xcrun',
105 |         'simctl',
106 |         'io',
107 |         '12345678-1234-4234-8234-123456789012',
108 |         'screenshot',
109 |         '/tmp/screenshot_test-uuid.png',
110 |       ]);
111 |     });
112 | 
113 |     it('should generate correct xcrun simctl command with different simulator UUID', async () => {
114 |       const capturedCommands: string[][] = [];
115 |       const trackingExecutor = async (command: string[]) => {
116 |         capturedCommands.push(command);
117 |         return {
118 |           success: true,
119 |           output: 'Screenshot saved',
120 |           error: undefined,
121 |           process: mockProcess,
122 |         };
123 |       };
124 | 
125 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
126 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
127 |         readFile: async () => mockImageBuffer.toString('utf8'),
128 |       });
129 | 
130 |       await screenshotLogic(
131 |         {
132 |           simulatorId: 'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
133 |         },
134 |         trackingExecutor,
135 |         mockFileSystemExecutor,
136 |         { tmpdir: () => '/var/tmp', join: (...paths) => paths.join('/') },
137 |         { v4: () => 'another-uuid' },
138 |       );
139 | 
140 |       expect(capturedCommands[0]).toEqual([
141 |         'xcrun',
142 |         'simctl',
143 |         'io',
144 |         'ABCDEF12-3456-7890-ABCD-ABCDEFABCDEF',
145 |         'screenshot',
146 |         '/var/tmp/screenshot_another-uuid.png',
147 |       ]);
148 |     });
149 | 
150 |     it('should generate correct xcrun simctl command with custom path dependencies', async () => {
151 |       const capturedCommands: string[][] = [];
152 |       const trackingExecutor = async (command: string[]) => {
153 |         capturedCommands.push(command);
154 |         return {
155 |           success: true,
156 |           output: 'Screenshot saved',
157 |           error: undefined,
158 |           process: mockProcess,
159 |         };
160 |       };
161 | 
162 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
163 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
164 |         readFile: async () => mockImageBuffer.toString('utf8'),
165 |       });
166 | 
167 |       await screenshotLogic(
168 |         {
169 |           simulatorId: '98765432-1098-7654-3210-987654321098',
170 |         },
171 |         trackingExecutor,
172 |         mockFileSystemExecutor,
173 |         {
174 |           tmpdir: () => '/custom/temp/dir',
175 |           join: (...paths) => paths.join('\\'), // Windows-style path joining
176 |         },
177 |         { v4: () => 'custom-uuid' },
178 |       );
179 | 
180 |       expect(capturedCommands[0]).toEqual([
181 |         'xcrun',
182 |         'simctl',
183 |         'io',
184 |         '98765432-1098-7654-3210-987654321098',
185 |         'screenshot',
186 |         '/custom/temp/dir\\screenshot_custom-uuid.png',
187 |       ]);
188 |     });
189 | 
190 |     it('should generate correct xcrun simctl command with generated UUID when no UUID deps provided', async () => {
191 |       const capturedCommands: string[][] = [];
192 |       const trackingExecutor = async (command: string[]) => {
193 |         capturedCommands.push(command);
194 |         return {
195 |           success: true,
196 |           output: 'Screenshot saved',
197 |           error: undefined,
198 |           process: mockProcess,
199 |         };
200 |       };
201 | 
202 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
203 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
204 |         readFile: async () => mockImageBuffer.toString('utf8'),
205 |       });
206 | 
207 |       await screenshotLogic(
208 |         {
209 |           simulatorId: '12345678-1234-4234-8234-123456789012',
210 |         },
211 |         trackingExecutor,
212 |         mockFileSystemExecutor,
213 |         { tmpdir: () => '/tmp', join: (...paths) => paths.join('/') },
214 |         // No UUID deps provided - should use real uuidv4()
215 |       );
216 | 
217 |       // Verify the command structure but not the exact UUID since it's generated
218 |       expect(capturedCommands[0].slice(0, 5)).toEqual([
219 |         'xcrun',
220 |         'simctl',
221 |         'io',
222 |         '12345678-1234-4234-8234-123456789012',
223 |         'screenshot',
224 |       ]);
225 |       expect(capturedCommands[0][5]).toMatch(/^\/tmp\/screenshot_[a-f0-9-]+\.png$/);
226 |     });
227 |   });
228 | 
229 |   describe('Handler Behavior (Complete Literal Returns)', () => {
230 |     it('should handle parameter validation via plugin handler (not logic function)', async () => {
231 |       // Note: With Zod validation in createTypedTool, the screenshotLogic function
232 |       // will never receive invalid parameters - validation happens at the handler level.
233 |       // This test documents that screenshotLogic assumes valid parameters.
234 |       const result = await screenshotLogic(
235 |         {
236 |           simulatorId: '12345678-1234-4234-8234-123456789012',
237 |         },
238 |         createMockExecutor({
239 |           success: true,
240 |           output: 'Screenshot saved',
241 |           error: undefined,
242 |         }),
243 |         createMockFileSystemExecutor({
244 |           readFile: async () => Buffer.from('fake-image-data', 'utf8').toString('utf8'),
245 |         }),
246 |       );
247 | 
248 |       expect(result.isError).toBe(false);
249 |       expect(result.content[0].type).toBe('image');
250 |     });
251 | 
252 |     it('should return success for valid screenshot capture', async () => {
253 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
254 | 
255 |       const mockExecutor = createMockExecutor({
256 |         success: true,
257 |         output: 'Screenshot saved',
258 |         error: undefined,
259 |       });
260 | 
261 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
262 |         readFile: async () => mockImageBuffer.toString('utf8'),
263 |       });
264 | 
265 |       const result = await screenshotLogic(
266 |         {
267 |           simulatorId: '12345678-1234-4234-8234-123456789012',
268 |         },
269 |         mockExecutor,
270 |         mockFileSystemExecutor,
271 |       );
272 | 
273 |       expect(result).toEqual({
274 |         content: [
275 |           {
276 |             type: 'image',
277 |             data: 'fake-image-data',
278 |             mimeType: 'image/jpeg',
279 |           },
280 |         ],
281 |         isError: false,
282 |       });
283 |     });
284 | 
285 |     it('should handle command execution failure', async () => {
286 |       const mockExecutor = createMockExecutor({
287 |         success: false,
288 |         output: '',
289 |         error: 'Simulator not found',
290 |       });
291 | 
292 |       const result = await screenshotLogic(
293 |         {
294 |           simulatorId: '12345678-1234-4234-8234-123456789012',
295 |         },
296 |         mockExecutor,
297 |         createMockFileSystemExecutor(),
298 |       );
299 | 
300 |       expect(result).toEqual({
301 |         content: [
302 |           {
303 |             type: 'text' as const,
304 |             text: 'Error: System error executing screenshot: Failed to capture screenshot: Simulator not found',
305 |           },
306 |         ],
307 |         isError: true,
308 |       });
309 |     });
310 | 
311 |     it('should handle file reading errors', async () => {
312 |       const mockExecutor = createMockExecutor({
313 |         success: true,
314 |         output: 'Screenshot saved',
315 |         error: undefined,
316 |       });
317 | 
318 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
319 |         readFile: async () => {
320 |           throw new Error('File not found');
321 |         },
322 |       });
323 | 
324 |       const result = await screenshotLogic(
325 |         {
326 |           simulatorId: '12345678-1234-4234-8234-123456789012',
327 |         },
328 |         mockExecutor,
329 |         mockFileSystemExecutor,
330 |       );
331 | 
332 |       expect(result).toEqual({
333 |         content: [
334 |           {
335 |             type: 'text' as const,
336 |             text: 'Error: Screenshot captured but failed to process image file: File not found',
337 |           },
338 |         ],
339 |         isError: true,
340 |       });
341 |     });
342 | 
343 |     it('should handle file cleanup errors gracefully', async () => {
344 |       const mockImageBuffer = Buffer.from('fake-image-data', 'utf8');
345 | 
346 |       const mockExecutor = createMockExecutor({
347 |         success: true,
348 |         output: 'Screenshot saved',
349 |         error: undefined,
350 |       });
351 | 
352 |       const mockFileSystemExecutor = createMockFileSystemExecutor({
353 |         readFile: async () => mockImageBuffer.toString('utf8'),
354 |         // unlink method is not overridden, so it will use the default (no-op)
355 |         // which simulates the cleanup failure being caught and logged
356 |       });
357 | 
358 |       const result = await screenshotLogic(
359 |         {
360 |           simulatorId: '12345678-1234-4234-8234-123456789012',
361 |         },
362 |         mockExecutor,
363 |         mockFileSystemExecutor,
364 |       );
365 | 
366 |       // Should still return successful result despite cleanup failure
367 |       expect(result).toEqual({
368 |         content: [
369 |           {
370 |             type: 'image',
371 |             data: 'fake-image-data',
372 |             mimeType: 'image/jpeg',
373 |           },
374 |         ],
375 |         isError: false,
376 |       });
377 |     });
378 | 
379 |     it('should handle SystemError from command execution', async () => {
380 |       const mockExecutor = async () => {
381 |         throw new SystemError('System error occurred');
382 |       };
383 | 
384 |       const result = await screenshotLogic(
385 |         {
386 |           simulatorId: '12345678-1234-4234-8234-123456789012',
387 |         },
388 |         mockExecutor,
389 |         createMockFileSystemExecutor(),
390 |       );
391 | 
392 |       expect(result).toEqual({
393 |         content: [
394 |           {
395 |             type: 'text' as const,
396 |             text: 'Error: System error executing screenshot: System error occurred',
397 |           },
398 |         ],
399 |         isError: true,
400 |       });
401 |     });
402 | 
403 |     it('should handle unexpected Error objects', async () => {
404 |       const mockExecutor = async () => {
405 |         throw new Error('Unexpected error');
406 |       };
407 | 
408 |       const result = await screenshotLogic(
409 |         {
410 |           simulatorId: '12345678-1234-4234-8234-123456789012',
411 |         },
412 |         mockExecutor,
413 |         createMockFileSystemExecutor(),
414 |       );
415 | 
416 |       expect(result).toEqual({
417 |         content: [
418 |           { type: 'text' as const, text: 'Error: An unexpected error occurred: Unexpected error' },
419 |         ],
420 |         isError: true,
421 |       });
422 |     });
423 | 
424 |     it('should handle unexpected string errors', async () => {
425 |       const mockExecutor = async () => {
426 |         throw 'String error';
427 |       };
428 | 
429 |       const result = await screenshotLogic(
430 |         {
431 |           simulatorId: '12345678-1234-4234-8234-123456789012',
432 |         },
433 |         mockExecutor,
434 |         createMockFileSystemExecutor(),
435 |       );
436 | 
437 |       expect(result).toEqual({
438 |         content: [
439 |           { type: 'text' as const, text: 'Error: An unexpected error occurred: String error' },
440 |         ],
441 |         isError: true,
442 |       });
443 |     });
444 |   });
445 | });
446 | 
```

--------------------------------------------------------------------------------
/docs/dev/NODEJS_2025.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Modern Node.js Development Guide
  2 | 
  3 | This guide provides actionable instructions for AI agents to apply modern Node.js patterns when the scenarios are applicable. Use these patterns when creating or modifying Node.js code that fits these use cases.
  4 | 
  5 | ## Core Principles
  6 | 
  7 | **WHEN APPLICABLE** apply these modern patterns:
  8 | 
  9 | 1. **Use ES Modules** with `node:` prefix for built-in modules
 10 | 2. **Leverage built-in APIs** over external dependencies when the functionality matches
 11 | 3. **Use top-level await** instead of IIFE patterns when initialization is needed
 12 | 4. **Implement structured error handling** with proper context when handling application errors
 13 | 5. **Use built-in testing** over external test frameworks when adding tests
 14 | 6. **Apply modern async patterns** for better performance when dealing with async operations
 15 | 
 16 | ## 1. Module System Patterns
 17 | 
 18 | ### WHEN USING MODULES: ES Modules with node: Prefix
 19 | 
 20 | **✅ DO THIS:**
 21 | ```javascript
 22 | // Use ES modules with node: prefix for built-ins
 23 | import { readFile } from 'node:fs/promises';
 24 | import { createServer } from 'node:http';
 25 | import { EventEmitter } from 'node:events';
 26 | 
 27 | export function myFunction() {
 28 |   return 'modern code';
 29 | }
 30 | ```
 31 | 
 32 | **❌ AVOID:**
 33 | ```javascript
 34 | // Don't use CommonJS or bare imports for built-ins
 35 | const fs = require('fs');
 36 | const { readFile } = require('fs/promises');
 37 | import { readFile } from 'fs/promises'; // Missing node: prefix
 38 | ```
 39 | 
 40 | ### WHEN INITIALIZING: Top-Level Await
 41 | 
 42 | **✅ DO THIS:**
 43 | ```javascript
 44 | // Use top-level await for initialization
 45 | import { readFile } from 'node:fs/promises';
 46 | 
 47 | const config = JSON.parse(await readFile('config.json', 'utf8'));
 48 | const server = createServer(/* ... */);
 49 | 
 50 | console.log('App started with config:', config.appName);
 51 | ```
 52 | 
 53 | **❌ AVOID:**
 54 | ```javascript
 55 | // Don't wrap in IIFE
 56 | (async () => {
 57 |   const config = JSON.parse(await readFile('config.json', 'utf8'));
 58 |   // ...
 59 | })();
 60 | ```
 61 | 
 62 | ### WHEN USING ES MODULES: Package.json Settings
 63 | 
 64 | **✅ ENSURE package.json includes:**
 65 | ```json
 66 | {
 67 |   "type": "module",
 68 |   "engines": {
 69 |     "node": ">=20.0.0"
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | ## 2. HTTP and Network Patterns
 75 | 
 76 | ### WHEN MAKING HTTP REQUESTS: Use Built-in fetch
 77 | 
 78 | **✅ DO THIS:**
 79 | ```javascript
 80 | // Use built-in fetch with AbortSignal.timeout
 81 | async function fetchData(url) {
 82 |   try {
 83 |     const response = await fetch(url, {
 84 |       signal: AbortSignal.timeout(5000)
 85 |     });
 86 | 
 87 |     if (!response.ok) {
 88 |       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 89 |     }
 90 | 
 91 |     return await response.json();
 92 |   } catch (error) {
 93 |     if (error.name === 'TimeoutError') {
 94 |       throw new Error('Request timed out');
 95 |     }
 96 |     throw error;
 97 |   }
 98 | }
 99 | ```
100 | 
101 | **❌ AVOID:**
102 | ```javascript
103 | // Don't add axios, node-fetch, or similar dependencies
104 | const axios = require('axios');
105 | const response = await axios.get(url);
106 | ```
107 | 
108 | ### WHEN NEEDING CANCELLATION: AbortController Pattern
109 | 
110 | **✅ DO THIS:**
111 | ```javascript
112 | // Implement proper cancellation
113 | const controller = new AbortController();
114 | setTimeout(() => controller.abort(), 10000);
115 | 
116 | try {
117 |   const data = await fetch(url, { signal: controller.signal });
118 |   console.log('Data received:', data);
119 | } catch (error) {
120 |   if (error.name === 'AbortError') {
121 |     console.log('Request was cancelled');
122 |   } else {
123 |     console.error('Unexpected error:', error);
124 |   }
125 | }
126 | ```
127 | 
128 | ## 3. Testing Patterns
129 | 
130 | ### WHEN ADDING TESTS: Use Built-in Test Runner
131 | 
132 | **✅ DO THIS:**
133 | ```javascript
134 | // Use node:test instead of external frameworks
135 | import { test, describe } from 'node:test';
136 | import assert from 'node:assert';
137 | 
138 | describe('My Module', () => {
139 |   test('should work correctly', () => {
140 |     assert.strictEqual(myFunction(), 'expected');
141 |   });
142 | 
143 |   test('should handle async operations', async () => {
144 |     const result = await myAsyncFunction();
145 |     assert.strictEqual(result, 'expected');
146 |   });
147 | 
148 |   test('should throw on invalid input', () => {
149 |     assert.throws(() => myFunction('invalid'), /Expected error/);
150 |   });
151 | });
152 | ```
153 | 
154 | **✅ RECOMMENDED package.json scripts:**
155 | ```json
156 | {
157 |   "scripts": {
158 |     "test": "node --test",
159 |     "test:watch": "node --test --watch",
160 |     "test:coverage": "node --test --experimental-test-coverage"
161 |   }
162 | }
163 | ```
164 | 
165 | **❌ AVOID:**
166 | ```javascript
167 | // Don't add Jest, Mocha, or other test frameworks unless specifically required
168 | ```
169 | 
170 | ## 4. Async Pattern Recommendations
171 | 
172 | ### WHEN HANDLING MULTIPLE ASYNC OPERATIONS: Parallel Execution with Promise.all
173 | 
174 | **✅ DO THIS:**
175 | ```javascript
176 | // Execute independent operations in parallel
177 | async function processData() {
178 |   try {
179 |     const [config, userData] = await Promise.all([
180 |       readFile('config.json', 'utf8'),
181 |       fetch('/api/user').then(r => r.json())
182 |     ]);
183 | 
184 |     const processed = processUserData(userData, JSON.parse(config));
185 |     await writeFile('output.json', JSON.stringify(processed, null, 2));
186 | 
187 |     return processed;
188 |   } catch (error) {
189 |     console.error('Processing failed:', {
190 |       error: error.message,
191 |       stack: error.stack,
192 |       timestamp: new Date().toISOString()
193 |     });
194 |     throw error;
195 |   }
196 | }
197 | ```
198 | 
199 | ### WHEN PROCESSING EVENT STREAMS: AsyncIterators Pattern
200 | 
201 | **✅ DO THIS:**
202 | ```javascript
203 | // Use async iterators for event processing
204 | import { EventEmitter } from 'node:events';
205 | 
206 | class DataProcessor extends EventEmitter {
207 |   async *processStream() {
208 |     for (let i = 0; i < 10; i++) {
209 |       this.emit('data', `chunk-${i}`);
210 |       yield `processed-${i}`;
211 |       await new Promise(resolve => setTimeout(resolve, 100));
212 |     }
213 |     this.emit('end');
214 |   }
215 | }
216 | 
217 | // Consume with for-await-of
218 | const processor = new DataProcessor();
219 | for await (const result of processor.processStream()) {
220 |   console.log('Processed:', result);
221 | }
222 | ```
223 | 
224 | ## 5. Stream Processing Patterns
225 | 
226 | ### WHEN PROCESSING STREAMS: Use pipeline with Promises
227 | 
228 | **✅ DO THIS:**
229 | ```javascript
230 | import { pipeline } from 'node:stream/promises';
231 | import { createReadStream, createWriteStream } from 'node:fs';
232 | import { Transform } from 'node:stream';
233 | 
234 | // Always use pipeline for stream processing
235 | async function processFile(inputFile, outputFile) {
236 |   try {
237 |     await pipeline(
238 |       createReadStream(inputFile),
239 |       new Transform({
240 |         transform(chunk, encoding, callback) {
241 |           this.push(chunk.toString().toUpperCase());
242 |           callback();
243 |         }
244 |       }),
245 |       createWriteStream(outputFile)
246 |     );
247 |     console.log('File processed successfully');
248 |   } catch (error) {
249 |     console.error('Pipeline failed:', error);
250 |     throw error;
251 |   }
252 | }
253 | ```
254 | 
255 | ### WHEN NEEDING BROWSER COMPATIBILITY: Web Streams
256 | 
257 | **✅ DO THIS:**
258 | ```javascript
259 | import { Readable } from 'node:stream';
260 | 
261 | // Convert between Web Streams and Node streams when needed
262 | const webReadable = new ReadableStream({
263 |   start(controller) {
264 |     controller.enqueue('Hello ');
265 |     controller.enqueue('World!');
266 |     controller.close();
267 |   }
268 | });
269 | 
270 | const nodeStream = Readable.fromWeb(webReadable);
271 | ```
272 | 
273 | ## 6. CPU-Intensive Task Patterns
274 | 
275 | ### WHEN DOING HEAVY COMPUTATION: Worker Threads
276 | 
277 | **✅ DO THIS:**
278 | ```javascript
279 | // worker.js - Separate file for CPU-intensive tasks
280 | import { parentPort, workerData } from 'node:worker_threads';
281 | 
282 | function heavyComputation(data) {
283 |   // CPU-intensive work here
284 |   return processedData;
285 | }
286 | 
287 | const result = heavyComputation(workerData);
288 | parentPort.postMessage(result);
289 | ```
290 | 
291 | ```javascript
292 | // main.js - Delegate to worker
293 | import { Worker } from 'node:worker_threads';
294 | import { fileURLToPath } from 'node:url';
295 | 
296 | async function processHeavyTask(data) {
297 |   return new Promise((resolve, reject) => {
298 |     const worker = new Worker(
299 |       fileURLToPath(new URL('./worker.js', import.meta.url)),
300 |       { workerData: data }
301 |     );
302 | 
303 |     worker.on('message', resolve);
304 |     worker.on('error', reject);
305 |     worker.on('exit', (code) => {
306 |       if (code !== 0) {
307 |         reject(new Error(`Worker stopped with exit code ${code}`));
308 |       }
309 |     });
310 |   });
311 | }
312 | ```
313 | 
314 | ## 7. Development Configuration Patterns
315 | 
316 | ### FOR NEW PROJECTS: Modern package.json
317 | 
318 | **✅ RECOMMENDED for new projects:**
319 | ```json
320 | {
321 |   "name": "modern-node-app",
322 |   "type": "module",
323 |   "engines": {
324 |     "node": ">=20.0.0"
325 |   },
326 |   "scripts": {
327 |     "dev": "node --watch --env-file=.env app.js",
328 |     "test": "node --test --watch",
329 |     "start": "node app.js"
330 |   }
331 | }
332 | ```
333 | 
334 | ### WHEN LOADING ENVIRONMENT VARIABLES: Built-in Support
335 | 
336 | **✅ DO THIS:**
337 | ```javascript
338 | // Use --env-file flag instead of dotenv package
339 | // Environment variables are automatically available
340 | console.log('Database URL:', process.env.DATABASE_URL);
341 | console.log('API Key loaded:', process.env.API_KEY ? 'Yes' : 'No');
342 | ```
343 | 
344 | **❌ AVOID:**
345 | ```javascript
346 | // Don't add dotenv dependency
347 | require('dotenv').config();
348 | ```
349 | 
350 | ## 8. Error Handling Patterns
351 | 
352 | ### WHEN CREATING CUSTOM ERRORS: Structured Error Classes
353 | 
354 | **✅ DO THIS:**
355 | ```javascript
356 | class AppError extends Error {
357 |   constructor(message, code, statusCode = 500, context = {}) {
358 |     super(message);
359 |     this.name = 'AppError';
360 |     this.code = code;
361 |     this.statusCode = statusCode;
362 |     this.context = context;
363 |     this.timestamp = new Date().toISOString();
364 |   }
365 | 
366 |   toJSON() {
367 |     return {
368 |       name: this.name,
369 |       message: this.message,
370 |       code: this.code,
371 |       statusCode: this.statusCode,
372 |       context: this.context,
373 |       timestamp: this.timestamp,
374 |       stack: this.stack
375 |     };
376 |   }
377 | }
378 | 
379 | // Usage with rich context
380 | throw new AppError(
381 |   'Database connection failed',
382 |   'DB_CONNECTION_ERROR',
383 |   503,
384 |   { host: 'localhost', port: 5432, retryAttempt: 3 }
385 | );
386 | ```
387 | 
388 | ## 9. Performance Monitoring Patterns
389 | 
390 | ### WHEN MONITORING PERFORMANCE: Built-in Performance APIs
391 | 
392 | **✅ DO THIS:**
393 | ```javascript
394 | import { PerformanceObserver, performance } from 'node:perf_hooks';
395 | 
396 | // Set up performance monitoring
397 | const obs = new PerformanceObserver((list) => {
398 |   for (const entry of list.getEntries()) {
399 |     if (entry.duration > 100) {
400 |       console.log(`Slow operation: ${entry.name} took ${entry.duration}ms`);
401 |     }
402 |   }
403 | });
404 | obs.observe({ entryTypes: ['function', 'http', 'dns'] });
405 | 
406 | // Instrument operations
407 | async function processLargeDataset(data) {
408 |   performance.mark('processing-start');
409 |   
410 |   const result = await heavyProcessing(data);
411 |   
412 |   performance.mark('processing-end');
413 |   performance.measure('data-processing', 'processing-start', 'processing-end');
414 |   
415 |   return result;
416 | }
417 | ```
418 | 
419 | ## 10. Module Organization Patterns
420 | 
421 | ### WHEN ORGANIZING INTERNAL MODULES: Import Maps
422 | 
423 | **✅ DO THIS in package.json:**
424 | ```json
425 | {
426 |   "imports": {
427 |     "#config": "./src/config/index.js",
428 |     "#utils/*": "./src/utils/*.js",
429 |     "#db": "./src/database/connection.js"
430 |   }
431 | }
432 | ```
433 | 
434 | **✅ Use in code:**
435 | ```javascript
436 | // Clean internal imports
437 | import config from '#config';
438 | import { logger, validator } from '#utils/common';
439 | import db from '#db';
440 | ```
441 | 
442 | ### WHEN LOADING CONDITIONALLY: Dynamic Imports
443 | 
444 | **✅ DO THIS:**
445 | ```javascript
446 | // Load features based on environment
447 | async function loadDatabaseAdapter() {
448 |   const dbType = process.env.DATABASE_TYPE || 'sqlite';
449 |   
450 |   try {
451 |     const adapter = await import(`#db/adapters/${dbType}`);
452 |     return adapter.default;
453 |   } catch (error) {
454 |     console.warn(`Database adapter ${dbType} not available, falling back to sqlite`);
455 |     const fallback = await import('#db/adapters/sqlite');
456 |     return fallback.default;
457 |   }
458 | }
459 | ```
460 | 
461 | ## 11. Diagnostic Patterns
462 | 
463 | ### WHEN ADDING OBSERVABILITY: Diagnostic Channels
464 | 
465 | **✅ DO THIS:**
466 | ```javascript
467 | import diagnostics_channel from 'node:diagnostics_channel';
468 | 
469 | // Create diagnostic channels
470 | const dbChannel = diagnostics_channel.channel('app:database');
471 | 
472 | // Subscribe to events
473 | dbChannel.subscribe((message) => {
474 |   console.log('Database operation:', {
475 |     operation: message.operation,
476 |     duration: message.duration,
477 |     query: message.query
478 |   });
479 | });
480 | 
481 | // Publish diagnostic information
482 | async function queryDatabase(sql, params) {
483 |   const start = performance.now();
484 |   
485 |   try {
486 |     const result = await db.query(sql, params);
487 |     
488 |     dbChannel.publish({
489 |       operation: 'query',
490 |       sql,
491 |       params,
492 |       duration: performance.now() - start,
493 |       success: true
494 |     });
495 |     
496 |     return result;
497 |   } catch (error) {
498 |     dbChannel.publish({
499 |       operation: 'query',
500 |       sql,
501 |       params,
502 |       duration: performance.now() - start,
503 |       success: false,
504 |       error: error.message
505 |     });
506 |     throw error;
507 |   }
508 | }
509 | ```
510 | 
511 | ## Modernization Checklist
512 | 
513 | When working with Node.js code, consider applying these patterns where applicable:
514 | 
515 | - [ ] `"type": "module"` in package.json
516 | - [ ] `"engines": {"node": ">=20.0.0"}` specified
517 | - [ ] All built-in imports use `node:` prefix
518 | - [ ] Using `fetch()` instead of HTTP libraries
519 | - [ ] Using `node --test` instead of external test frameworks
520 | - [ ] Using `--watch` and `--env-file` flags
521 | - [ ] Implementing structured error handling
522 | - [ ] Using `Promise.all()` for parallel operations
523 | - [ ] Using `pipeline()` for stream processing
524 | - [ ] Implementing performance monitoring where appropriate
525 | - [ ] Using worker threads for CPU-intensive tasks
526 | - [ ] Using import maps for internal modules
527 | 
528 | ## Dependencies to Remove
529 | 
530 | When modernizing, remove these dependencies if present:
531 | 
532 | - `axios`, `node-fetch`, `got` → Use built-in `fetch()`
533 | - `jest`, `mocha`, `ava` → Use `node:test`
534 | - `nodemon` → Use `node --watch`
535 | - `dotenv` → Use `--env-file`
536 | - `cross-env` → Use native environment handling
537 | 
538 | ## Security Patterns
539 | 
540 | **WHEN SECURITY IS A CONCERN** apply these practices:
541 | 
542 | ```bash
543 | # Use permission model for enhanced security
544 | node --experimental-permission --allow-fs-read=./data --allow-fs-write=./logs app.js
545 | 
546 | # Network restrictions
547 | node --experimental-permission --allow-net=api.example.com app.js
548 | ```
549 | 
550 | This guide provides modern Node.js patterns to apply when the specific scenarios are encountered, ensuring code follows 2025 best practices for performance, security, and maintainability without forcing unnecessary changes.
```

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

```typescript
  1 | /**
  2 |  * Tests for swift_package_list plugin
  3 |  * Following CLAUDE.md testing standards with literal validation
  4 |  * Using pure dependency injection for deterministic testing
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach } from 'vitest';
  8 | import swiftPackageList, { swift_package_listLogic } from '../swift_package_list.ts';
  9 | 
 10 | describe('swift_package_list plugin', () => {
 11 |   // No mocks to clear with pure dependency injection
 12 | 
 13 |   describe('Export Field Validation (Literal)', () => {
 14 |     it('should have correct name', () => {
 15 |       expect(swiftPackageList.name).toBe('swift_package_list');
 16 |     });
 17 | 
 18 |     it('should have correct description', () => {
 19 |       expect(swiftPackageList.description).toBe('Lists currently running Swift Package processes');
 20 |     });
 21 | 
 22 |     it('should have handler function', () => {
 23 |       expect(typeof swiftPackageList.handler).toBe('function');
 24 |     });
 25 | 
 26 |     it('should validate schema correctly', () => {
 27 |       // The schema is an empty object, so any input should be valid
 28 |       expect(typeof swiftPackageList.schema).toBe('object');
 29 |       expect(Object.keys(swiftPackageList.schema)).toEqual([]);
 30 |     });
 31 |   });
 32 | 
 33 |   describe('Handler Behavior (Complete Literal Returns)', () => {
 34 |     it('should return empty list when no processes are running', async () => {
 35 |       // Create empty mock process map
 36 |       const mockProcessMap = new Map();
 37 | 
 38 |       // Use pure dependency injection with stub functions
 39 |       const mockArrayFrom = () => [];
 40 |       const mockDateNow = () => Date.now();
 41 | 
 42 |       const result = await swift_package_listLogic(
 43 |         {},
 44 |         {
 45 |           processMap: mockProcessMap,
 46 |           arrayFrom: mockArrayFrom,
 47 |           dateNow: mockDateNow,
 48 |         },
 49 |       );
 50 | 
 51 |       expect(result).toEqual({
 52 |         content: [
 53 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
 54 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
 55 |         ],
 56 |       });
 57 |     });
 58 | 
 59 |     it('should handle empty args object', async () => {
 60 |       // Create empty mock process map
 61 |       const mockProcessMap = new Map();
 62 | 
 63 |       // Use pure dependency injection with stub functions
 64 |       const mockArrayFrom = () => [];
 65 |       const mockDateNow = () => Date.now();
 66 | 
 67 |       const result = await swift_package_listLogic(
 68 |         {},
 69 |         {
 70 |           processMap: mockProcessMap,
 71 |           arrayFrom: mockArrayFrom,
 72 |           dateNow: mockDateNow,
 73 |         },
 74 |       );
 75 | 
 76 |       expect(result).toEqual({
 77 |         content: [
 78 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
 79 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
 80 |         ],
 81 |       });
 82 |     });
 83 | 
 84 |     it('should handle null args', async () => {
 85 |       // Create empty mock process map
 86 |       const mockProcessMap = new Map();
 87 | 
 88 |       // Use pure dependency injection with stub functions
 89 |       const mockArrayFrom = () => [];
 90 |       const mockDateNow = () => Date.now();
 91 | 
 92 |       const result = await swift_package_listLogic(null, {
 93 |         processMap: mockProcessMap,
 94 |         arrayFrom: mockArrayFrom,
 95 |         dateNow: mockDateNow,
 96 |       });
 97 | 
 98 |       expect(result).toEqual({
 99 |         content: [
100 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
101 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
102 |         ],
103 |       });
104 |     });
105 | 
106 |     it('should handle undefined args', async () => {
107 |       // Create empty mock process map
108 |       const mockProcessMap = new Map();
109 | 
110 |       // Use pure dependency injection with stub functions
111 |       const mockArrayFrom = () => [];
112 |       const mockDateNow = () => Date.now();
113 | 
114 |       const result = await swift_package_listLogic(undefined, {
115 |         processMap: mockProcessMap,
116 |         arrayFrom: mockArrayFrom,
117 |         dateNow: mockDateNow,
118 |       });
119 | 
120 |       expect(result).toEqual({
121 |         content: [
122 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
123 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
124 |         ],
125 |       });
126 |     });
127 | 
128 |     it('should handle args with extra properties', async () => {
129 |       // Create empty mock process map
130 |       const mockProcessMap = new Map();
131 | 
132 |       // Use pure dependency injection with stub functions
133 |       const mockArrayFrom = () => [];
134 |       const mockDateNow = () => Date.now();
135 | 
136 |       const result = await swift_package_listLogic(
137 |         {
138 |           extraProperty: 'value',
139 |           anotherProperty: 123,
140 |         },
141 |         {
142 |           processMap: mockProcessMap,
143 |           arrayFrom: mockArrayFrom,
144 |           dateNow: mockDateNow,
145 |         },
146 |       );
147 | 
148 |       expect(result).toEqual({
149 |         content: [
150 |           { type: 'text', text: 'ℹ️ No Swift Package processes currently running.' },
151 |           { type: 'text', text: '💡 Use swift_package_run to start an executable.' },
152 |         ],
153 |       });
154 |     });
155 | 
156 |     it('should return single process when one process is running', async () => {
157 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
158 |       const mockProcess = {
159 |         executableName: 'MyApp',
160 |         packagePath: '/test/package',
161 |         startedAt: startedAt,
162 |       };
163 | 
164 |       // Create mock process map with one process
165 |       const mockProcessMap = new Map([[12345, mockProcess]]);
166 | 
167 |       // Use pure dependency injection with stub functions
168 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
169 |       const mockDateNow = () => startedAt.getTime() + 5000; // 5 seconds after start
170 | 
171 |       const result = await swift_package_listLogic(
172 |         {},
173 |         {
174 |           processMap: mockProcessMap,
175 |           arrayFrom: mockArrayFrom,
176 |           dateNow: mockDateNow,
177 |         },
178 |       );
179 | 
180 |       expect(result).toEqual({
181 |         content: [
182 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
183 |           { type: 'text', text: '  • PID 12345: MyApp (/test/package) - running 5s' },
184 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
185 |         ],
186 |       });
187 |     });
188 | 
189 |     it('should return multiple processes when several are running', async () => {
190 |       const startedAt1 = new Date('2023-01-01T10:00:00.000Z');
191 |       const startedAt2 = new Date('2023-01-01T10:00:07.000Z');
192 | 
193 |       const mockProcess1 = {
194 |         executableName: 'MyApp',
195 |         packagePath: '/test/package1',
196 |         startedAt: startedAt1,
197 |       };
198 | 
199 |       const mockProcess2 = {
200 |         executableName: undefined, // Test default executable name
201 |         packagePath: '/test/package2',
202 |         startedAt: startedAt2,
203 |       };
204 | 
205 |       // Create mock process map with multiple processes
206 |       const mockProcessMap = new Map<
207 |         number,
208 |         { executableName?: string; packagePath: string; startedAt: Date }
209 |       >([
210 |         [12345, mockProcess1],
211 |         [12346, mockProcess2],
212 |       ]);
213 | 
214 |       // Use pure dependency injection with stub functions
215 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
216 |       const mockDateNow = () => startedAt1.getTime() + 10000; // 10 seconds after first start
217 | 
218 |       const result = await swift_package_listLogic(
219 |         {},
220 |         {
221 |           processMap: mockProcessMap,
222 |           arrayFrom: mockArrayFrom,
223 |           dateNow: mockDateNow,
224 |         },
225 |       );
226 | 
227 |       expect(result).toEqual({
228 |         content: [
229 |           { type: 'text', text: '📋 Active Swift Package processes (2):' },
230 |           { type: 'text', text: '  • PID 12345: MyApp (/test/package1) - running 10s' },
231 |           { type: 'text', text: '  • PID 12346: default (/test/package2) - running 3s' },
232 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
233 |         ],
234 |       });
235 |     });
236 | 
237 |     it('should handle process with missing executableName', async () => {
238 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
239 |       const mockProcess = {
240 |         executableName: undefined, // Test missing executable name
241 |         packagePath: '/test/package',
242 |         startedAt: startedAt,
243 |       };
244 | 
245 |       // Create mock process map with one process
246 |       const mockProcessMap = new Map<
247 |         number,
248 |         { executableName?: string; packagePath: string; startedAt: Date }
249 |       >([[12345, mockProcess]]);
250 | 
251 |       // Use pure dependency injection with stub functions
252 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
253 |       const mockDateNow = () => startedAt.getTime() + 1000; // 1 second after start
254 | 
255 |       const result = await swift_package_listLogic(
256 |         {},
257 |         {
258 |           processMap: mockProcessMap,
259 |           arrayFrom: mockArrayFrom,
260 |           dateNow: mockDateNow,
261 |         },
262 |       );
263 | 
264 |       expect(result).toEqual({
265 |         content: [
266 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
267 |           { type: 'text', text: '  • PID 12345: default (/test/package) - running 1s' },
268 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
269 |         ],
270 |       });
271 |     });
272 | 
273 |     it('should handle process with empty string executableName', async () => {
274 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
275 |       const mockProcess = {
276 |         executableName: '', // Test empty string executable name
277 |         packagePath: '/test/package',
278 |         startedAt: startedAt,
279 |       };
280 | 
281 |       // Create mock process map with one process
282 |       const mockProcessMap = new Map([[12345, mockProcess]]);
283 | 
284 |       // Use pure dependency injection with stub functions
285 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
286 |       const mockDateNow = () => startedAt.getTime() + 2000; // 2 seconds after start
287 | 
288 |       const result = await swift_package_listLogic(
289 |         {},
290 |         {
291 |           processMap: mockProcessMap,
292 |           arrayFrom: mockArrayFrom,
293 |           dateNow: mockDateNow,
294 |         },
295 |       );
296 | 
297 |       expect(result).toEqual({
298 |         content: [
299 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
300 |           { type: 'text', text: '  • PID 12345: default (/test/package) - running 2s' },
301 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
302 |         ],
303 |       });
304 |     });
305 | 
306 |     it('should handle very recent process (less than 1 second)', async () => {
307 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
308 |       const mockProcess = {
309 |         executableName: 'FastApp',
310 |         packagePath: '/test/package',
311 |         startedAt: startedAt,
312 |       };
313 | 
314 |       // Create mock process map with one process
315 |       const mockProcessMap = new Map([[12345, mockProcess]]);
316 | 
317 |       // Use pure dependency injection with stub functions
318 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
319 |       const mockDateNow = () => startedAt.getTime() + 500; // 500ms after start
320 | 
321 |       const result = await swift_package_listLogic(
322 |         {},
323 |         {
324 |           processMap: mockProcessMap,
325 |           arrayFrom: mockArrayFrom,
326 |           dateNow: mockDateNow,
327 |         },
328 |       );
329 | 
330 |       expect(result).toEqual({
331 |         content: [
332 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
333 |           { type: 'text', text: '  • PID 12345: FastApp (/test/package) - running 1s' },
334 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
335 |         ],
336 |       });
337 |     });
338 | 
339 |     it('should handle process running for exactly 0 milliseconds', async () => {
340 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
341 |       const mockProcess = {
342 |         executableName: 'InstantApp',
343 |         packagePath: '/test/package',
344 |         startedAt: startedAt,
345 |       };
346 | 
347 |       // Create mock process map with one process
348 |       const mockProcessMap = new Map([[12345, mockProcess]]);
349 | 
350 |       // Use pure dependency injection with stub functions
351 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
352 |       const mockDateNow = () => startedAt.getTime(); // Same time as start
353 | 
354 |       const result = await swift_package_listLogic(
355 |         {},
356 |         {
357 |           processMap: mockProcessMap,
358 |           arrayFrom: mockArrayFrom,
359 |           dateNow: mockDateNow,
360 |         },
361 |       );
362 | 
363 |       expect(result).toEqual({
364 |         content: [
365 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
366 |           { type: 'text', text: '  • PID 12345: InstantApp (/test/package) - running 1s' },
367 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
368 |         ],
369 |       });
370 |     });
371 | 
372 |     it('should handle process running for a long time', async () => {
373 |       const startedAt = new Date('2023-01-01T10:00:00.000Z');
374 |       const mockProcess = {
375 |         executableName: 'LongRunningApp',
376 |         packagePath: '/test/package',
377 |         startedAt: startedAt,
378 |       };
379 | 
380 |       // Create mock process map with one process
381 |       const mockProcessMap = new Map([[12345, mockProcess]]);
382 | 
383 |       // Use pure dependency injection with stub functions
384 |       const mockArrayFrom = (mapEntries: any) => Array.from(mapEntries);
385 |       const mockDateNow = () => startedAt.getTime() + 7200000; // 2 hours later
386 | 
387 |       const result = await swift_package_listLogic(
388 |         {},
389 |         {
390 |           processMap: mockProcessMap,
391 |           arrayFrom: mockArrayFrom,
392 |           dateNow: mockDateNow,
393 |         },
394 |       );
395 | 
396 |       expect(result).toEqual({
397 |         content: [
398 |           { type: 'text', text: '📋 Active Swift Package processes (1):' },
399 |           { type: 'text', text: '  • PID 12345: LongRunningApp (/test/package) - running 7200s' },
400 |           { type: 'text', text: '💡 Use swift_package_stop with a PID to terminate a process.' },
401 |         ],
402 |       });
403 |     });
404 |   });
405 | });
406 | 
```

--------------------------------------------------------------------------------
/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Utilities Plugin: Scaffold macOS Project
  3 |  *
  4 |  * Scaffold a new macOS project from templates.
  5 |  */
  6 | 
  7 | import * as z from 'zod';
  8 | import { join, dirname, basename } from 'path';
  9 | import { log } from '../../../utils/logging/index.ts';
 10 | import { ValidationError } from '../../../utils/responses/index.ts';
 11 | import { TemplateManager } from '../../../utils/template/index.ts';
 12 | import { ToolResponse } from '../../../types/common.ts';
 13 | import {
 14 |   CommandExecutor,
 15 |   getDefaultCommandExecutor,
 16 |   getDefaultFileSystemExecutor,
 17 | } from '../../../utils/command.ts';
 18 | import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
 19 | 
 20 | // Common base schema for both iOS and macOS
 21 | const BaseScaffoldSchema = z.object({
 22 |   projectName: z.string().min(1).describe('Name of the new project'),
 23 |   outputPath: z.string().describe('Path where the project should be created'),
 24 |   bundleIdentifier: z
 25 |     .string()
 26 |     .optional()
 27 |     .describe(
 28 |       'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname',
 29 |     ),
 30 |   displayName: z
 31 |     .string()
 32 |     .optional()
 33 |     .describe(
 34 |       'App display name (shown on home screen/dock). If not provided, will use projectName',
 35 |     ),
 36 |   marketingVersion: z
 37 |     .string()
 38 |     .optional()
 39 |     .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'),
 40 |   currentProjectVersion: z
 41 |     .string()
 42 |     .optional()
 43 |     .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'),
 44 |   customizeNames: z
 45 |     .boolean()
 46 |     .default(true)
 47 |     .describe('Whether to customize project names and identifiers. Default is true.'),
 48 | });
 49 | 
 50 | // macOS-specific schema
 51 | const ScaffoldmacOSProjectSchema = BaseScaffoldSchema.extend({
 52 |   deploymentTarget: z
 53 |     .string()
 54 |     .optional()
 55 |     .describe('macOS deployment target (e.g., 15.4, 14.0). If not provided, will use 15.4'),
 56 | });
 57 | 
 58 | // Use z.infer for type safety
 59 | type ScaffoldMacOSProjectParams = z.infer<typeof ScaffoldmacOSProjectSchema>;
 60 | 
 61 | /**
 62 |  * Update Package.swift file with deployment target
 63 |  */
 64 | function updatePackageSwiftFile(
 65 |   content: string,
 66 |   params: ScaffoldMacOSProjectParams & { platform: string },
 67 | ): string {
 68 |   let result = content;
 69 | 
 70 |   // Update ALL target name references in Package.swift
 71 |   const featureName = `${params.projectName}Feature`;
 72 |   const testName = `${params.projectName}FeatureTests`;
 73 | 
 74 |   // Replace ALL occurrences of MyProjectFeatureTests first (more specific)
 75 |   result = result.replace(/MyProjectFeatureTests/g, testName);
 76 |   // Then replace ALL occurrences of MyProjectFeature (less specific, so comes after)
 77 |   result = result.replace(/MyProjectFeature/g, featureName);
 78 | 
 79 |   // Update deployment targets based on platform
 80 |   if (params.platform === 'macOS') {
 81 |     if (params.deploymentTarget) {
 82 |       // Extract major version (e.g., "14.0" -> "14")
 83 |       const majorVersion = params.deploymentTarget.split('.')[0];
 84 |       result = result.replace(/\.macOS\(\.v\d+\)/, `.macOS(.v${majorVersion})`);
 85 |     }
 86 |   }
 87 | 
 88 |   return result;
 89 | }
 90 | 
 91 | /**
 92 |  * Update XCConfig file with scaffold parameters
 93 |  */
 94 | function updateXCConfigFile(
 95 |   content: string,
 96 |   params: ScaffoldMacOSProjectParams & { platform: string },
 97 | ): string {
 98 |   let result = content;
 99 | 
100 |   // Update project identity settings
101 |   result = result.replace(/PRODUCT_NAME = .+/g, `PRODUCT_NAME = ${params.projectName}`);
102 |   result = result.replace(
103 |     /PRODUCT_DISPLAY_NAME = .+/g,
104 |     `PRODUCT_DISPLAY_NAME = ${params.displayName ?? params.projectName}`,
105 |   );
106 |   result = result.replace(
107 |     /PRODUCT_BUNDLE_IDENTIFIER = .+/g,
108 |     `PRODUCT_BUNDLE_IDENTIFIER = ${params.bundleIdentifier ?? `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`,
109 |   );
110 |   result = result.replace(
111 |     /MARKETING_VERSION = .+/g,
112 |     `MARKETING_VERSION = ${params.marketingVersion ?? '1.0'}`,
113 |   );
114 |   result = result.replace(
115 |     /CURRENT_PROJECT_VERSION = .+/g,
116 |     `CURRENT_PROJECT_VERSION = ${params.currentProjectVersion ?? '1'}`,
117 |   );
118 | 
119 |   // Platform-specific updates
120 |   if (params.platform === 'macOS') {
121 |     // macOS deployment target
122 |     if (params.deploymentTarget) {
123 |       result = result.replace(
124 |         /MACOSX_DEPLOYMENT_TARGET = .+/g,
125 |         `MACOSX_DEPLOYMENT_TARGET = ${params.deploymentTarget}`,
126 |       );
127 |     }
128 | 
129 |     // Update entitlements path for macOS
130 |     result = result.replace(
131 |       /CODE_SIGN_ENTITLEMENTS = .+/g,
132 |       `CODE_SIGN_ENTITLEMENTS = Config/${params.projectName}.entitlements`,
133 |     );
134 |   }
135 | 
136 |   // Update test bundle identifier and target name
137 |   result = result.replace(/TEST_TARGET_NAME = .+/g, `TEST_TARGET_NAME = ${params.projectName}`);
138 | 
139 |   // Update comments that reference MyProject in entitlements paths
140 |   result = result.replace(
141 |     /Config\/MyProject\.entitlements/g,
142 |     `Config/${params.projectName}.entitlements`,
143 |   );
144 | 
145 |   return result;
146 | }
147 | 
148 | /**
149 |  * Replace placeholders in a string (for non-XCConfig files)
150 |  */
151 | function replacePlaceholders(
152 |   content: string,
153 |   projectName: string,
154 |   bundleIdentifier: string,
155 | ): string {
156 |   let result = content;
157 | 
158 |   // Replace project name
159 |   result = result.replace(/MyProject/g, projectName);
160 | 
161 |   // Replace bundle identifier - check for both patterns used in templates
162 |   if (bundleIdentifier) {
163 |     result = result.replace(/com\.example\.MyProject/g, bundleIdentifier);
164 |     result = result.replace(/com\.mycompany\.MyProject/g, bundleIdentifier);
165 |   }
166 | 
167 |   return result;
168 | }
169 | 
170 | /**
171 |  * Process a single file, replacing placeholders if it's a text file
172 |  */
173 | async function processFile(
174 |   sourcePath: string,
175 |   destPath: string,
176 |   params: ScaffoldMacOSProjectParams & { platform: string },
177 |   fileSystemExecutor: FileSystemExecutor,
178 | ): Promise<void> {
179 |   // Determine the destination file path
180 |   let finalDestPath = destPath;
181 |   if (params.customizeNames) {
182 |     // Replace MyProject in file/directory names
183 |     const fileName = basename(destPath);
184 |     const dirName = dirname(destPath);
185 |     const newFileName = fileName.replace(/MyProject/g, params.projectName);
186 |     finalDestPath = join(dirName, newFileName);
187 |   }
188 | 
189 |   // Text file extensions that should be processed
190 |   const textExtensions = [
191 |     '.swift',
192 |     '.h',
193 |     '.m',
194 |     '.mm',
195 |     '.cpp',
196 |     '.c',
197 |     '.pbxproj',
198 |     '.plist',
199 |     '.xcscheme',
200 |     '.xctestplan',
201 |     '.xcworkspacedata',
202 |     '.xcconfig',
203 |     '.json',
204 |     '.xml',
205 |     '.entitlements',
206 |     '.storyboard',
207 |     '.xib',
208 |     '.md',
209 |   ];
210 | 
211 |   const ext = sourcePath.toLowerCase();
212 |   const isTextFile = textExtensions.some((textExt) => ext.endsWith(textExt));
213 |   const isXCConfig = sourcePath.endsWith('.xcconfig');
214 |   const isPackageSwift = sourcePath.endsWith('Package.swift');
215 | 
216 |   if (isTextFile && params.customizeNames) {
217 |     // Read the file content
218 |     const content = await fileSystemExecutor.readFile(sourcePath, 'utf-8');
219 | 
220 |     let processedContent;
221 | 
222 |     if (isXCConfig) {
223 |       // Use special XCConfig processing
224 |       processedContent = updateXCConfigFile(content, params);
225 |     } else if (isPackageSwift) {
226 |       // Use special Package.swift processing
227 |       processedContent = updatePackageSwiftFile(content, params);
228 |     } else {
229 |       // Use standard placeholder replacement
230 |       const bundleIdentifier =
231 |         params.bundleIdentifier ??
232 |         `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
233 |       processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier);
234 |     }
235 | 
236 |     await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
237 |     await fileSystemExecutor.writeFile(finalDestPath, processedContent, 'utf-8');
238 |   } else {
239 |     // Copy binary files as-is
240 |     await fileSystemExecutor.mkdir(dirname(finalDestPath), { recursive: true });
241 |     await fileSystemExecutor.cp(sourcePath, finalDestPath, { recursive: true });
242 |   }
243 | }
244 | 
245 | /**
246 |  * Recursively process a directory
247 |  */
248 | async function processDirectory(
249 |   sourceDir: string,
250 |   destDir: string,
251 |   params: ScaffoldMacOSProjectParams & { platform: string },
252 |   fileSystemExecutor: FileSystemExecutor,
253 | ): Promise<void> {
254 |   const entries = await fileSystemExecutor.readdir(sourceDir, { withFileTypes: true });
255 | 
256 |   for (const entry of entries) {
257 |     const dirent = entry as { isDirectory(): boolean; isFile(): boolean; name: string };
258 |     const sourcePath = join(sourceDir, dirent.name);
259 |     let destName = dirent.name;
260 | 
261 |     if (params.customizeNames) {
262 |       // Replace MyProject in directory names
263 |       destName = destName.replace(/MyProject/g, params.projectName);
264 |     }
265 | 
266 |     const destPath = join(destDir, destName);
267 | 
268 |     if (dirent.isDirectory()) {
269 |       // Skip certain directories
270 |       if (dirent.name === '.git' || dirent.name === 'xcuserdata') {
271 |         continue;
272 |       }
273 |       await fileSystemExecutor.mkdir(destPath, { recursive: true });
274 |       await processDirectory(sourcePath, destPath, params, fileSystemExecutor);
275 |     } else if (dirent.isFile()) {
276 |       // Skip certain files
277 |       if (dirent.name === '.DS_Store' || dirent.name.endsWith('.xcuserstate')) {
278 |         continue;
279 |       }
280 |       await processFile(sourcePath, destPath, params, fileSystemExecutor);
281 |     }
282 |   }
283 | }
284 | 
285 | /**
286 |  * Scaffold a new iOS or macOS project
287 |  */
288 | async function scaffoldProject(
289 |   params: ScaffoldMacOSProjectParams & { platform: string },
290 |   commandExecutor: CommandExecutor,
291 |   fileSystemExecutor: FileSystemExecutor,
292 | ): Promise<string> {
293 |   const projectName = params.projectName;
294 |   const outputPath = params.outputPath;
295 |   const platform = params.platform;
296 |   const customizeNames = params.customizeNames ?? true;
297 | 
298 |   log('info', `Scaffolding project: ${projectName} (${platform}) at ${outputPath}`);
299 | 
300 |   // Validate project name
301 |   if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(projectName)) {
302 |     throw new ValidationError(
303 |       'Project name must start with a letter and contain only letters, numbers, and underscores',
304 |     );
305 |   }
306 | 
307 |   // Get template path from TemplateManager
308 |   let templatePath;
309 |   try {
310 |     templatePath = await TemplateManager.getTemplatePath(
311 |       platform as 'macOS' | 'iOS',
312 |       commandExecutor,
313 |       fileSystemExecutor,
314 |     );
315 |   } catch (error) {
316 |     throw new ValidationError(
317 |       `Failed to get template for ${platform}: ${error instanceof Error ? error.message : String(error)}`,
318 |     );
319 |   }
320 | 
321 |   // Use outputPath directly as the destination
322 |   const projectPath = outputPath;
323 | 
324 |   // Check if the output directory already has Xcode project files
325 |   const xcworkspaceExists = fileSystemExecutor.existsSync(
326 |     join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcworkspace`),
327 |   );
328 |   const xcodeprojExists = fileSystemExecutor.existsSync(
329 |     join(projectPath, `${customizeNames ? projectName : 'MyProject'}.xcodeproj`),
330 |   );
331 | 
332 |   if (xcworkspaceExists || xcodeprojExists) {
333 |     throw new ValidationError(`Xcode project files already exist in ${projectPath}`);
334 |   }
335 | 
336 |   try {
337 |     // Process the template directly into the output path
338 |     await processDirectory(templatePath, projectPath, params, fileSystemExecutor);
339 | 
340 |     return projectPath;
341 |   } finally {
342 |     // Clean up downloaded template if needed
343 |     await TemplateManager.cleanup(templatePath, fileSystemExecutor);
344 |   }
345 | }
346 | 
347 | /**
348 |  * Business logic for scaffolding macOS projects
349 |  * Extracted for testability and Separation of Concerns
350 |  */
351 | export async function scaffold_macos_projectLogic(
352 |   params: ScaffoldMacOSProjectParams,
353 |   commandExecutor: CommandExecutor,
354 |   fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
355 | ): Promise<ToolResponse> {
356 |   try {
357 |     const projectParams = { ...params, platform: 'macOS' as const };
358 |     const projectPath = await scaffoldProject(projectParams, commandExecutor, fileSystemExecutor);
359 | 
360 |     const response = {
361 |       success: true,
362 |       projectPath,
363 |       platform: 'macOS',
364 |       message: `Successfully scaffolded macOS project "${params.projectName}" in ${projectPath}`,
365 |       nextSteps: [
366 |         `Important: Before working on the project make sure to read the README.md file in the workspace root directory.`,
367 |         `Build for macOS: build_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
368 |         `Build & Run on macOS: build_run_macos({ workspacePath: "${projectPath}/${params.customizeNames ? params.projectName : 'MyProject'}.xcworkspace", scheme: "${params.customizeNames ? params.projectName : 'MyProject'}" })`,
369 |       ],
370 |     };
371 | 
372 |     return {
373 |       content: [
374 |         {
375 |           type: 'text',
376 |           text: JSON.stringify(response, null, 2),
377 |         },
378 |       ],
379 |     };
380 |   } catch (error) {
381 |     log(
382 |       'error',
383 |       `Failed to scaffold macOS project: ${error instanceof Error ? error.message : String(error)}`,
384 |     );
385 | 
386 |     return {
387 |       content: [
388 |         {
389 |           type: 'text',
390 |           text: JSON.stringify(
391 |             {
392 |               success: false,
393 |               error: error instanceof Error ? error.message : 'Unknown error occurred',
394 |             },
395 |             null,
396 |             2,
397 |           ),
398 |         },
399 |       ],
400 |       isError: true,
401 |     };
402 |   }
403 | }
404 | 
405 | export default {
406 |   name: 'scaffold_macos_project',
407 |   description:
408 |     'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.',
409 |   schema: ScaffoldmacOSProjectSchema.shape,
410 |   annotations: {
411 |     title: 'Scaffold macOS Project',
412 |     destructiveHint: true,
413 |   },
414 |   async handler(args: Record<string, unknown>): Promise<ToolResponse> {
415 |     // Validate the arguments against the schema before processing
416 |     const validatedArgs = ScaffoldmacOSProjectSchema.parse(args);
417 |     return scaffold_macos_projectLogic(
418 |       validatedArgs,
419 |       getDefaultCommandExecutor(),
420 |       getDefaultFileSystemExecutor(),
421 |     );
422 |   },
423 | };
424 | 
```

--------------------------------------------------------------------------------
/src/utils/build-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Build Utilities - Higher-level abstractions for Xcode build operations
  3 |  *
  4 |  * This utility module provides specialized functions for build-related operations
  5 |  * across different platforms (macOS, iOS, watchOS, etc.). It serves as a higher-level
  6 |  * abstraction layer on top of the core Xcode utilities.
  7 |  *
  8 |  * Responsibilities:
  9 |  * - Providing a unified interface (executeXcodeBuild) for all build operations
 10 |  * - Handling build-specific parameter formatting and validation
 11 |  * - Standardizing response formatting for build results
 12 |  * - Managing build-specific error handling and reporting
 13 |  * - Supporting various build actions (build, clean, showBuildSettings, etc.)
 14 |  * - Supporting xcodemake as an alternative build strategy for faster incremental builds
 15 |  *
 16 |  * This file depends on the lower-level utilities in xcode.ts for command execution
 17 |  * while adding build-specific behavior, formatting, and error handling.
 18 |  */
 19 | 
 20 | import { log } from './logger.ts';
 21 | import { XcodePlatform, constructDestinationString } from './xcode.ts';
 22 | import { CommandExecutor, CommandExecOptions } from './command.ts';
 23 | import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.ts';
 24 | import { createTextResponse, consolidateContentForClaudeCode } from './validation.ts';
 25 | import {
 26 |   isXcodemakeEnabled,
 27 |   isXcodemakeAvailable,
 28 |   executeXcodemakeCommand,
 29 |   executeMakeCommand,
 30 |   doesMakefileExist,
 31 |   doesMakeLogFileExist,
 32 | } from './xcodemake.ts';
 33 | import { sessionStore } from './session-store.ts';
 34 | import path from 'path';
 35 | 
 36 | /**
 37 |  * Common function to execute an Xcode build command across platforms
 38 |  * @param params Common build parameters
 39 |  * @param platformOptions Platform-specific options
 40 |  * @param preferXcodebuild Whether to prefer xcodebuild over xcodemake, useful for if xcodemake is failing
 41 |  * @param buildAction The xcodebuild action to perform (e.g., 'build', 'clean', 'test')
 42 |  * @param executor Optional command executor for dependency injection (used for testing)
 43 |  * @returns Promise resolving to tool response
 44 |  */
 45 | export async function executeXcodeBuildCommand(
 46 |   params: SharedBuildParams,
 47 |   platformOptions: PlatformBuildOptions,
 48 |   preferXcodebuild: boolean = false,
 49 |   buildAction: string = 'build',
 50 |   executor: CommandExecutor,
 51 |   execOpts?: CommandExecOptions,
 52 | ): Promise<ToolResponse> {
 53 |   // Collect warnings, errors, and stderr messages from the build output
 54 |   const buildMessages: { type: 'text'; text: string }[] = [];
 55 |   function grepWarningsAndErrors(text: string): { type: 'warning' | 'error'; content: string }[] {
 56 |     return text
 57 |       .split('\n')
 58 |       .map((content) => {
 59 |         if (/warning:/i.test(content)) return { type: 'warning', content };
 60 |         if (/error:/i.test(content)) return { type: 'error', content };
 61 |         return null;
 62 |       })
 63 |       .filter(Boolean) as { type: 'warning' | 'error'; content: string }[];
 64 |   }
 65 | 
 66 |   log('info', `Starting ${platformOptions.logPrefix} ${buildAction} for scheme ${params.scheme}`);
 67 | 
 68 |   // Check if xcodemake is enabled and available
 69 |   const isXcodemakeEnabledFlag = isXcodemakeEnabled();
 70 |   let xcodemakeAvailableFlag = false;
 71 | 
 72 |   if (isXcodemakeEnabledFlag && buildAction === 'build') {
 73 |     xcodemakeAvailableFlag = await isXcodemakeAvailable();
 74 | 
 75 |     if (xcodemakeAvailableFlag && preferXcodebuild) {
 76 |       log(
 77 |         'info',
 78 |         'xcodemake is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
 79 |       );
 80 |       buildMessages.push({
 81 |         type: 'text',
 82 |         text: '⚠️ incremental build support is enabled but preferXcodebuild is set to true. Falling back to xcodebuild.',
 83 |       });
 84 |     } else if (!xcodemakeAvailableFlag) {
 85 |       buildMessages.push({
 86 |         type: 'text',
 87 |         text: '⚠️ xcodemake is enabled but not available. Falling back to xcodebuild.',
 88 |       });
 89 |       log('info', 'xcodemake is enabled but not available. Falling back to xcodebuild.');
 90 |     } else {
 91 |       log('info', 'xcodemake is enabled and available, using it for incremental builds.');
 92 |       buildMessages.push({
 93 |         type: 'text',
 94 |         text: 'ℹ️ xcodemake is enabled and available, using it for incremental builds.',
 95 |       });
 96 |     }
 97 |   }
 98 | 
 99 |   try {
100 |     const command = ['xcodebuild'];
101 | 
102 |     let projectDir = '';
103 |     if (params.workspacePath) {
104 |       projectDir = path.dirname(params.workspacePath);
105 |       command.push('-workspace', params.workspacePath);
106 |     } else if (params.projectPath) {
107 |       projectDir = path.dirname(params.projectPath);
108 |       command.push('-project', params.projectPath);
109 |     }
110 | 
111 |     command.push('-scheme', params.scheme);
112 |     command.push('-configuration', params.configuration);
113 |     command.push('-skipMacroValidation');
114 | 
115 |     // Construct destination string based on platform
116 |     let destinationString: string;
117 |     const isSimulatorPlatform = [
118 |       XcodePlatform.iOSSimulator,
119 |       XcodePlatform.watchOSSimulator,
120 |       XcodePlatform.tvOSSimulator,
121 |       XcodePlatform.visionOSSimulator,
122 |     ].includes(platformOptions.platform);
123 | 
124 |     if (isSimulatorPlatform) {
125 |       if (platformOptions.simulatorId) {
126 |         destinationString = constructDestinationString(
127 |           platformOptions.platform,
128 |           undefined,
129 |           platformOptions.simulatorId,
130 |         );
131 |       } else if (platformOptions.simulatorName) {
132 |         destinationString = constructDestinationString(
133 |           platformOptions.platform,
134 |           platformOptions.simulatorName,
135 |           undefined,
136 |           platformOptions.useLatestOS,
137 |         );
138 |       } else {
139 |         return createTextResponse(
140 |           `For ${platformOptions.platform} platform, either simulatorId or simulatorName must be provided`,
141 |           true,
142 |         );
143 |       }
144 |     } else if (platformOptions.platform === XcodePlatform.macOS) {
145 |       destinationString = constructDestinationString(
146 |         platformOptions.platform,
147 |         undefined,
148 |         undefined,
149 |         false,
150 |         platformOptions.arch,
151 |       );
152 |     } else if (platformOptions.platform === XcodePlatform.iOS) {
153 |       if (platformOptions.deviceId) {
154 |         destinationString = `platform=iOS,id=${platformOptions.deviceId}`;
155 |       } else {
156 |         destinationString = 'generic/platform=iOS';
157 |       }
158 |     } else if (platformOptions.platform === XcodePlatform.watchOS) {
159 |       if (platformOptions.deviceId) {
160 |         destinationString = `platform=watchOS,id=${platformOptions.deviceId}`;
161 |       } else {
162 |         destinationString = 'generic/platform=watchOS';
163 |       }
164 |     } else if (platformOptions.platform === XcodePlatform.tvOS) {
165 |       if (platformOptions.deviceId) {
166 |         destinationString = `platform=tvOS,id=${platformOptions.deviceId}`;
167 |       } else {
168 |         destinationString = 'generic/platform=tvOS';
169 |       }
170 |     } else if (platformOptions.platform === XcodePlatform.visionOS) {
171 |       if (platformOptions.deviceId) {
172 |         destinationString = `platform=visionOS,id=${platformOptions.deviceId}`;
173 |       } else {
174 |         destinationString = 'generic/platform=visionOS';
175 |       }
176 |     } else {
177 |       return createTextResponse(`Unsupported platform: ${platformOptions.platform}`, true);
178 |     }
179 | 
180 |     command.push('-destination', destinationString);
181 | 
182 |     if (params.derivedDataPath) {
183 |       command.push('-derivedDataPath', params.derivedDataPath);
184 |     }
185 | 
186 |     if (params.extraArgs && params.extraArgs.length > 0) {
187 |       command.push(...params.extraArgs);
188 |     }
189 | 
190 |     command.push(buildAction);
191 | 
192 |     // Execute the command using xcodemake or xcodebuild
193 |     let result;
194 |     if (
195 |       isXcodemakeEnabledFlag &&
196 |       xcodemakeAvailableFlag &&
197 |       buildAction === 'build' &&
198 |       !preferXcodebuild
199 |     ) {
200 |       // Check if Makefile already exists
201 |       const makefileExists = doesMakefileExist(projectDir);
202 |       log('debug', 'Makefile exists: ' + makefileExists);
203 | 
204 |       // Check if Makefile log already exists
205 |       const makeLogFileExists = doesMakeLogFileExist(projectDir, command);
206 |       log('debug', 'Makefile log exists: ' + makeLogFileExists);
207 | 
208 |       if (makefileExists && makeLogFileExists) {
209 |         // Use make for incremental builds
210 |         buildMessages.push({
211 |           type: 'text',
212 |           text: 'ℹ️ Using make for incremental build',
213 |         });
214 |         result = await executeMakeCommand(projectDir, platformOptions.logPrefix);
215 |       } else {
216 |         // Generate Makefile using xcodemake
217 |         buildMessages.push({
218 |           type: 'text',
219 |           text: 'ℹ️ Generating Makefile with xcodemake (first build may take longer)',
220 |         });
221 |         // Remove 'xcodebuild' from the command array before passing to executeXcodemakeCommand
222 |         result = await executeXcodemakeCommand(
223 |           projectDir,
224 |           command.slice(1),
225 |           platformOptions.logPrefix,
226 |         );
227 |       }
228 |     } else {
229 |       // Use standard xcodebuild
230 |       // Pass projectDir as cwd to ensure CocoaPods relative paths resolve correctly
231 |       result = await executor(command, platformOptions.logPrefix, true, {
232 |         ...execOpts,
233 |         cwd: projectDir,
234 |       });
235 |     }
236 | 
237 |     // Grep warnings and errors from stdout (build output)
238 |     const warningOrErrorLines = grepWarningsAndErrors(result.output);
239 |     const suppressWarnings = sessionStore.get('suppressWarnings');
240 |     warningOrErrorLines.forEach(({ type, content }) => {
241 |       if (type === 'warning' && suppressWarnings) {
242 |         return;
243 |       }
244 |       buildMessages.push({
245 |         type: 'text',
246 |         text: type === 'warning' ? `⚠️ Warning: ${content}` : `❌ Error: ${content}`,
247 |       });
248 |     });
249 | 
250 |     // Include all stderr lines as errors
251 |     if (result.error) {
252 |       result.error.split('\n').forEach((content) => {
253 |         if (content.trim()) {
254 |           buildMessages.push({ type: 'text', text: `❌ [stderr] ${content}` });
255 |         }
256 |       });
257 |     }
258 | 
259 |     if (!result.success) {
260 |       const isMcpError = result.exitCode === 64;
261 | 
262 |       log(
263 |         isMcpError ? 'error' : 'warning',
264 |         `${platformOptions.logPrefix} ${buildAction} failed: ${result.error}`,
265 |         { sentry: isMcpError },
266 |       );
267 |       const errorResponse = createTextResponse(
268 |         `❌ ${platformOptions.logPrefix} ${buildAction} failed for scheme ${params.scheme}.`,
269 |         true,
270 |       );
271 | 
272 |       if (buildMessages.length > 0 && errorResponse.content) {
273 |         errorResponse.content.unshift(...buildMessages);
274 |       }
275 | 
276 |       // If using xcodemake and build failed but no compiling errors, suggest using xcodebuild
277 |       if (
278 |         warningOrErrorLines.length == 0 &&
279 |         isXcodemakeEnabledFlag &&
280 |         xcodemakeAvailableFlag &&
281 |         buildAction === 'build' &&
282 |         !preferXcodebuild
283 |       ) {
284 |         errorResponse.content.push({
285 |           type: 'text',
286 |           text: `💡 Incremental build using xcodemake failed, suggest using preferXcodebuild option to try build again using slower xcodebuild command.`,
287 |         });
288 |       }
289 | 
290 |       return consolidateContentForClaudeCode(errorResponse);
291 |     }
292 | 
293 |     log('info', `✅ ${platformOptions.logPrefix} ${buildAction} succeeded.`);
294 | 
295 |     // Create additional info based on platform and action
296 |     let additionalInfo = '';
297 | 
298 |     // Add xcodemake info if relevant
299 |     if (
300 |       isXcodemakeEnabledFlag &&
301 |       xcodemakeAvailableFlag &&
302 |       buildAction === 'build' &&
303 |       !preferXcodebuild
304 |     ) {
305 |       additionalInfo += `xcodemake: Using faster incremental builds with xcodemake. 
306 | Future builds will use the generated Makefile for improved performance.
307 | 
308 | `;
309 |     }
310 | 
311 |     // Only show next steps for 'build' action
312 |     if (buildAction === 'build') {
313 |       if (platformOptions.platform === XcodePlatform.macOS) {
314 |         additionalInfo = `Next Steps:
315 | 1. Get app path: get_mac_app_path({ scheme: '${params.scheme}' })
316 | 2. Get bundle ID: get_mac_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
317 | 3. Launch: launch_mac_app({ appPath: 'PATH_FROM_STEP_1' })`;
318 |       } else if (platformOptions.platform === XcodePlatform.iOS) {
319 |         additionalInfo = `Next Steps:
320 | 1. Get app path: get_device_app_path({ scheme: '${params.scheme}' })
321 | 2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
322 | 3. Launch: launch_app_device({ bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
323 |       } else if (isSimulatorPlatform) {
324 |         const simIdParam = platformOptions.simulatorId ? 'simulatorId' : 'simulatorName';
325 |         const simIdValue = platformOptions.simulatorId ?? platformOptions.simulatorName;
326 | 
327 |         additionalInfo = `Next Steps:
328 | 1. Get app path: get_sim_app_path({ ${simIdParam}: '${simIdValue}', scheme: '${params.scheme}', platform: 'iOS Simulator' })
329 | 2. Get bundle ID: get_app_bundle_id({ appPath: 'PATH_FROM_STEP_1' })
330 | 3. Launch: launch_app_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })
331 |    Or with logs: launch_app_logs_sim({ ${simIdParam}: '${simIdValue}', bundleId: 'BUNDLE_ID_FROM_STEP_2' })`;
332 |       }
333 |     }
334 | 
335 |     const successResponse: ToolResponse = {
336 |       content: [
337 |         ...buildMessages,
338 |         {
339 |           type: 'text',
340 |           text: `✅ ${platformOptions.logPrefix} ${buildAction} succeeded for scheme ${params.scheme}.`,
341 |         },
342 |       ],
343 |     };
344 | 
345 |     // Only add additional info if we have any
346 |     if (additionalInfo) {
347 |       successResponse.content.push({
348 |         type: 'text',
349 |         text: additionalInfo,
350 |       });
351 |     }
352 | 
353 |     return consolidateContentForClaudeCode(successResponse);
354 |   } catch (error) {
355 |     const errorMessage = error instanceof Error ? error.message : String(error);
356 | 
357 |     const isSpawnError =
358 |       error instanceof Error &&
359 |       'code' in error &&
360 |       ['ENOENT', 'EACCES', 'EPERM'].includes((error as NodeJS.ErrnoException).code ?? '');
361 | 
362 |     log('error', `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`, {
363 |       sentry: !isSpawnError,
364 |     });
365 | 
366 |     return consolidateContentForClaudeCode(
367 |       createTextResponse(
368 |         `Error during ${platformOptions.logPrefix} ${buildAction}: ${errorMessage}`,
369 |         true,
370 |       ),
371 |     );
372 |   }
373 | }
374 | 
```

--------------------------------------------------------------------------------
/docs/dev/TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md:
--------------------------------------------------------------------------------

```markdown
  1 | # TEST_RUNNER_ Environment Variables Implementation Plan
  2 | 
  3 | ## Problem Statement
  4 | 
  5 | **GitHub Issue**: [#101 - Support TEST_RUNNER_ prefixed env vars](https://github.com/cameroncooke/XcodeBuildMCP/issues/101)
  6 | 
  7 | **Core Need**: Enable conditional test behavior by passing TEST_RUNNER_ prefixed environment variables from MCP client configurations to xcodebuild test processes. This addresses the specific use case of disabling `runsForEachTargetApplicationUIConfiguration` for faster development testing.
  8 | 
  9 | ## Background Context
 10 | 
 11 | ### xcodebuild Environment Variable Support
 12 | 
 13 | From the xcodebuild man page:
 14 | ```
 15 | TEST_RUNNER_<VAR>   Set an environment variable whose name is prefixed
 16 |                     with TEST_RUNNER_ to have that variable passed, with
 17 |                     its prefix stripped, to all test runner processes
 18 |                     launched during a test action. For example,
 19 |                     TEST_RUNNER_Foo=Bar xcodebuild test ... sets the
 20 |                     environment variable Foo=Bar in the test runner's
 21 |                     environment.
 22 | ```
 23 | 
 24 | ### User Requirements
 25 | 
 26 | Users want to configure their MCP server with TEST_RUNNER_ prefixed environment variables:
 27 | 
 28 | ```json
 29 | {
 30 |   "mcpServers": {
 31 |     "XcodeBuildMCP": {
 32 |       "type": "stdio",
 33 |       "command": "npx",
 34 |       "args": ["-y", "xcodebuildmcp@latest"],
 35 |       "env": {
 36 |         "TEST_RUNNER_USE_DEV_MODE": "YES"
 37 |       }
 38 |     }
 39 |   }
 40 | }
 41 | ```
 42 | 
 43 | And have tests that can conditionally execute based on these variables:
 44 | 
 45 | ```swift
 46 | func testFoo() throws {
 47 |   let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
 48 |   guard useDevMode else {
 49 |     XCTFail("Test requires USE_DEV_MODE to be true")
 50 |     return
 51 |   }
 52 |   // Test logic here...
 53 | }
 54 | ```
 55 | 
 56 | ## Current Architecture Analysis
 57 | 
 58 | ### XcodeBuildMCP Execution Flow
 59 | 1. All Xcode commands flow through `executeXcodeBuildCommand()` function
 60 | 2. Generic `CommandExecutor` interface handles all command execution
 61 | 3. Test tools exist for device/simulator/macOS platforms
 62 | 4. Zod schemas provide parameter validation and type safety
 63 | 
 64 | ### Key Files in Current Architecture
 65 | - `src/utils/CommandExecutor.ts` - Command execution interface
 66 | - `src/utils/build-utils.ts` - Contains `executeXcodeBuildCommand`
 67 | - `src/mcp/tools/device/test_device.ts` - Device testing tool
 68 | - `src/mcp/tools/simulator/test_sim.ts` - Simulator testing tool  
 69 | - `src/mcp/tools/macos/test_macos.ts` - macOS testing tool
 70 | - `src/utils/test/index.ts` - Shared test logic for simulator
 71 | 
 72 | ## Solution Analysis
 73 | 
 74 | ### Design Options Considered
 75 | 
 76 | 1. **Automatic Detection** (❌ Rejected)
 77 |    - Scan `process.env` for TEST_RUNNER_ variables and always pass them
 78 |    - **Issue**: Security risk of environment variable leakage
 79 |    - **Issue**: Unpredictable behavior based on server environment
 80 | 
 81 | 2. **Explicit Parameter** (✅ Chosen)
 82 |    - Add `testRunnerEnv` parameter to test tools
 83 |    - Users explicitly specify which variables to pass
 84 |    - **Benefits**: Secure, predictable, well-validated
 85 | 
 86 | 3. **Hybrid Approach** (🤔 Future Enhancement)
 87 |    - Both automatic + explicit with explicit overriding
 88 |    - **Issue**: Adds complexity, deferred for future consideration
 89 | 
 90 | ### Expert Analysis Summary
 91 | 
 92 | **RepoPrompt Analysis**: Comprehensive architectural plan emphasizing security, type safety, and integration with existing patterns.
 93 | 
 94 | **Gemini Analysis**: Confirmed explicit approach as optimal, highlighting:
 95 | - Security benefits of explicit allow-list approach
 96 | - Architectural soundness of extending CommandExecutor
 97 | - Recommendation for automatic prefix handling for better UX
 98 | 
 99 | ## Recommended Solution: Explicit Parameter with Automatic Prefix Handling
100 | 
101 | ### Key Design Decisions
102 | 
103 | 1. **Security-First**: Only explicitly provided variables are passed (no automatic process.env scanning)
104 | 2. **User Experience**: Automatic prefix handling - users provide unprefixed keys
105 | 3. **Architecture**: Extend execution layer generically for future extensibility  
106 | 4. **Validation**: Zod schema enforcement with proper type safety
107 | 
108 | ### User Experience Design
109 | 
110 | **Input** (what users specify):
111 | ```json
112 | {
113 |   "testRunnerEnv": {
114 |     "USE_DEV_MODE": "YES",
115 |     "runsForEachTargetApplicationUIConfiguration": "NO"
116 |   }
117 | }
118 | ```
119 | 
120 | **Output** (what gets passed to xcodebuild):
121 | ```bash
122 | TEST_RUNNER_USE_DEV_MODE=YES \
123 | TEST_RUNNER_runsForEachTargetApplicationUIConfiguration=NO \
124 | xcodebuild test ...
125 | ```
126 | 
127 | ## Implementation Plan
128 | 
129 | ### Phase 0: Test-Driven Development Setup
130 | 
131 | **Objective**: Create reproduction test to validate issue and later prove fix works
132 | 
133 | #### Tasks:
134 | - [ ] Create test in `example_projects/iOS/MCPTest` that checks for environment variable
135 | - [ ] Run current test tools to demonstrate limitation (test should fail)
136 | - [ ] Document baseline behavior
137 | 
138 | **Test Code Example**:
139 | ```swift
140 | func testEnvironmentVariablePassthrough() throws {
141 |   let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
142 |   guard useDevMode else {
143 |     XCTFail("Test requires USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE")
144 |     return
145 |   }
146 |   XCTAssertTrue(true, "Environment variable successfully passed through")
147 | }
148 | ```
149 | 
150 | ### Phase 1: Core Infrastructure Updates
151 | 
152 | **Objective**: Extend CommandExecutor and build utilities to support environment variables
153 | 
154 | #### 1.1 Update CommandExecutor Interface
155 | 
156 | **File**: `src/utils/CommandExecutor.ts`
157 | 
158 | **Changes**:
159 | - Add `CommandExecOptions` type for execution options
160 | - Update `CommandExecutor` type signature to accept optional execution options
161 | 
162 | ```typescript
163 | export type CommandExecOptions = {
164 |   cwd?: string;
165 |   env?: Record<string, string | undefined>;
166 | };
167 | 
168 | export type CommandExecutor = (
169 |   args: string[],
170 |   description?: string,
171 |   quiet?: boolean,
172 |   opts?: CommandExecOptions
173 | ) => Promise<CommandResponse>;
174 | ```
175 | 
176 | #### 1.2 Update Execution Facade
177 | 
178 | **File**: `src/utils/execution/index.ts`
179 | 
180 | **Changes**:
181 | - Re-export `CommandExecOptions` type
182 | 
183 | ```typescript
184 | export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.js';
185 | ```
186 | 
187 | #### 1.3 Update Default Command Executor
188 | 
189 | **File**: `src/utils/command.ts`
190 | 
191 | **Changes**:
192 | - Modify `getDefaultCommandExecutor` to merge `opts.env` with `process.env` when spawning
193 | 
194 | ```typescript
195 | // In the returned function:
196 | const env = { ...process.env, ...(opts?.env ?? {}) };
197 | // Pass env and opts?.cwd to spawn/exec call
198 | ```
199 | 
200 | #### 1.4 Create Environment Variable Utility
201 | 
202 | **File**: `src/utils/environment.ts`
203 | 
204 | **Changes**:
205 | - Add `normalizeTestRunnerEnv` function
206 | 
207 | ```typescript
208 | export function normalizeTestRunnerEnv(
209 |   userVars?: Record<string, string | undefined>
210 | ): Record<string, string> {
211 |   const result: Record<string, string> = {};
212 |   if (userVars) {
213 |     for (const [key, value] of Object.entries(userVars)) {
214 |       if (value !== undefined) {
215 |         result[`TEST_RUNNER_${key}`] = value;
216 |       }
217 |     }
218 |   }
219 |   return result;
220 | }
221 | ```
222 | 
223 | #### 1.5 Update executeXcodeBuildCommand
224 | 
225 | **File**: `src/utils/build-utils.ts`
226 | 
227 | **Changes**:
228 | - Add optional `execOpts?: CommandExecOptions` parameter (6th parameter)
229 | - Pass execution options through to `CommandExecutor` calls
230 | 
231 | ```typescript
232 | export async function executeXcodeBuildCommand(
233 |   build: { /* existing fields */ },
234 |   runtime: { /* existing fields */ },
235 |   preferXcodebuild = false,
236 |   action: 'build' | 'test' | 'archive' | 'analyze' | string,
237 |   executor: CommandExecutor = getDefaultCommandExecutor(),
238 |   execOpts?: CommandExecOptions, // NEW
239 | ): Promise<ToolResponse>
240 | ```
241 | 
242 | ### Phase 2: Test Tool Integration
243 | 
244 | **Objective**: Add `testRunnerEnv` parameter to all test tools and wire through execution
245 | 
246 | #### 2.1 Update Device Test Tool
247 | 
248 | **File**: `src/mcp/tools/device/test_device.ts`
249 | 
250 | **Changes**:
251 | - Add `testRunnerEnv` to Zod schema with validation
252 | - Import and use `normalizeTestRunnerEnv`
253 | - Pass execution options to `executeXcodeBuildCommand`
254 | 
255 | **Schema Addition**:
256 | ```typescript
257 | testRunnerEnv: z
258 |   .record(z.string(), z.string().optional())
259 |   .optional()
260 |   .describe('Test runner environment variables (TEST_RUNNER_ prefix added automatically)')
261 | ```
262 | 
263 | **Usage**:
264 | ```typescript
265 | const execEnv = normalizeTestRunnerEnv(params.testRunnerEnv);
266 | const testResult = await executeXcodeBuildCommand(
267 |   { /* build params */ },
268 |   { /* runtime params */ },
269 |   params.preferXcodebuild ?? false,
270 |   'test',
271 |   executor,
272 |   { env: execEnv } // NEW
273 | );
274 | ```
275 | 
276 | #### 2.2 Update macOS Test Tool
277 | 
278 | **File**: `src/mcp/tools/macos/test_macos.ts`
279 | 
280 | **Changes**: Same pattern as device test tool
281 | - Schema addition for `testRunnerEnv`
282 | - Import `normalizeTestRunnerEnv` 
283 | - Pass execution options to `executeXcodeBuildCommand`
284 | 
285 | #### 2.3 Update Simulator Test Tool and Logic
286 | 
287 | **File**: `src/mcp/tools/simulator/test_sim.ts`
288 | 
289 | **Changes**:
290 | - Add `testRunnerEnv` to schema
291 | - Pass through to `handleTestLogic`
292 | 
293 | **File**: `src/utils/test/index.ts`
294 | 
295 | **Changes**:
296 | - Update `handleTestLogic` signature to accept `testRunnerEnv?: Record<string, string | undefined>`
297 | - Import and use `normalizeTestRunnerEnv`
298 | - Pass execution options to `executeXcodeBuildCommand`
299 | 
300 | ### Phase 3: Testing and Validation
301 | 
302 | **Objective**: Comprehensive testing coverage for new functionality
303 | 
304 | #### 3.1 Unit Tests
305 | 
306 | **File**: `src/utils/__tests__/environment.test.ts`
307 | 
308 | **Tests**:
309 | - Test `normalizeTestRunnerEnv` with various inputs
310 | - Verify prefix addition
311 | - Verify undefined filtering
312 | - Verify empty input handling
313 | 
314 | #### 3.2 Integration Tests  
315 | 
316 | **Files**: Update existing test files for test tools
317 | 
318 | **Tests**:
319 | - Verify `testRunnerEnv` parameter is properly validated
320 | - Verify environment variables are passed through `CommandExecutor`
321 | - Mock executor to verify correct env object construction
322 | 
323 | #### 3.3 Tool Export Validation
324 | 
325 | **Files**: Test files in each tool directory
326 | 
327 | **Tests**:
328 | - Verify schema exports include new `testRunnerEnv` field
329 | - Verify parameter typing is correct
330 | 
331 | ### Phase 4: End-to-End Validation
332 | 
333 | **Objective**: Prove the fix works with real xcodebuild scenarios
334 | 
335 | #### 4.1 Reproduction Test Validation
336 | 
337 | **Tasks**:
338 | - Run reproduction test from Phase 0 with new `testRunnerEnv` parameter
339 | - Verify test passes (proving env var was successfully passed)
340 | - Document the before/after behavior
341 | 
342 | #### 4.2 Real-World Scenario Testing
343 | 
344 | **Tasks**:
345 | - Test with actual iOS project using `runsForEachTargetApplicationUIConfiguration`
346 | - Verify performance difference when variable is set
347 | - Test with multiple environment variables
348 | - Test edge cases (empty values, special characters)
349 | 
350 | ## Security Considerations
351 | 
352 | ### Security Benefits
353 | - **No Environment Leakage**: Only explicit user-provided variables are passed
354 | - **Command Injection Prevention**: Environment variables passed as separate object, not interpolated into command string
355 | - **Input Validation**: Zod schemas prevent malformed inputs
356 | - **Prefix Enforcement**: Only TEST_RUNNER_ prefixed variables can be set
357 | 
358 | ### Security Best Practices
359 | - Never log environment variable values (keys only for debugging)
360 | - Filter out undefined values to prevent accidental exposure
361 | - Validate all user inputs through Zod schemas
362 | - Document supported TEST_RUNNER_ variables from Apple's documentation
363 | 
364 | ## Architectural Benefits
365 | 
366 | ### Clean Integration
367 | - Extends existing `CommandExecutor` pattern generically
368 | - Maintains backward compatibility (all existing calls remain valid)
369 | - Follows established Zod validation patterns
370 | - Consistent API across all test tools
371 | 
372 | ### Future Extensibility  
373 | - `CommandExecOptions` can support additional execution options (timeout, cwd, etc.)
374 | - Pattern can be extended to other tools that need environment variables
375 | - Generic approach allows for non-TEST_RUNNER_ use cases in the future
376 | 
377 | ## File Modification Summary
378 | 
379 | ### New Files
380 | - `src/utils/__tests__/environment.test.ts` - Unit tests for environment utilities
381 | 
382 | ### Modified Files
383 | - `src/utils/CommandExecutor.ts` - Add execution options types
384 | - `src/utils/execution/index.ts` - Re-export new types  
385 | - `src/utils/command.ts` - Update default executor to handle env
386 | - `src/utils/environment.ts` - Add `normalizeTestRunnerEnv` utility
387 | - `src/utils/build-utils.ts` - Update `executeXcodeBuildCommand` signature
388 | - `src/mcp/tools/device/test_device.ts` - Add schema and integration
389 | - `src/mcp/tools/macos/test_macos.ts` - Add schema and integration
390 | - `src/mcp/tools/simulator/test_sim.ts` - Add schema and pass-through
391 | - `src/utils/test/index.ts` - Update `handleTestLogic` for simulator path
392 | - Test files for each modified tool - Add validation tests
393 | 
394 | ## Success Criteria
395 | 
396 | 1. **Functionality**: Users can pass `testRunnerEnv` parameter to test tools and have variables appear in test runner environment
397 | 2. **Security**: No unintended environment variable leakage from server process
398 | 3. **Usability**: Users specify unprefixed variable names for better UX
399 | 4. **Compatibility**: All existing test tool calls continue to work unchanged
400 | 5. **Validation**: Comprehensive test coverage proves the feature works end-to-end
401 | 
402 | ## Future Enhancements (Out of Scope)
403 | 
404 | 1. **Configuration Profiles**: Allow users to define common TEST_RUNNER_ variable sets in config files
405 | 2. **Variable Discovery**: Help users discover available TEST_RUNNER_ variables
406 | 3. **Build Tool Support**: Extend to build tools if Apple adds similar BUILD_RUNNER_ support
407 | 4. **Performance Monitoring**: Track impact of environment variable passing on build times
408 | 
409 | ## Implementation Timeline
410 | 
411 | - **Phase 0**: 1-2 hours (reproduction test setup)
412 | - **Phase 1**: 4-6 hours (infrastructure changes)
413 | - **Phase 2**: 3-4 hours (tool integration)
414 | - **Phase 3**: 4-5 hours (testing)  
415 | - **Phase 4**: 2-3 hours (validation)
416 | 
417 | **Total Estimated Time**: 14-20 hours
418 | 
419 | ## Conclusion
420 | 
421 | This implementation plan provides a secure, user-friendly, and architecturally sound solution for TEST_RUNNER_ environment variable support. The explicit parameter approach with automatic prefix handling balances security concerns with user experience, while the test-driven development approach ensures we can prove the solution works as intended.
422 | 
423 | The plan leverages XcodeBuildMCP's existing patterns and provides a foundation for future environment variable needs across the tool ecosystem.
```

--------------------------------------------------------------------------------
/docs/dev/RELOADEROO.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Reloaderoo Integration Guide
  2 | 
  3 | This guide explains how to use Reloaderoo v1.1.2+ for testing and developing XcodeBuildMCP with both CLI inspection tools and transparent proxy capabilities.
  4 | 
  5 | ## Overview
  6 | 
  7 | **Reloaderoo** is a dual-mode MCP development tool that operates as both a CLI inspection tool and a transparent proxy server for the Model Context Protocol (MCP). It provides two distinct operational modes for different development workflows.
  8 | 
  9 | ## Installation
 10 | 
 11 | Reloaderoo is available via npm and can be used with npx for universal compatibility.
 12 | 
 13 | ```bash
 14 | # Use npx to run reloaderoo (works on any system)
 15 | npx reloaderoo@latest --help
 16 | 
 17 | # Or install globally if preferred
 18 | npm install -g reloaderoo
 19 | reloaderoo --help
 20 | ```
 21 | 
 22 | ## Two Operational Modes
 23 | 
 24 | ### 🔍 **CLI Mode** (Inspection & Testing)
 25 | 
 26 | Direct command-line access to MCP servers without client setup - perfect for testing and debugging:
 27 | 
 28 | **Key Benefits:**
 29 | - ✅ **One-shot commands** - Test tools, list resources, get server info
 30 | - ✅ **No MCP client required** - Perfect for testing and debugging
 31 | - ✅ **Raw JSON output** - Ideal for scripts and automation  
 32 | - ✅ **8 inspection commands** - Complete MCP protocol coverage
 33 | - ✅ **AI agent friendly** - Designed for terminal-based AI development workflows
 34 | 
 35 | **Basic Commands:**
 36 | 
 37 | ```bash
 38 | # List all available tools
 39 | npx reloaderoo@latest inspect list-tools -- node build/index.js
 40 | 
 41 | # Call any tool with parameters  
 42 | npx reloaderoo@latest inspect call-tool <tool_name> --params '<json>' -- node build/index.js
 43 | 
 44 | # Get server information
 45 | npx reloaderoo@latest inspect server-info -- node build/index.js
 46 | 
 47 | # List available resources
 48 | npx reloaderoo@latest inspect list-resources -- node build/index.js
 49 | 
 50 | # Read a specific resource
 51 | npx reloaderoo@latest inspect read-resource "<uri>" -- node build/index.js
 52 | 
 53 | # List available prompts
 54 | npx reloaderoo@latest inspect list-prompts -- node build/index.js
 55 | 
 56 | # Get a specific prompt
 57 | npx reloaderoo@latest inspect get-prompt <name> --args '<json>' -- node build/index.js
 58 | 
 59 | # Check server connectivity
 60 | npx reloaderoo@latest inspect ping -- node build/index.js
 61 | ```
 62 | 
 63 | **Example Tool Calls:**
 64 | 
 65 | ```bash
 66 | # List connected devices
 67 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
 68 | 
 69 | # Get doctor information
 70 | npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js
 71 | 
 72 | # List iOS simulators
 73 | npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js
 74 | 
 75 | # Read devices resource
 76 | npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/index.js
 77 | ```
 78 | 
 79 | ### 🔄 **Proxy Mode** (Hot-Reload Development)
 80 | 
 81 | Transparent MCP proxy server that enables seamless hot-reloading during development:
 82 | 
 83 | **Key Benefits:**
 84 | - ✅ **Hot-reload MCP servers** without disconnecting your AI client
 85 | - ✅ **Session persistence** - Keep your development context intact
 86 | - ✅ **Automatic `restart_server` tool** - AI agents can restart servers on demand
 87 | - ✅ **Transparent forwarding** - Full MCP protocol passthrough
 88 | - ✅ **Process management** - Spawns, monitors, and restarts your server process
 89 | 
 90 | **Usage:**
 91 | 
 92 | ```bash
 93 | # Start proxy mode (your AI client connects to this)
 94 | npx reloaderoo@latest proxy -- node build/index.js
 95 | 
 96 | # With debug logging
 97 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
 98 | 
 99 | # Then in your AI session, request:
100 | # "Please restart the MCP server to load my latest changes"
101 | ```
102 | 
103 | The AI agent will automatically call the `restart_server` tool, preserving your session while reloading code changes.
104 | 
105 | ## MCP Inspection Server Mode
106 | 
107 | Start CLI mode as a persistent MCP server for interactive debugging through MCP clients:
108 | 
109 | ```bash
110 | # Start reloaderoo in CLI mode as an MCP server
111 | npx reloaderoo@latest inspect mcp -- node build/index.js
112 | ```
113 | 
114 | This runs CLI mode as a persistent MCP server, exposing 8 debug tools through the MCP protocol:
115 | - `list_tools` - List all server tools
116 | - `call_tool` - Call any server tool
117 | - `list_resources` - List all server resources  
118 | - `read_resource` - Read any server resource
119 | - `list_prompts` - List all server prompts
120 | - `get_prompt` - Get any server prompt
121 | - `get_server_info` - Get comprehensive server information
122 | - `ping` - Test server connectivity
123 | 
124 | ## Claude Code Compatibility
125 | 
126 | When running under Claude Code, XcodeBuildMCP automatically detects the environment and consolidates multiple content blocks into single responses with `---` separators.
127 | 
128 | **Automatic Detection Methods:**
129 | 1. **Environment Variables**: `CLAUDECODE=1` or `CLAUDE_CODE_ENTRYPOINT=cli`
130 | 2. **Parent Process Analysis**: Checks if parent process contains 'claude'
131 | 3. **Graceful Fallback**: Falls back to environment variables if process detection fails
132 | 
133 | **No Configuration Required**: The consolidation happens automatically when Claude Code is detected.
134 | 
135 | ## Command Reference
136 | 
137 | ### Command Structure
138 | 
139 | ```bash
140 | npx reloaderoo@latest [options] [command]
141 | 
142 | Two modes, one tool:
143 | • Proxy MCP server that adds support for hot-reloading MCP servers.
144 | • CLI tool for inspecting MCP servers.
145 | 
146 | Global Options:
147 |   -V, --version    Output the version number
148 |   -h, --help       Display help for command
149 | 
150 | Commands:
151 |   proxy [options]  🔄 Run as MCP proxy server (default behavior)
152 |   inspect          🔍 Inspect and debug MCP servers
153 |   info [options]   📊 Display version and configuration information
154 |   help [command]   ❓ Display help for command
155 | ```
156 | 
157 | ### 🔄 **Proxy Mode Commands**
158 | 
159 | ```bash
160 | npx reloaderoo@latest proxy [options] -- <child-command> [child-args...]
161 | 
162 | Options:
163 |   -w, --working-dir <directory>    Working directory for the child process
164 |   -l, --log-level <level>          Log level (debug, info, notice, warning, error, critical)
165 |   -f, --log-file <path>            Custom log file path (logs to stderr by default)
166 |   -t, --restart-timeout <ms>       Timeout for restart operations (default: 30000ms)
167 |   -m, --max-restarts <number>      Maximum restart attempts (0-10, default: 3)
168 |   -d, --restart-delay <ms>         Delay between restart attempts (default: 1000ms)
169 |   -q, --quiet                      Suppress non-essential output
170 |   --no-auto-restart                Disable automatic restart on crashes
171 |   --debug                          Enable debug mode with verbose logging
172 |   --dry-run                        Validate configuration without starting proxy
173 | 
174 | Examples:
175 |   npx reloaderoo proxy -- node build/index.js
176 |   npx reloaderoo -- node build/index.js                    # Same as above (proxy is default)
177 |   npx reloaderoo proxy --log-level debug -- node build/index.js
178 | ```
179 | 
180 | ### 🔍 **CLI Mode Commands**
181 | 
182 | ```bash
183 | npx reloaderoo@latest inspect [subcommand] [options] -- <child-command> [child-args...]
184 | 
185 | Subcommands:
186 |   server-info [options]            Get server information and capabilities
187 |   list-tools [options]             List all available tools
188 |   call-tool [options] <name>       Call a specific tool
189 |   list-resources [options]         List all available resources
190 |   read-resource [options] <uri>    Read a specific resource
191 |   list-prompts [options]           List all available prompts
192 |   get-prompt [options] <name>      Get a specific prompt
193 |   ping [options]                   Check server connectivity
194 | 
195 | Examples:
196 |   npx reloaderoo@latest inspect list-tools -- node build/index.js
197 |   npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
198 |   npx reloaderoo@latest inspect server-info -- node build/index.js
199 | ```
200 | 
201 | ### **Info Command**
202 | 
203 | ```bash
204 | npx reloaderoo@latest info [options]
205 | 
206 | Options:
207 |   -v, --verbose                    Show detailed information
208 |   -h, --help                       Display help for command
209 |   
210 | Examples:
211 |   npx reloaderoo@latest info              # Show basic system information
212 |   npx reloaderoo@latest info --verbose    # Show detailed system information
213 | ```
214 | 
215 | ### Response Format
216 | 
217 | All CLI commands return structured JSON:
218 | 
219 | ```json
220 | {
221 |   "success": true,
222 |   "data": {
223 |     // Command-specific response data
224 |   },
225 |   "metadata": {
226 |     "command": "call-tool:list_devices",
227 |     "timestamp": "2025-07-25T08:32:47.042Z",
228 |     "duration": 1782
229 |   }
230 | }
231 | ```
232 | 
233 | ### Error Handling
234 | 
235 | When commands fail, you'll receive:
236 | 
237 | ```json
238 | {
239 |   "success": false,
240 |   "error": {
241 |     "message": "Error description",
242 |     "code": "ERROR_CODE"
243 |   },
244 |   "metadata": {
245 |     "command": "failed-command",
246 |     "timestamp": "2025-07-25T08:32:47.042Z",
247 |     "duration": 100
248 |   }
249 | }
250 | ```
251 | 
252 | ## Development Workflow
253 | 
254 | ### 🔍 **CLI Mode Workflow** (Testing & Debugging)
255 | 
256 | Perfect for testing individual tools or debugging server issues without MCP client setup:
257 | 
258 | ```bash
259 | # 1. Build XcodeBuildMCP
260 | npm run build
261 | 
262 | # 2. Test your server quickly
263 | npx reloaderoo@latest inspect list-tools -- node build/index.js
264 | 
265 | # 3. Call specific tools to verify behavior
266 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
267 | 
268 | # 4. Check server health and resources
269 | npx reloaderoo@latest inspect ping -- node build/index.js
270 | npx reloaderoo@latest inspect list-resources -- node build/index.js
271 | ```
272 | 
273 | ### 🔄 **Proxy Mode Workflow** (Hot-Reload Development)
274 | 
275 | For full development sessions with AI clients that need persistent connections:
276 | 
277 | #### 1. **Start Development Session**
278 | Configure your AI client to connect to reloaderoo proxy instead of your server directly:
279 | ```bash
280 | npx reloaderoo@latest proxy -- node build/index.js
281 | # or with debug logging:
282 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
283 | ```
284 | 
285 | #### 2. **Develop Your MCP Server**
286 | Work on your XcodeBuildMCP code as usual - make changes, add tools, modify functionality.
287 | 
288 | #### 3. **Test Changes Instantly**
289 | ```bash
290 | # Rebuild your changes
291 | npm run build
292 | 
293 | # Then ask your AI agent to restart the server:
294 | # "Please restart the MCP server to load my latest changes"
295 | ```
296 | 
297 | The agent will call the `restart_server` tool automatically. Your new capabilities are immediately available!
298 | 
299 | #### 4. **Continue Development**
300 | Your AI session continues with the updated server capabilities. No connection loss, no context reset.
301 | 
302 | ### 🛠️ **MCP Inspection Server** (Interactive CLI Debugging)
303 | 
304 | For interactive debugging through MCP clients:
305 | 
306 | ```bash
307 | # Start reloaderoo CLI mode as an MCP server
308 | npx reloaderoo@latest inspect mcp -- node build/index.js
309 | 
310 | # Then connect with an MCP client to access debug tools
311 | # Available tools: list_tools, call_tool, list_resources, etc.
312 | ```
313 | 
314 | ## Troubleshooting
315 | 
316 | ### 🔄 **Proxy Mode Issues**
317 | 
318 | **Server won't start in proxy mode:**
319 | ```bash
320 | # Check if XcodeBuildMCP runs independently first
321 | node build/index.js
322 | 
323 | # Then try with reloaderoo proxy to validate configuration
324 | npx reloaderoo@latest proxy -- node build/index.js
325 | ```
326 | 
327 | **Connection problems with MCP clients:**
328 | ```bash
329 | # Enable debug logging to see what's happening
330 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
331 | 
332 | # Check system info and configuration
333 | npx reloaderoo@latest info --verbose
334 | ```
335 | 
336 | **Restart failures in proxy mode:**
337 | ```bash
338 | # Increase restart timeout
339 | npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js
340 | 
341 | # Check restart limits  
342 | npx reloaderoo@latest proxy --max-restarts 5 -- node build/index.js
343 | ```
344 | 
345 | ### 🔍 **CLI Mode Issues**
346 | 
347 | **CLI commands failing:**
348 | ```bash
349 | # Test basic connectivity first
350 | npx reloaderoo@latest inspect ping -- node build/index.js
351 | 
352 | # Enable debug logging for CLI commands (via proxy debug mode)
353 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js
354 | ```
355 | 
356 | **JSON parsing errors:**
357 | ```bash
358 | # Check server information for troubleshooting
359 | npx reloaderoo@latest inspect server-info -- node build/index.js
360 | 
361 | # Ensure your server outputs valid JSON
362 | node build/index.js | head -10
363 | ```
364 | 
365 | ### **General Issues**
366 | 
367 | **Command not found:**
368 | ```bash
369 | # Ensure npx can find reloaderoo
370 | npx reloaderoo@latest --help
371 | 
372 | # If that fails, try installing globally
373 | npm install -g reloaderoo
374 | ```
375 | 
376 | **Parameter validation:**
377 | ```bash
378 | # Ensure JSON parameters are properly quoted
379 | npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js
380 | ```
381 | 
382 | ### **General Debug Mode**
383 | 
384 | ```bash
385 | # Get detailed information about what's happening
386 | npx reloaderoo@latest proxy --debug -- node build/index.js  # For proxy mode
387 | npx reloaderoo@latest proxy --log-level debug -- node build/index.js  # For detailed proxy logging
388 | 
389 | # View system information
390 | npx reloaderoo@latest info --verbose
391 | ```
392 | 
393 | ### Debug Tips
394 | 
395 | 1. **Always build first**: Run `npm run build` before testing
396 | 2. **Check tool names**: Use `inspect list-tools` to see exact tool names
397 | 3. **Validate JSON**: Ensure parameters are valid JSON strings
398 | 4. **Enable debug logging**: Use `--log-level debug` or `--debug` for verbose output
399 | 5. **Test connectivity**: Use `inspect ping` to verify server communication
400 | 
401 | ## Advanced Usage
402 | 
403 | ### Environment Variables
404 | 
405 | Configure reloaderoo behavior via environment variables:
406 | 
407 | ```bash
408 | # Logging Configuration
409 | export MCPDEV_PROXY_LOG_LEVEL=debug           # Log level (debug, info, notice, warning, error, critical)
410 | export MCPDEV_PROXY_LOG_FILE=/path/to/log     # Custom log file path (default: stderr)
411 | export MCPDEV_PROXY_DEBUG_MODE=true           # Enable debug mode (true/false)
412 | 
413 | # Process Management
414 | export MCPDEV_PROXY_RESTART_LIMIT=5           # Maximum restart attempts (0-10, default: 3)
415 | export MCPDEV_PROXY_AUTO_RESTART=true         # Enable/disable auto-restart (true/false)
416 | export MCPDEV_PROXY_TIMEOUT=30000             # Operation timeout in milliseconds
417 | export MCPDEV_PROXY_RESTART_DELAY=1000        # Delay between restart attempts in milliseconds
418 | export MCPDEV_PROXY_CWD=/path/to/directory     # Default working directory
419 | ```
420 | 
421 | ### Custom Working Directory
422 | 
423 | ```bash
424 | npx reloaderoo@latest proxy --working-dir /custom/path -- node build/index.js
425 | npx reloaderoo@latest inspect list-tools --working-dir /custom/path -- node build/index.js
426 | ```
427 | 
428 | ### Timeout Configuration
429 | 
430 | ```bash
431 | npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/index.js
432 | ```
433 | 
434 | ## Integration with XcodeBuildMCP
435 | 
436 | Reloaderoo is specifically configured to work with XcodeBuildMCP's:
437 | 
438 | - **84+ Tools**: All workflow groups accessible via CLI
439 | - **4 Resources**: Direct access to devices, simulators, environment, swift-packages
440 | - **Claude Code Detection**: Automatic consolidation of multiple content blocks
441 | - **Hot-Reload Support**: Seamless development workflow with `restart_server`
442 | 
443 | For more information about XcodeBuildMCP's architecture and capabilities, see:
444 | - [Architecture Guide](ARCHITECTURE.md)
445 | - [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)
446 | - [Testing Guide](TESTING.md)
447 | 
```

--------------------------------------------------------------------------------
/scripts/analysis/tools-analysis.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * XcodeBuildMCP Tools Analysis
  5 |  *
  6 |  * Core TypeScript module for analyzing XcodeBuildMCP tools using AST parsing.
  7 |  * Provides reliable extraction of tool information without fallback strategies.
  8 |  */
  9 | 
 10 | import {
 11 |   createSourceFile,
 12 |   forEachChild,
 13 |   isExportAssignment,
 14 |   isIdentifier,
 15 |   isNoSubstitutionTemplateLiteral,
 16 |   isObjectLiteralExpression,
 17 |   isPropertyAssignment,
 18 |   isStringLiteral,
 19 |   isTemplateExpression,
 20 |   isVariableDeclaration,
 21 |   isVariableStatement,
 22 |   type Node,
 23 |   type ObjectLiteralExpression,
 24 |   ScriptTarget,
 25 |   type SourceFile,
 26 |   SyntaxKind,
 27 | } from 'typescript';
 28 | import * as fs from 'fs';
 29 | import * as path from 'path';
 30 | import { glob } from 'glob';
 31 | import { fileURLToPath } from 'url';
 32 | 
 33 | // Get project root
 34 | const __filename = fileURLToPath(import.meta.url);
 35 | const __dirname = path.dirname(__filename);
 36 | const projectRoot = path.resolve(__dirname, '..', '..');
 37 | const toolsDir = path.join(projectRoot, 'src', 'mcp', 'tools');
 38 | 
 39 | export interface ToolInfo {
 40 |   name: string;
 41 |   workflow: string;
 42 |   path: string;
 43 |   relativePath: string;
 44 |   description: string;
 45 |   isCanonical: boolean;
 46 | }
 47 | 
 48 | export interface WorkflowInfo {
 49 |   name: string;
 50 |   displayName: string;
 51 |   description: string;
 52 |   tools: ToolInfo[];
 53 |   toolCount: number;
 54 |   canonicalCount: number;
 55 |   reExportCount: number;
 56 | }
 57 | 
 58 | export interface AnalysisStats {
 59 |   totalTools: number;
 60 |   canonicalTools: number;
 61 |   reExportTools: number;
 62 |   workflowCount: number;
 63 | }
 64 | 
 65 | export interface StaticAnalysisResult {
 66 |   workflows: WorkflowInfo[];
 67 |   tools: ToolInfo[];
 68 |   stats: AnalysisStats;
 69 | }
 70 | 
 71 | /**
 72 |  * Extract the description from a tool's default export using TypeScript AST
 73 |  */
 74 | function extractToolDescription(sourceFile: SourceFile): string {
 75 |   let description: string | null = null;
 76 | 
 77 |   function visit(node: Node): void {
 78 |     let objectExpression: ObjectLiteralExpression | null = null;
 79 | 
 80 |     // Look for export default { ... } - the standard TypeScript pattern
 81 |     // isExportEquals is undefined for `export default` and true for `export = `
 82 |     if (isExportAssignment(node) && !node.isExportEquals) {
 83 |       if (isObjectLiteralExpression(node.expression)) {
 84 |         objectExpression = node.expression;
 85 |       }
 86 |     }
 87 | 
 88 |     if (objectExpression) {
 89 |       // Found export default { ... }, now look for description property
 90 |       for (const property of objectExpression.properties) {
 91 |         if (
 92 |           isPropertyAssignment(property) &&
 93 |           isIdentifier(property.name) &&
 94 |           property.name.text === 'description'
 95 |         ) {
 96 |           // Extract the description value
 97 |           if (isStringLiteral(property.initializer)) {
 98 |             // This is the most common case - simple string literal
 99 |             description = property.initializer.text;
100 |           } else if (
101 |             isTemplateExpression(property.initializer) ||
102 |             isNoSubstitutionTemplateLiteral(property.initializer)
103 |           ) {
104 |             // Handle template literals - get the raw text and clean it
105 |             description = property.initializer.getFullText(sourceFile).trim();
106 |             // Remove surrounding backticks
107 |             if (description.startsWith('`') && description.endsWith('`')) {
108 |               description = description.slice(1, -1);
109 |             }
110 |           } else {
111 |             // Handle any other expression (multiline strings, computed values)
112 |             const fullText = property.initializer.getFullText(sourceFile).trim();
113 |             // This covers cases where the description spans multiple lines
114 |             // Remove surrounding quotes and normalize whitespace
115 |             let cleaned = fullText;
116 |             if (
117 |               (cleaned.startsWith('"') && cleaned.endsWith('"')) ||
118 |               (cleaned.startsWith("'") && cleaned.endsWith("'"))
119 |             ) {
120 |               cleaned = cleaned.slice(1, -1);
121 |             }
122 |             // Collapse multiple whitespaces and newlines into single spaces
123 |             description = cleaned.replace(/\s+/g, ' ').trim();
124 |           }
125 |           return; // Found description, stop looking
126 |         }
127 |       }
128 |     }
129 | 
130 |     forEachChild(node, visit);
131 |   }
132 | 
133 |   visit(sourceFile);
134 | 
135 |   if (description === null) {
136 |     throw new Error('Could not extract description from tool export default object');
137 |   }
138 | 
139 |   return description;
140 | }
141 | 
142 | /**
143 |  * Check if a file is a re-export by examining its content
144 |  */
145 | function isReExportFile(filePath: string): boolean {
146 |   const content = fs.readFileSync(filePath, 'utf-8');
147 | 
148 |   // Remove comments and empty lines, then check for re-export pattern
149 |   // First remove multi-line comments
150 |   const contentWithoutBlockComments = content.replace(/\/\*[\s\S]*?\*\//g, '');
151 | 
152 |   const cleanedLines = contentWithoutBlockComments
153 |     .split('\n')
154 |     .map((line) => {
155 |       // Remove inline comments but preserve the code before them
156 |       const codeBeforeComment = line.split('//')[0].trim();
157 |       return codeBeforeComment;
158 |     })
159 |     .filter((line) => line.length > 0);
160 | 
161 |   // Should have exactly one line: export { default } from '...';
162 |   if (cleanedLines.length !== 1) {
163 |     return false;
164 |   }
165 | 
166 |   const exportLine = cleanedLines[0];
167 |   return /^export\s*{\s*default\s*}\s*from\s*['"][^'"]+['"];?\s*$/.test(exportLine);
168 | }
169 | 
170 | /**
171 |  * Get workflow metadata from index.ts file if it exists
172 |  */
173 | async function getWorkflowMetadata(
174 |   workflowDir: string,
175 | ): Promise<{ displayName: string; description: string } | null> {
176 |   const indexPath = path.join(toolsDir, workflowDir, 'index.ts');
177 | 
178 |   if (!fs.existsSync(indexPath)) {
179 |     return null;
180 |   }
181 | 
182 |   try {
183 |     const content = fs.readFileSync(indexPath, 'utf-8');
184 |     const sourceFile = createSourceFile(indexPath, content, ScriptTarget.Latest, true);
185 | 
186 |     const workflowExport: { name?: string; description?: string } = {};
187 | 
188 |     function visit(node: Node): void {
189 |       // Look for: export const workflow = { ... }
190 |       if (
191 |         isVariableStatement(node) &&
192 |         node.modifiers?.some((mod) => mod.kind === SyntaxKind.ExportKeyword)
193 |       ) {
194 |         for (const declaration of node.declarationList.declarations) {
195 |           if (
196 |             isVariableDeclaration(declaration) &&
197 |             isIdentifier(declaration.name) &&
198 |             declaration.name.text === 'workflow' &&
199 |             declaration.initializer &&
200 |             isObjectLiteralExpression(declaration.initializer)
201 |           ) {
202 |             // Extract name and description properties
203 |             for (const property of declaration.initializer.properties) {
204 |               if (isPropertyAssignment(property) && isIdentifier(property.name)) {
205 |                 const propertyName = property.name.text;
206 | 
207 |                 if (propertyName === 'name' && isStringLiteral(property.initializer)) {
208 |                   workflowExport.name = property.initializer.text;
209 |                 } else if (
210 |                   propertyName === 'description' &&
211 |                   isStringLiteral(property.initializer)
212 |                 ) {
213 |                   workflowExport.description = property.initializer.text;
214 |                 }
215 |               }
216 |             }
217 |           }
218 |         }
219 |       }
220 | 
221 |       forEachChild(node, visit);
222 |     }
223 | 
224 |     visit(sourceFile);
225 | 
226 |     if (workflowExport.name && workflowExport.description) {
227 |       return {
228 |         displayName: workflowExport.name,
229 |         description: workflowExport.description,
230 |       };
231 |     }
232 |   } catch (error) {
233 |     console.error(`Warning: Could not parse workflow metadata from ${indexPath}: ${error}`);
234 |   }
235 | 
236 |   return null;
237 | }
238 | 
239 | /**
240 |  * Get a human-readable workflow name from directory name
241 |  */
242 | function getWorkflowDisplayName(workflowDir: string): string {
243 |   const displayNames: Record<string, string> = {
244 |     device: 'iOS Device Development',
245 |     doctor: 'System Doctor',
246 |     logging: 'Logging & Monitoring',
247 |     macos: 'macOS Development',
248 |     'project-discovery': 'Project Discovery',
249 |     'project-scaffolding': 'Project Scaffolding',
250 |     simulator: 'iOS Simulator Development',
251 |     'simulator-management': 'Simulator Management',
252 |     'swift-package': 'Swift Package Manager',
253 |     'ui-testing': 'UI Testing & Automation',
254 |     utilities: 'Utilities',
255 |   };
256 | 
257 |   return displayNames[workflowDir] || workflowDir;
258 | }
259 | 
260 | /**
261 |  * Get workflow description
262 |  */
263 | function getWorkflowDescription(workflowDir: string): string {
264 |   const descriptions: Record<string, string> = {
265 |     device: 'Physical device development, testing, and deployment',
266 |     doctor: 'System health checks and environment validation',
267 |     logging: 'Log capture and monitoring across platforms',
268 |     macos: 'Native macOS application development and testing',
269 |     'project-discovery': 'Project analysis and information gathering',
270 |     'project-scaffolding': 'Create new projects from templates',
271 |     simulator: 'Simulator-based development, testing, and deployment',
272 |     'simulator-management': 'Simulator environment and configuration management',
273 |     'swift-package': 'Swift Package development and testing',
274 |     'ui-testing': 'Automated UI interaction and testing',
275 |     utilities: 'General utility operations',
276 |   };
277 | 
278 |   return descriptions[workflowDir] || `${workflowDir} related tools`;
279 | }
280 | 
281 | /**
282 |  * Perform static analysis of all tools in the project
283 |  */
284 | export async function getStaticToolAnalysis(): Promise<StaticAnalysisResult> {
285 |   // Find all workflow directories
286 |   const workflowDirs = fs
287 |     .readdirSync(toolsDir, { withFileTypes: true })
288 |     .filter((dirent) => dirent.isDirectory())
289 |     .map((dirent) => dirent.name)
290 |     .sort();
291 | 
292 |   // Find all tool files
293 |   const files = await glob('**/*.ts', {
294 |     cwd: toolsDir,
295 |     ignore: [
296 |       '**/__tests__/**',
297 |       '**/index.ts',
298 |       '**/*.test.ts',
299 |       '**/lib/**',
300 |       '**/*-processes.ts', // Process management utilities
301 |       '**/*.deps.ts', // Dependency files
302 |       '**/*-utils.ts', // Utility files
303 |       '**/*-common.ts', // Common/shared code
304 |       '**/*-types.ts', // Type definition files
305 |     ],
306 |     absolute: true,
307 |   });
308 | 
309 |   const allTools: ToolInfo[] = [];
310 |   const workflowMap = new Map<string, ToolInfo[]>();
311 | 
312 |   let canonicalCount = 0;
313 |   let reExportCount = 0;
314 | 
315 |   // Initialize workflow map
316 |   for (const workflowDir of workflowDirs) {
317 |     workflowMap.set(workflowDir, []);
318 |   }
319 | 
320 |   // Process each tool file
321 |   for (const filePath of files) {
322 |     const toolName = path.basename(filePath, '.ts');
323 |     const workflowDir = path.basename(path.dirname(filePath));
324 |     const relativePath = path.relative(projectRoot, filePath);
325 | 
326 |     const isReExport = isReExportFile(filePath);
327 | 
328 |     let description = '';
329 | 
330 |     if (!isReExport) {
331 |       // Extract description from canonical tool using AST
332 |       try {
333 |         const content = fs.readFileSync(filePath, 'utf-8');
334 |         const sourceFile = createSourceFile(filePath, content, ScriptTarget.Latest, true);
335 | 
336 |         description = extractToolDescription(sourceFile);
337 |         canonicalCount++;
338 |       } catch (error) {
339 |         throw new Error(`Failed to extract description from ${relativePath}: ${error}`);
340 |       }
341 |     } else {
342 |       description = '(Re-exported from shared workflow)';
343 |       reExportCount++;
344 |     }
345 | 
346 |     const toolInfo: ToolInfo = {
347 |       name: toolName,
348 |       workflow: workflowDir,
349 |       path: filePath,
350 |       relativePath,
351 |       description,
352 |       isCanonical: !isReExport,
353 |     };
354 | 
355 |     allTools.push(toolInfo);
356 | 
357 |     const workflowTools = workflowMap.get(workflowDir);
358 |     if (workflowTools) {
359 |       workflowTools.push(toolInfo);
360 |     }
361 |   }
362 | 
363 |   // Build workflow information
364 |   const workflows: WorkflowInfo[] = [];
365 | 
366 |   for (const workflowDir of workflowDirs) {
367 |     const workflowTools = workflowMap.get(workflowDir) ?? [];
368 |     const canonicalTools = workflowTools.filter((t) => t.isCanonical);
369 |     const reExportTools = workflowTools.filter((t) => !t.isCanonical);
370 | 
371 |     // Try to get metadata from index.ts, fall back to hardcoded names/descriptions
372 |     const metadata = await getWorkflowMetadata(workflowDir);
373 | 
374 |     const workflowInfo: WorkflowInfo = {
375 |       name: workflowDir,
376 |       displayName: metadata?.displayName ?? getWorkflowDisplayName(workflowDir),
377 |       description: metadata?.description ?? getWorkflowDescription(workflowDir),
378 |       tools: workflowTools.sort((a, b) => a.name.localeCompare(b.name)),
379 |       toolCount: workflowTools.length,
380 |       canonicalCount: canonicalTools.length,
381 |       reExportCount: reExportTools.length,
382 |     };
383 | 
384 |     workflows.push(workflowInfo);
385 |   }
386 | 
387 |   const stats: AnalysisStats = {
388 |     totalTools: allTools.length,
389 |     canonicalTools: canonicalCount,
390 |     reExportTools: reExportCount,
391 |     workflowCount: workflows.length,
392 |   };
393 | 
394 |   return {
395 |     workflows: workflows.sort((a, b) => a.displayName.localeCompare(b.displayName)),
396 |     tools: allTools.sort((a, b) => a.name.localeCompare(b.name)),
397 |     stats,
398 |   };
399 | }
400 | 
401 | /**
402 |  * Get only canonical tools (excluding re-exports) for documentation generation
403 |  */
404 | export async function getCanonicalTools(): Promise<ToolInfo[]> {
405 |   const analysis = await getStaticToolAnalysis();
406 |   return analysis.tools.filter((tool) => tool.isCanonical);
407 | }
408 | 
409 | /**
410 |  * Get tools grouped by workflow for documentation generation
411 |  */
412 | export async function getToolsByWorkflow(): Promise<Map<string, ToolInfo[]>> {
413 |   const analysis = await getStaticToolAnalysis();
414 |   const workflowMap = new Map<string, ToolInfo[]>();
415 | 
416 |   for (const workflow of analysis.workflows) {
417 |     // Only include canonical tools for documentation
418 |     const canonicalTools = workflow.tools.filter((tool) => tool.isCanonical);
419 |     if (canonicalTools.length > 0) {
420 |       workflowMap.set(workflow.name, canonicalTools);
421 |     }
422 |   }
423 | 
424 |   return workflowMap;
425 | }
426 | 
427 | // CLI support - if run directly, perform analysis and output results
428 | if (import.meta.url === `file://${process.argv[1]}`) {
429 |   async function main(): Promise<void> {
430 |     try {
431 |       console.log('🔍 Performing static analysis...');
432 |       const analysis = await getStaticToolAnalysis();
433 | 
434 |       console.log('\n📊 Analysis Results:');
435 |       console.log(`   Workflows: ${analysis.stats.workflowCount}`);
436 |       console.log(`   Total tools: ${analysis.stats.totalTools}`);
437 |       console.log(`   Canonical tools: ${analysis.stats.canonicalTools}`);
438 |       console.log(`   Re-export tools: ${analysis.stats.reExportTools}`);
439 | 
440 |       if (process.argv.includes('--json')) {
441 |         console.log('\n' + JSON.stringify(analysis, null, 2));
442 |       } else {
443 |         console.log('\n📂 Workflows:');
444 |         for (const workflow of analysis.workflows) {
445 |           console.log(
446 |             `   • ${workflow.displayName} (${workflow.canonicalCount} canonical, ${workflow.reExportCount} re-exports)`,
447 |           );
448 |         }
449 |       }
450 |     } catch (error) {
451 |       console.error('❌ Analysis failed:', error);
452 |       process.exit(1);
453 |     }
454 |   }
455 | 
456 |   main();
457 | }
458 | 
```
Page 10/16FirstPrevNextLast