The Command Execution Layer provides a unified abstraction for executing system commands and file system operations throughout XcodeBuildMCP. This layer serves as the boundary between tool logic and actual system interactions, enabling testability through dependency injection while maintaining type safety and consistent error handling.
This document covers the CommandExecutor and FileSystemExecutor abstractions, test safety mechanisms, and integration patterns. For information about how tools are invoked and orchestrated, see Tool Invocation Pipeline. For comprehensive testing patterns, see Testing Architecture.
The type definitions live in two dedicated files:
src/utils/CommandExecutor.ts — defines CommandExecutor, CommandResponse, and CommandExecOptionssrc/utils/FileSystemExecutor.ts — defines FileSystemExecutorsrc/utils/command.ts imports from both and re-exports them for backward compatibility src/utils/command.ts19-21
The CommandExecutor type is a function signature: it accepts a command array, optional logging prefix, shell mode flag, execution options, and detached mode flag, and returns a Promise<CommandResponse>.
Types and their code locations:
| Type | Defined in | Re-exported from |
|---|---|---|
CommandExecutor | src/utils/CommandExecutor.ts | src/utils/command.ts |
CommandResponse | src/utils/CommandExecutor.ts | src/utils/command.ts |
CommandExecOptions | src/utils/CommandExecutor.ts | src/utils/command.ts |
FileSystemExecutor | src/utils/FileSystemExecutor.ts | src/utils/command.ts |
Type structure:
Sources: src/utils/command.ts17-20
The CommandResponse output field holds captured stdout; error holds stderr (only populated on failure). Tools inspect success and exitCode to determine how to proceed.
The production implementation uses Node.js spawn for process management with comprehensive output capture and error handling.
Sources: src/utils/command.ts33-163
When useShell is true, the executor escapes shell metacharacters to prevent injection vulnerabilities and command parsing errors. The regex pattern /[\s,"'=$;&|<>(){}[]\*?~]/` is used to detect arguments that require quoting src/utils/command.ts47
Shell escaping flow:
Sources: src/utils/command.ts40-56
For non-shell xcodebuild commands, the executor automatically wraps them with xcrun to ensure proper Xcode toolchain resolution:
Sources: src/utils/command.ts62-68
This ensures builds use the active Xcode installation selected via xcode-select.
The Command Execution Layer implements strict test safety guards to prevent accidental real command execution during tests, enforcing explicit mock injection.
Sources: src/utils/command.ts224-256
The getDefaultCommandExecutor() and getDefaultFileSystemExecutor() functions enforce test safety:
| Function | Purpose | Test Behavior | Production Behavior |
|---|---|---|---|
getDefaultCommandExecutor() | Command execution guard | Throws if no mock override set | Returns defaultExecutor |
getDefaultFileSystemExecutor() | File system guard | Throws if no mock override set | Returns defaultFileSystemExecutor |
__setTestCommandExecutorOverride() | Test setup | Sets mock executor | Should not be called |
__setTestFileSystemExecutorOverride() | Test setup | Sets mock filesystem | Should not be called |
__clearTestExecutorOverrides() | Test cleanup | Clears mocks | Should not be called |
Sources: src/utils/command.ts227-274
The error message provides clear remediation steps for developers:
🚨 REAL SYSTEM EXECUTOR DETECTED IN TEST! 🚨
This test is trying to use the default command executor instead of a mock.
Fix: Pass createMockExecutor() as the commandExecutor parameter in your test.
Example: await plugin.handler(args, createMockExecutor({success: true}), mockFileSystem)
See docs/dev/TESTING.md for proper testing patterns.
Sources: src/utils/command.ts247-252
The FileSystemExecutor interface (defined in src/utils/FileSystemExecutor.ts, re-exported by src/utils/command.ts) provides a parallel abstraction for file system operations. The production singleton defaultFileSystemExecutor is a private const inside src/utils/command.ts src/utils/command.ts169-222
Interface methods and their backing implementations:
Sources: src/utils/command.ts169-222
The default implementation uses dynamic import('fs/promises') for async operations and direct imports (existsSync, createWriteStream) for synchronous calls. Test code substitutes a full mock via __setTestFileSystemExecutorOverride().
Tools receive executors through dependency injection, with production code calling guard functions and test code injecting mocks.
Sources: Example pattern used throughout codebase
executeXcodeBuildCommand in src/utils/build-utils.ts is the canonical example of CommandExecutor integration. The executor parameter is required (not optional), forcing callers to be explicit about which executor they provide src/utils/build-utils.ts52-59
executeXcodeBuildCommand execution flow:
Sources: src/utils/build-utils.ts52-392
Key integration points:
| Aspect | Detail |
|---|---|
| Required executor | executor: CommandExecutor is non-optional, forcing explicit injection |
| cwd propagation | projectDir (derived from workspace/project path) is passed as cwd so CocoaPods relative paths resolve correctly |
| No default fallback | Callers must provide an executor; getDefaultCommandExecutor() is called at the call site (e.g., tool handler), not inside this function |
| Warning filtering | suppressWarnings session key is read from sessionStore to optionally suppress ⚠️ Warning: lines |
MCP resource handlers use an optional executor parameter pattern, accepting CommandExecutor | undefined and calling getDefaultCommandExecutor() internally when none is supplied. This is reflected in the ResourceMeta.handler signature in src/core/resources.ts src/core/resources.ts28-34
The defaultExecutor implementation handles process spawning, output capture, and error management.
Sources: src/utils/command.ts75-163
For long-running processes like video recording, the detached parameter enables fire-and-forget execution:
Sources: src/utils/command.ts109-141
This pattern is used by tools like simulator_video_start that need to return control immediately while recording continues.
The implementation captures and logs detailed spawn errors:
Sources: src/utils/command.ts83-94
Error details include code, errno, syscall, path, spawnargs, and stack, providing comprehensive diagnostics for system-level failures.
The Command Execution Layer implements a consistent dependency injection pattern across all tools and utilities.
Sources: Pattern enforced by src/utils/command.ts244-256
Sources: src/utils/command.ts244-256
The typical pattern for a tool that uses both CommandExecutor and FileSystemExecutor:
executor: CommandExecutor (required) and optionally fileSystem: FileSystemExecutorfileSystem, tools call getDefaultFileSystemExecutor() when none is provided, because it is often optional in tool signaturesgetDefaultCommandExecutor() is typically called at the tool's entry point or passed through from the invoker layerspawn or fscreateMockExecutor({success: true, output: '...'}) and a matching mock filesystem, passing both into the handlerDefaultToolInvoker, which ultimately calls getDefaultCommandExecutor() onceThe executeXcodeBuildCommand function at src/utils/build-utils.ts52-59 is the most complete real example: it receives a required executor, constructs an xcodebuild command array, and calls executor(command, logPrefix, false, {cwd: projectDir}).
The Command Execution Layer provides:
| Component | Purpose | Key Files |
|---|---|---|
CommandExecutor | Command execution abstraction | src/utils/command.ts23-39 |
CommandResponse | Standardized result format | src/types/common.ts112-118 |
defaultExecutor | Production implementation | src/utils/command.ts33-163 |
| Test guards | Prevent accidental real execution | src/utils/command.ts244-274 |
FileSystemExecutor | File system abstraction | src/utils/command.ts169-222 |
| Dependency injection | Testability pattern | Used throughout codebase |
This architecture enables comprehensive unit testing without real system commands while maintaining type safety and clear error handling in production.
Refresh this wiki
This wiki was recently refreshed. Please wait 4 days to refresh again.