From 1c9c5443eda00fec00f5b4921ab70742370d4e1e Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 8 Apr 2026 22:06:08 +0100 Subject: [PATCH 1/3] refactor: update manifests, config, docs, and example projects for rendering pipeline --- AGENTS.md | 1 + docs/CLI.md | 35 + docs/TOOLS-CLI.md | 35 +- docs/TOOLS.md | 35 +- docs/dev/ARCHITECTURE.md | 28 +- docs/dev/FIXTURE_DESIGNS.md | 877 +++++++++++++ ...STIGATION_OUTPUT_FORMATTING_CONSISTENCY.md | 210 ++++ docs/dev/MANIFEST_FORMAT.md | 91 +- docs/dev/QUERY_TOOL_FORMAT_SPEC.md | 123 ++ docs/dev/RENDERING_PIPELINE.md | 280 +++++ docs/dev/RENDERING_PIPELINE_REFACTOR.md | 382 ++++++ docs/dev/STRUCTURED_XCODEBUILD_EVENTS_PLAN.md | 723 +++++++++++ docs/dev/TESTING.md | 39 +- docs/dev/TOOL_DISCOVERY_LOGIC.md | 23 +- docs/dev/simulator-test-benchmark.md | 83 ++ .../CalculatorApp/CalculatorApp.swift | 15 + .../CalculatorAppFeature/ContentView.swift | 2 +- .../CalculatorAppTests.swift | 18 + .../CompileError.fixture.swift | 3 + .../macOS/MCPTest.xcodeproj/project.pbxproj | 4 +- .../macOS/MCPTestTests/MCPTestTests.swift | 15 +- .../macOS/MCPTestTests/MCPTestsXCTests.swift | 13 + .../spm/.xcodebuildmcp/config.yaml | 9 + .../spm/Sources/quick-task/main.swift | 2 +- .../spm/Tests/TestLibTests/SimpleTests.swift | 17 +- knip.json | 26 + .../monolithic-workflows-investigation.md | 284 +++++ local-research/pipeline-coupling-audit.md | 121 ++ .../rendering-pipeline-remaining-cleanup.md | 36 + ...codebuild-command-builder-investigation.md | 157 +++ manifests/resources/devices.yaml | 6 + manifests/resources/doctor.yaml | 6 + manifests/resources/session-status.yaml | 6 + manifests/resources/simulators.yaml | 6 + manifests/resources/xcode-ide-state.yaml | 8 + manifests/tools/boot_sim.yaml | 3 + manifests/tools/build_device.yaml | 5 + manifests/tools/build_macos.yaml | 5 + manifests/tools/build_run_device.yaml | 3 +- manifests/tools/build_run_macos.yaml | 4 + manifests/tools/build_run_sim.yaml | 3 +- manifests/tools/build_sim.yaml | 5 + manifests/tools/debug_attach_sim.yaml | 3 + manifests/tools/discover_projs.yaml | 2 + manifests/tools/get_app_bundle_id.yaml | 4 + manifests/tools/get_coverage_report.yaml | 1 + manifests/tools/get_device_app_path.yaml | 3 + manifests/tools/get_file_coverage.yaml | 1 + manifests/tools/get_mac_app_path.yaml | 2 + manifests/tools/get_mac_bundle_id.yaml | 2 + manifests/tools/get_sim_app_path.yaml | 4 + manifests/tools/install_app_sim.yaml | 2 + manifests/tools/launch_app_device.yaml | 1 + manifests/tools/launch_app_sim.yaml | 7 +- manifests/tools/list_devices.yaml | 3 + manifests/tools/list_schemes.yaml | 4 + manifests/tools/list_sims.yaml | 4 + manifests/tools/open_sim.yaml | 1 + manifests/tools/record_sim_video.yaml | 1 + manifests/tools/scaffold_ios_project.yaml | 3 + manifests/tools/scaffold_macos_project.yaml | 3 + manifests/tools/show_build_settings.yaml | 3 + manifests/tools/snapshot_ui.yaml | 3 + package-lock.json | 1115 ++++++++--------- package.json | 18 +- scripts/benchmark-simulator-test.ts | 264 ++++ scripts/capture-xcodebuild-wrapper.ts | 128 ++ scripts/copy-build-assets.js | 33 - .../vitest-executor-safety.setup.ts | 34 + vitest.config.ts | 2 + vitest.flowdeck.config.ts | 23 + vitest.snapshot.config.ts | 27 + 72 files changed, 4728 insertions(+), 720 deletions(-) create mode 100644 docs/dev/FIXTURE_DESIGNS.md create mode 100644 docs/dev/INVESTIGATION_OUTPUT_FORMATTING_CONSISTENCY.md create mode 100644 docs/dev/QUERY_TOOL_FORMAT_SPEC.md create mode 100644 docs/dev/RENDERING_PIPELINE.md create mode 100644 docs/dev/RENDERING_PIPELINE_REFACTOR.md create mode 100644 docs/dev/STRUCTURED_XCODEBUILD_EVENTS_PLAN.md create mode 100644 docs/dev/simulator-test-benchmark.md create mode 100644 example_projects/iOS_Calculator/manual-fixtures/CalculatorAppTests/CompileError.fixture.swift create mode 100644 example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift create mode 100644 example_projects/spm/.xcodebuildmcp/config.yaml create mode 100644 knip.json create mode 100644 local-research/monolithic-workflows-investigation.md create mode 100644 local-research/pipeline-coupling-audit.md create mode 100644 local-research/rendering-pipeline-remaining-cleanup.md create mode 100644 local-research/xcodebuild-command-builder-investigation.md create mode 100644 manifests/resources/devices.yaml create mode 100644 manifests/resources/doctor.yaml create mode 100644 manifests/resources/session-status.yaml create mode 100644 manifests/resources/simulators.yaml create mode 100644 manifests/resources/xcode-ide-state.yaml create mode 100644 scripts/benchmark-simulator-test.ts create mode 100644 scripts/capture-xcodebuild-wrapper.ts delete mode 100644 scripts/copy-build-assets.js create mode 100644 src/test-utils/vitest-executor-safety.setup.ts create mode 100644 vitest.flowdeck.config.ts create mode 100644 vitest.snapshot.config.ts diff --git a/AGENTS.md b/AGENTS.md index 108093fe..a9ec8a14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ Use these sections under `## [Unreleased]`: - Append to existing subsections (e.g., `### Fixed`), do not create duplicates - NEVER modify already-released version sections (e.g., `## [0.12.2]`) - Each version section is immutable once released +- NEVER update snapshot fixtures unless asked to do so, these are integration tests, on failure assume code is wrong before questioning the fixture - #### Attribution - **Internal changes (from issues)**: `Fixed foo bar ([#123](https://github.com/cameroncook/XcodeBuildMCP/issues/123))` diff --git a/docs/CLI.md b/docs/CLI.md index 737627ee..279b6f4f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -77,6 +77,34 @@ xcodebuildmcp simulator launch-app --simulator-id --bundle-id io.sentry.M xcodebuildmcp simulator build-and-run --scheme MyApp --project-path ./MyApp.xcodeproj ``` +### Human-readable build-and-run output + +For xcodebuild-backed build-and-run tools: + +- CLI text mode prints a durable preflight block first +- interactive terminals then show active phases as live replace-in-place updates +- warnings, errors, failures, summaries, and next steps are durable output +- success output order is: front matter -> runtime state/diagnostics -> summary -> execution-derived footer -> next steps +- failed structured xcodebuild runs do not render next steps +- compiler/build diagnostics should be grouped into a readable failure block before the failed summary +- the final footer should only contain execution-derived values such as app path, bundle ID, app ID, or process ID +- requested values like scheme, project/workspace, configuration, and platform stay in front matter and should not be repeated later +- when the tool computes a concrete value during execution, prefer showing it directly in the footer instead of relegating it to a hint or redundant next step + +For example, a successful build-and-run footer should prefer: + +```text +✅ Build & Run complete + + └ App Path: /tmp/.../MyApp.app +``` + +rather than forcing the user to run another command just to retrieve a value the tool already knows. + +MCP uses the same human-readable formatting semantics, but buffers the rendered output instead of streaming it to stdout live. It is the same section model and ordering, just a different sink. + +`--output json` is still streamed JSONL events, not the human-readable section format. + ### Testing ```bash @@ -85,8 +113,15 @@ xcodebuildmcp simulator test --scheme MyAppTests --project-path ./MyApp.xcodepro # Run with specific simulator xcodebuildmcp simulator test --scheme MyAppTests --simulator-name "iPhone 17 Pro" + +# Run with pre-resolved test discovery and live progress +xcodebuildmcp simulator test --json '{"workspacePath":"./MyApp.xcworkspace","scheme":"MyApp","simulatorName":"iPhone 17 Pro","progress":true,"extraArgs":["-only-testing:MyAppTests"]}' ``` +Simulator test output now pre-resolves concrete Swift XCTest and Swift Testing cases when it can, then streams filtered milestones for package resolution, compilation, and test execution plus a grouped failure summary instead of raw `xcodebuild` noise. + +For reproducible performance comparisons against Flowdeck CLI, see [dev/simulator-test-benchmark.md](dev/simulator-test-benchmark.md). + For a full list of workflows and tools, see [TOOLS-CLI.md](TOOLS-CLI.md). ## Configuration diff --git a/docs/TOOLS-CLI.md b/docs/TOOLS-CLI.md index 89ceaf3f..87aa254b 100644 --- a/docs/TOOLS-CLI.md +++ b/docs/TOOLS-CLI.md @@ -2,7 +2,7 @@ This document lists CLI tool names as exposed by `xcodebuildmcp `. -XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. +XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups. ## Workflow Groups @@ -22,10 +22,10 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. ### iOS Device Development (`device`) -**Purpose**: Complete iOS development workflow for physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). (17 tools) +**Purpose**: Complete iOS development workflow for physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). (15 tools) - `build` - Build for device. -- `build-and-run` - Build, install, and launch on physical device. Preferred single-step run tool when defaults are set. +- `build-and-run` - Build, install, and launch on physical device. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. - `clean` - Clean build products. - `discover-projects` - Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files. Use when project/workspace path is unknown. - `get-app-bundle-id` - Extract bundle id from .app. @@ -37,19 +37,17 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. - `list` - List connected devices. - `list-schemes` - List Xcode schemes. - `show-build-settings` - Show build settings. -- `start-device-log-capture` - Start device log capture. - `stop` - Stop device app. -- `stop-device-log-capture` - Stop device app and return logs. - `test` - Test on device. ### iOS Simulator Development (`simulator`) -**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. (23 tools) +**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. (20 tools) - `boot` - Defined in Simulator Management workflow. - `build` - Build for iOS sim (compile-only, no launch). -- `build-and-run` - Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Preferred single-step run tool when defaults are set. +- `build-and-run` - Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. - `clean` - Defined in iOS Device Development workflow. - `discover-projects` - Defined in iOS Device Development workflow. - `get-app-bundle-id` - Defined in iOS Device Development workflow. @@ -57,8 +55,7 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. - `get-coverage-report` - Defined in Code Coverage workflow. - `get-file-coverage` - Defined in Code Coverage workflow. - `install` - Install app on sim. -- `launch-app` - Launch app on simulator. -- `launch-app-with-logs` - Launch sim app with logs. +- `launch-app` - Launch app on simulator. Runtime logs are captured automatically and the log file path is included in the response. - `list` - Defined in Simulator Management workflow. - `list-schemes` - Defined in iOS Device Development workflow. - `open` - Defined in Simulator Management workflow. @@ -66,9 +63,7 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. - `screenshot` - Capture screenshot. - `show-build-settings` - Defined in iOS Device Development workflow. - `snapshot-ui` - Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements. -- `start-simulator-log-capture` - Defined in Log Capture workflow. - `stop` - Stop sim app. -- `stop-simulator-log-capture` - Defined in Log Capture workflow. - `test` - Test on iOS sim. @@ -87,16 +82,6 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. -### Log Capture (`logging`) -**Purpose**: Capture and retrieve logs from simulator and device apps. (4 tools) - -- `start-device-log-capture` - Defined in iOS Device Development workflow. -- `start-simulator-log-capture` - Start sim log capture. -- `stop-device-log-capture` - Defined in iOS Device Development workflow. -- `stop-simulator-log-capture` - Stop sim app and return logs. - - - ### macOS Development (`macos`) **Purpose**: Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications. (13 tools) @@ -200,10 +185,10 @@ XcodeBuildMCP provides 76 canonical tools organized into 14 workflow groups. ## Summary Statistics -- **Canonical Tools**: 76 -- **Total Tools**: 108 -- **Workflow Groups**: 14 +- **Canonical Tools**: 71 +- **Total Tools**: 99 +- **Workflow Groups**: 13 --- -*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-03-16T20:47:13.697Z UTC* +*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-07T11:23:03.868Z UTC* diff --git a/docs/TOOLS.md b/docs/TOOLS.md index fd054466..bf9c1b5a 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -1,6 +1,6 @@ # XcodeBuildMCP MCP Tools Reference -This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 82 canonical tools organized into 16 workflow groups for comprehensive Apple development workflows. +This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 77 canonical tools organized into 15 workflow groups for comprehensive Apple development workflows. ## Workflow Groups @@ -20,10 +20,10 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov ### iOS Device Development (`device`) -**Purpose**: Complete iOS development workflow for physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). (17 tools) +**Purpose**: Complete iOS development workflow for physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). (15 tools) - `build_device` - Build for device. -- `build_run_device` - Build, install, and launch on physical device. Preferred single-step run tool when defaults are set. +- `build_run_device` - Build, install, and launch on physical device. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. - `clean` - Clean build products. - `discover_projs` - Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files. Use when project/workspace path is unknown. - `get_app_bundle_id` - Extract bundle id from .app. @@ -35,18 +35,16 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov - `list_devices` - List connected devices. - `list_schemes` - List Xcode schemes. - `show_build_settings` - Show build settings. -- `start_device_log_cap` - Start device log capture. - `stop_app_device` - Stop device app. -- `stop_device_log_cap` - Stop device app and return logs. - `test_device` - Test on device. ### iOS Simulator Development (`simulator`) -**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. (23 tools) +**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. (20 tools) - `boot_sim` - Defined in Simulator Management workflow. -- `build_run_sim` - Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Preferred single-step run tool when defaults are set. +- `build_run_sim` - Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. - `build_sim` - Build for iOS sim (compile-only, no launch). - `clean` - Defined in iOS Device Development workflow. - `discover_projs` - Defined in iOS Device Development workflow. @@ -55,8 +53,7 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov - `get_file_coverage` - Defined in Code Coverage workflow. - `get_sim_app_path` - Get sim built app path. - `install_app_sim` - Install app on sim. -- `launch_app_logs_sim` - Launch sim app with logs. -- `launch_app_sim` - Launch app on simulator. +- `launch_app_sim` - Launch app on simulator. Runtime logs are captured automatically and the log file path is included in the response. - `list_schemes` - Defined in iOS Device Development workflow. - `list_sims` - Defined in Simulator Management workflow. - `open_sim` - Defined in Simulator Management workflow. @@ -64,9 +61,7 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov - `screenshot` - Capture screenshot. - `show_build_settings` - Defined in iOS Device Development workflow. - `snapshot_ui` - Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements. -- `start_sim_log_cap` - Defined in Log Capture workflow. - `stop_app_sim` - Stop sim app. -- `stop_sim_log_cap` - Defined in Log Capture workflow. - `test_sim` - Test on iOS sim. @@ -85,16 +80,6 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov -### Log Capture (`logging`) -**Purpose**: Capture and retrieve logs from simulator and device apps. (4 tools) - -- `start_device_log_cap` - Defined in iOS Device Development workflow. -- `start_sim_log_cap` - Start sim log capture. -- `stop_device_log_cap` - Defined in iOS Device Development workflow. -- `stop_sim_log_cap` - Stop sim app and return logs. - - - ### macOS Development (`macos`) **Purpose**: Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications. (13 tools) @@ -216,10 +201,10 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov ## Summary Statistics -- **Canonical Tools**: 82 -- **Total Tools**: 114 -- **Workflow Groups**: 16 +- **Canonical Tools**: 77 +- **Total Tools**: 105 +- **Workflow Groups**: 15 --- -*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-03-16T20:47:13.697Z UTC* +*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-07T11:23:03.868Z UTC* diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 5a0c7583..3d43d930 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -38,20 +38,20 @@ XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operat - MCP server created with stdio transport - Plugin discovery system initialized -3. **Plugin Discovery (Build-Time)** - - A build-time script (`build-plugins/plugin-discovery.ts`) scans the `src/mcp/tools/` and `src/mcp/resources/` directories - - It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps - - This approach improves startup performance by avoiding synchronous file system scans and enables code-splitting - - Tool code is only loaded when needed, reducing initial memory footprint - -4. **Plugin & Resource Loading (Runtime)** - - At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step - - All workflow loaders are executed at startup to register tools -- If `XCODEBUILDMCP_ENABLED_WORKFLOWS` is set, only those workflows (plus `session-management`) are registered; `workflow-discovery` is only auto-included when `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true` - -5. **Tool Registration** - - Discovered tools automatically registered with server using pre-generated maps - - No manual registration or configuration required +3. **Manifest-Driven Discovery** + - YAML manifests in `manifests/tools/`, `manifests/workflows/`, and `manifests/resources/` define all metadata + - `loadManifest()` reads and validates all YAML files at startup against Zod schemas + - Tool and resource code modules are dynamically imported on demand + +4. **Tool & Resource Loading (Runtime)** + - `registerWorkflowsFromManifest()` selects workflows based on config and predicate context, then dynamically imports tool modules + - `registerResources()` loads resource manifests, filters by predicates, and dynamically imports resource modules + - Both systems share the same `PredicateContext` for visibility filtering + - If `XCODEBUILDMCP_ENABLED_WORKFLOWS` is set, only those workflows (plus `session-management`) are registered; `workflow-discovery` is only auto-included when `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true` + +5. **Tool & Resource Registration** + - Tools are registered via `server.registerTool()` after manifest-driven workflow selection + - Resources are registered via `server.resource()` after manifest-driven predicate filtering - Environment variables control workflow selection behavior 5. **Request Handling** diff --git a/docs/dev/FIXTURE_DESIGNS.md b/docs/dev/FIXTURE_DESIGNS.md new file mode 100644 index 00000000..9397f11a --- /dev/null +++ b/docs/dev/FIXTURE_DESIGNS.md @@ -0,0 +1,877 @@ +# Snapshot test fixture designs + +Target UX for all tool output. This is the TDD reference — write fixtures first, then update rendering code until output matches. + +Delete this file once all fixtures are written and tests pass. + +## Output rhythm (all tools) + +``` + + + : + : + + + +Next steps: +1. +``` + +## Design principles + +1. No JSON output — all tools render structured data as human-readable text +2. Every tool gets a header — emoji + operation name + indented params +3. File paths always relative where possible (rendered by `displayPath`) +4. Grouped/structured body — not raw command dumps. Focus on useful information +5. Concise for AI agents — minimize tokens while maximizing signal +6. Success + error + failure fixtures for every tool where appropriate (error = can't run; failure = ran, bad outcome) +11. Error fixtures must test real executable errors — not just pre-call validation (file-exists checks, param validation). The fixture should exercise the underlying CLI/tool and capture how we handle its error response. Pre-call validation should be handled by yargs or input schemas, not tested as snapshot fixtures. +7. Consistent icons — status emojis owned by renderer, not tools +8. Consistent spacing — one blank line between sections, always +9. No next steps on error paths +10. Tree chars (├/└) for informational lists (paths, IDs, metadata) — not for result lists (errors, failures, test outcomes) + +### Error fixture policy + +Every error fixture must test a **real executable/CLI error** — not pre-call validation (file-exists checks, param validation). The fixture should exercise the underlying tool and capture how we handle its error response. Pre-call validation should be handled by yargs or input schemas, not tested as snapshot fixtures. + +One fixture per distinct CLI or output shape. The representative error fixtures cover all shapes: + +| CLI / Shape | Representative fixture | +|---|---| +| xcodebuild (wrong scheme) | `simulator/build--error-wrong-scheme` | +| simctl terminate (bad bundle) | `simulator/stop--error-no-app` | +| simctl boot (bad UUID) | `simulator-management/boot--error-invalid-id` | +| open (invalid app) | `macos/launch--error-invalid-app` | +| xcrun xccov (invalid bundle) | `coverage/get-coverage-report--error-invalid-bundle` | +| swift build (bad path) | `swift-package/build--error-bad-path` | +| AXe (bad simulator) | `ui-automation/tap--error-no-simulator` | +| Internal: idempotency check | `project-scaffolding/scaffold-ios--error-existing` | +| Internal: no active session | `debugging/continue--error-no-session` | +| Internal: file coverage | `coverage/get-file-coverage--error-invalid-bundle` | + +## Tracking checklist + +### coverage +- [x] `get-coverage-report--success.txt` +- [x] `get-coverage-report--error-invalid-bundle.txt` +- [x] `get-file-coverage--success.txt` +- [x] `get-file-coverage--error-invalid-bundle.txt` +- [ ] Code updated to match fixtures + +### session-management +- [x] `session-set-defaults--success.txt` +- [x] `session-show-defaults--success.txt` +- [x] `session-clear-defaults--success.txt` +- [ ] Code updated to match fixtures + +### simulator-management +- [x] `list--success.txt` +- [x] `boot--error-invalid-id.txt` +- [x] `open--success.txt` +- [x] `set-appearance--success.txt` +- [x] `set-location--success.txt` +- [x] `reset-location--success.txt` +- [ ] Code updated to match fixtures + +### simulator +- [x] `build--success.txt` +- [x] `build--error-wrong-scheme.txt` +- [x] `build--failure-compilation.txt` +- [x] `build-and-run--success.txt` +- [x] `test--success.txt` +- [x] `test--failure.txt` +- [x] `get-app-path--success.txt` +- [x] `list--success.txt` +- [x] `stop--error-no-app.txt` +- [ ] Code updated to match fixtures + +### project-discovery +- [x] `discover-projs--success.txt` +- [x] `list-schemes--success.txt` +- [x] `show-build-settings--success.txt` +- [ ] Code updated to match fixtures + +### project-scaffolding +- [x] `scaffold-ios--success.txt` +- [x] `scaffold-ios--error-existing.txt` +- [x] `scaffold-macos--success.txt` +- [ ] Code updated to match fixtures + +### device +- [x] `build--success.txt` +- [x] `build--failure-compilation.txt` +- [x] `get-app-path--success.txt` +- [x] `list--success.txt` +- [ ] Code updated to match fixtures + +### macos +- [x] `build--success.txt` +- [x] `build--failure-compilation.txt` +- [x] `build-and-run--success.txt` +- [x] `test--success.txt` +- [x] `test--failure.txt` +- [x] `get-app-path--success.txt` +- [x] `launch--error-invalid-app.txt` +- [ ] Code updated to match fixtures + +### swift-package +- [x] `build--success.txt` +- [x] `build--error-bad-path.txt` +- [x] `build--failure-compilation.txt` +- [x] `test--success.txt` +- [x] `test--failure.txt` +- [x] `clean--success.txt` +- [x] `list--success.txt` +- [x] `run--success.txt` +- [ ] Code updated to match fixtures + +### debugging +- [x] `attach--success.txt` +- [x] `add-breakpoint--success.txt` +- [x] `remove-breakpoint--success.txt` +- [x] `continue--success.txt` +- [x] `continue--error-no-session.txt` +- [x] `detach--success.txt` +- [x] `lldb-command--success.txt` +- [x] `stack--success.txt` +- [x] `variables--success.txt` +- [ ] Code updated to match fixtures + +### ui-automation +- [x] `snapshot-ui--success.txt` +- [x] `tap--error-no-simulator.txt` +- [ ] Code updated to match fixtures + +### utilities +- [x] `clean--success.txt` +- [ ] Code updated to match fixtures + +--- + +## Fixture designs by workflow + +### coverage + +**`get-coverage-report--success.txt`**: +``` +📊 Coverage Report + + xcresult: /TestResults.xcresult + Target Filter: CalculatorAppTests + +Overall: 94.9% (354/373 lines) + +Targets: + CalculatorAppTests.xctest — 94.9% (354/373 lines) + +Next steps: +1. View file-level coverage: xcodebuildmcp coverage get-file-coverage --xcresult-path "/TestResults.xcresult" +``` + +**`get-coverage-report--error-invalid-bundle.txt`** — real executable error (fake .xcresult dir passes file-exists check, xcrun xccov fails): +``` +📊 Coverage Report + + xcresult: /invalid.xcresult + +❌ Failed to get coverage report: Failed to load result bundle. + +Hint: Run tests with coverage enabled (e.g., xcodebuild test -enableCodeCoverage YES). +``` + +**`get-file-coverage--success.txt`** — already updated, keep current content. + +**`get-file-coverage--error-invalid-bundle.txt`** — real executable error (fake .xcresult dir passes file-exists check, xcrun xccov fails): +``` +📊 File Coverage + + xcresult: /invalid.xcresult + File: SomeFile.swift + +❌ Failed to get file coverage: Failed to load result bundle. + +Hint: Make sure the xcresult bundle contains coverage data for "SomeFile.swift". +``` + +--- + +### session-management + +**`session-set-defaults--success.txt`**: +``` +⚙️ Set Defaults + + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Scheme: CalculatorApp + +✅ Session defaults updated. +``` + +**`session-show-defaults--success.txt`**: +``` +⚙️ Show Defaults + + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Scheme: CalculatorApp +``` + +**`session-clear-defaults--success.txt`**: +``` +⚙️ Clear Defaults + +✅ Session defaults cleared. +``` + +--- + +### simulator-management + +**`list--success.txt`**: +``` +📱 List Simulators + +iOS 26.2: + iPhone 17 Pro Booted + iPhone 17 Pro Max + iPhone Air + iPhone 17 Booted + iPhone 16e + iPad Pro 13-inch (M5) + iPad Pro 11-inch (M5) + iPad mini (A17 Pro) + iPad (A16) + iPad Air 13-inch (M3) + iPad Air 11-inch (M3) + +watchOS 26.2: + Apple Watch Series 11 (46mm) + Apple Watch Series 11 (42mm) + Apple Watch Ultra 3 (49mm) + Apple Watch SE 3 (44mm) + Apple Watch SE 3 (40mm) + +tvOS 26.2: + Apple TV 4K (3rd generation) + Apple TV 4K (3rd generation) (at 1080p) + Apple TV + +xrOS 26.2: + Apple Vision Pro + +Next steps: +1. Boot simulator: xcodebuildmcp simulator-management boot --simulator-id "UUID_FROM_ABOVE" +2. Open Simulator UI: xcodebuildmcp simulator-management open +3. Build for simulator: xcodebuildmcp simulator build --scheme "YOUR_SCHEME" --simulator-id "UUID_FROM_ABOVE" +4. Get app path: xcodebuildmcp simulator get-app-path --scheme "YOUR_SCHEME" --platform "iOS Simulator" --simulator-id "UUID_FROM_ABOVE" +``` + +Runtime names shortened from `com.apple.CoreSimulator.SimRuntime.iOS-26-2` to `iOS 26.2`. Tabular layout. Booted state shown inline. + +**`boot--error-invalid-id.txt`**: +``` +🔌 Boot Simulator + + Simulator: + +❌ Failed to boot simulator: Invalid device or device pair: + +Next steps: +1. Open Simulator UI: xcodebuildmcp simulator-management open +2. Install app: xcodebuildmcp simulator install --simulator-id "SIMULATOR_UUID" --app-path "PATH_TO_YOUR_APP" +3. Launch app: xcodebuildmcp simulator launch-app --simulator-id "SIMULATOR_UUID" --bundle-id "YOUR_APP_BUNDLE_ID" +``` + +**`open--success.txt`**: +``` +📱 Open Simulator + +✅ Simulator app opened successfully. + +Next steps: +1. Boot simulator: xcodebuildmcp simulator-management boot --simulator-id "UUID_FROM_LIST_SIMS" +2. Start log capture: xcodebuildmcp logging start-simulator-log-capture --simulator-id "UUID" --bundle-id "YOUR_APP_BUNDLE_ID" +3. Launch app with logs: xcodebuildmcp simulator launch-app-with-logs --simulator-id "UUID" --bundle-id "YOUR_APP_BUNDLE_ID" +``` + +**`set-appearance--success.txt`**: +``` +🎨 Set Appearance + + Simulator: + Mode: dark + +✅ Appearance set to dark mode. +``` + +**`set-location--success.txt`**: +``` +📍 Set Location + + Simulator: + Latitude: 37.7749 + Longitude: -122.4194 + +✅ Location set to 37.7749, -122.4194. +``` + +**`reset-location--success.txt`**: +``` +📍 Reset Location + + Simulator: + +✅ Location reset to default. +``` + +--- + +### simulator + +**`build--success.txt`** — pipeline-rendered, review for unified UX consistency. + +**`build--error-wrong-scheme.txt`** — pipeline-rendered, representative pipeline error fixture. + +**`build--failure-compilation.txt`** — build ran but failed with compiler errors (uses CompileError.fixture.swift injected into app target): +``` +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +Errors (1): + ✗ CalculatorApp/CompileError.swift:3: Cannot convert value of type 'String' to specified type 'Int' + +❌ Build failed. (⏱️ ) +``` + +**`build-and-run--success.txt`** — pipeline-rendered, review for consistency. + +**`test--success.txt`** — all tests pass: +``` +🧪 Test + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +Resolved to test(s) + +✅ Test succeeded. (, ⏱️ ) + +Next steps: +1. View test coverage: xcodebuildmcp coverage get-coverage-report --xcresult-path "XCRESULT_PATH" +``` + +**`test--failure.txt`** — tests ran, assertion failures: +``` +🧪 Test + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +Resolved to test(s) + +Failures (1): + ✗ CalculatorAppTests.testCalculatorServiceFailure — XCTAssertEqual failed: ("0") is not equal to ("999") + +❌ Test failed. (, ⏱️ ) +``` + +**`get-app-path--success.txt`**: +``` +🔍 Get App Path + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + + └ App Path: /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app + +Next steps: +1. Get bundle ID: xcodebuildmcp project-discovery get-app-bundle-id --app-path "/Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" +2. Boot simulator: xcodebuildmcp simulator-management boot --simulator-id "" +3. Install on simulator: xcodebuildmcp simulator install --simulator-id "" --app-path "/Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphonesimulator/CalculatorApp.app" +4. Launch on simulator: xcodebuildmcp simulator launch-app --simulator-id "" --bundle-id "BUNDLE_ID" +``` + +**`list--success.txt`** — same as simulator-management/list--success.txt (shared tool). + +**`stop--error-no-app.txt`**: +``` +🛑 Stop App + + Simulator: + Bundle ID: com.nonexistent.app + +❌ Failed to stop app: An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=164): found nothing to terminate +``` + +--- + +### project-discovery + +**`discover-projs--success.txt`**: +``` +🔍 Discover Projects + + Search Path: . + +Workspaces: + example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Projects: + example_projects/iOS_Calculator/CalculatorApp.xcodeproj + +Next steps: +1. Build and run: xcodebuildmcp simulator build-and-run +``` + +**`list-schemes--success.txt`**: +``` +🔍 List Schemes + + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Schemes: + CalculatorApp + CalculatorAppFeature + +Next steps: +1. Build for macOS: xcodebuildmcp macos build --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" --scheme "CalculatorApp" +2. Build and run on simulator: xcodebuildmcp simulator build-and-run --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" --scheme "CalculatorApp" --simulator-name "iPhone 17" +3. Build for simulator: xcodebuildmcp simulator build --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" --scheme "CalculatorApp" --simulator-name "iPhone 17" +4. Show build settings: xcodebuildmcp device show-build-settings --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" --scheme "CalculatorApp" +``` + +**`show-build-settings--success.txt`** — curated summary (full dump behind `--verbose` flag): +``` +🔍 Show Build Settings + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Key Settings: + ├ PRODUCT_NAME: CalculatorApp + ├ PRODUCT_BUNDLE_IDENTIFIER: io.sentry.calculatorapp + ├ SDKROOT: iphoneos + ├ SUPPORTED_PLATFORMS: iphonesimulator iphoneos + ├ ARCHS: arm64 + ├ SWIFT_VERSION: 6.0 + ├ IPHONEOS_DEPLOYMENT_TARGET: 18.0 + ├ CODE_SIGNING_ALLOWED: YES + ├ CODE_SIGN_IDENTITY: Apple Development + ├ CONFIGURATION: Debug + ├ BUILD_DIR: /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products + └ BUILT_PRODUCTS_DIR: /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos + +Next steps: +1. Build for simulator: xcodebuildmcp simulator build --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" --scheme "CalculatorApp" +``` + +--- + +### project-scaffolding + +**`scaffold-ios--success.txt`**: +``` +🏗️ Scaffold iOS Project + + Name: SnapshotTestApp + Path: /ios + Platform: iOS + +✅ Project scaffolded successfully. + +Next steps: +1. Read the README.md in the workspace root directory before working on the project. +2. Build for simulator: xcodebuildmcp simulator build --workspace-path "/ios/SnapshotTestApp.xcworkspace" --scheme "SnapshotTestApp" --simulator-name "iPhone 17" +3. Build and run on simulator: xcodebuildmcp simulator build-and-run --workspace-path "/ios/SnapshotTestApp.xcworkspace" --scheme "SnapshotTestApp" --simulator-name "iPhone 17" +``` + +**`scaffold-ios--error-existing.txt`**: +``` +🏗️ Scaffold iOS Project + + Path: /ios-existing + +❌ Xcode project files already exist in /ios-existing. +``` + +**`scaffold-macos--success.txt`**: +``` +🏗️ Scaffold macOS Project + + Name: SnapshotTestApp + Path: /macos + Platform: macOS + +✅ Project scaffolded successfully. + +Next steps: +1. Build for macOS: xcodebuildmcp macos build --project-path "/macos/SnapshotTestApp.xcodeproj" --scheme "SnapshotTestApp" +2. Build and run on macOS: xcodebuildmcp macos build-and-run --project-path "/macos/SnapshotTestApp.xcodeproj" --scheme "SnapshotTestApp" +``` + +--- + +### device + +**`build--success.txt`** — pipeline-rendered, review for unified UX consistency. + +**`build--failure-compilation.txt`** — build ran but failed with compiler errors: +``` +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + +Errors (1): + ✗ CalculatorApp/CompileError.swift:3: Cannot convert value of type 'String' to specified type 'Int' + +❌ Build failed. (⏱️ ) +``` + +**`get-app-path--success.txt`**: +``` +🔍 Get App Path + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS + + └ App Path: /Library/Developer/Xcode/DerivedData/CalculatorApp-/Build/Products/Debug-iphoneos/CalculatorApp.app + +Next steps: +1. Get bundle ID: xcodebuildmcp project-discovery get-app-bundle-id --app-path "..." +2. Install on device: xcodebuildmcp device install --app-path "..." +3. Launch on device: xcodebuildmcp device launch --bundle-id "BUNDLE_ID" +``` + +**`list--success.txt`**: +``` +📱 List Devices + +✅ Available Devices: + + Cameron's Apple Watch + ├ UDID: + ├ Model: Watch4,2 + ├ Platform: Unknown 10.6.1 + ├ CPU: arm64_32 + └ Developer Mode: disabled + + Cameron's Apple Watch + ├ UDID: + ├ Model: Watch7,20 + ├ Platform: Unknown 26.1 + ├ CPU: arm64e + ├ Connection: localNetwork + └ Developer Mode: disabled + + Cameron's iPhone 16 Pro Max + ├ UDID: + ├ Model: iPhone17,2 + ├ Platform: Unknown 26.3.1 + ├ CPU: arm64e + ├ Connection: localNetwork + └ Developer Mode: enabled + + iPhone + ├ UDID: + ├ Model: iPhone99,11 + ├ Platform: Unknown 26.1 + └ CPU: arm64e + +Next steps: +1. Build for device: xcodebuildmcp device build --scheme "SCHEME" --device-id "DEVICE_UDID" +2. Run tests on device: xcodebuildmcp device test --scheme "SCHEME" --device-id "DEVICE_UDID" +3. Get app path: xcodebuildmcp device get-app-path --scheme "SCHEME" +``` + +--- + +### macos + +**`build--success.txt`** — pipeline-rendered, review for unified UX consistency. + +**`build--failure-compilation.txt`** — build ran but failed with compiler errors: +``` +🔨 Build + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + +Errors (1): + ✗ MCPTest/CompileError.swift:3: Cannot convert value of type 'String' to specified type 'Int' + +❌ Build failed. (⏱️ ) +``` + +**`build-and-run--success.txt`** — pipeline-rendered, review for consistency. + +**`test--success.txt`** — all tests pass (MCPTest has only passing tests). + +**`test--failure.txt`** — tests ran, assertion failures (requires intentional failure in MCPTest): +``` +🧪 Test + + Scheme: MCPTest + Project: example_projects/macOS/MCPTest.xcodeproj + Configuration: Debug + Platform: macOS + +Resolved to test(s) + +Failures (1): + ✗ MCPTestTests.testIntentionalFailure — Expectation failed + +❌ Test failed. (, ⏱️ ) +``` + +**`get-app-path--success.txt`** — same pattern as simulator/device get-app-path. + +**`launch--error-invalid-app.txt`** — real `open` CLI error (fake .app dir passes file-exists, open fails): +``` +🚀 Launch macOS App + + App: /Fake.app + +❌ Launch failed: The application cannot be opened because its executable is missing. +``` + +--- + +### swift-package + +**`build--success.txt`**: +``` +📦 Swift Package Build + + Package: example_projects/SwiftPackage + +✅ Build succeeded. () +``` + +**`build--error-bad-path.txt`** — real swift CLI error (swift build runs and fails on missing path): +``` +📦 Swift Package Build + + Package: example_projects/NONEXISTENT + +❌ Build failed: No such file or directory: example_projects/NONEXISTENT +``` + +**`build--failure-compilation.txt`** — build ran but failed with compiler errors: +``` +📦 Swift Package Build + + Package: example_projects/SwiftPackage + +Errors (1): + ✗ Sources/CompileError.swift:3: Cannot convert value of type 'String' to specified type 'Int' + +❌ Build failed. () +``` + +**`test--success.txt`**: +``` +🧪 Swift Package Test + + Package: example_projects/SwiftPackage + +✅ All tests passed. (5 tests, ) + +Tests: + ✔ Array operations + ✔ Basic math operations + ✔ Basic truth assertions + ✔ Optional handling + ✔ String operations +``` + +**`test--failure.txt`** — tests ran, assertion failures (requires intentional failure in SPM example): +``` +🧪 Swift Package Test + + Package: example_projects/SwiftPackage + +Failures (1): + ✗ IntentionalFailureTests.testShouldFail — #expect failed + +❌ Tests failed. (1 failure, ) +``` + +**`clean--success.txt`**: +``` +🧹 Swift Package Clean + + Package: example_projects/SwiftPackage + +✅ Clean succeeded. Build artifacts removed. +``` + +**`list--success.txt`**: +``` +📦 Swift Package List + +ℹ️ No Swift Package processes currently running. +``` + +**`run--success.txt`**: +``` +📦 Swift Package Run + + Package: example_projects/SwiftPackage + +✅ Executable completed successfully. + +Output: + Hello, world! +``` + +--- + +### debugging + +**`attach--success.txt`** — debugger attached to running simulator process: +``` +🐛 Attach Debugger + + Simulator: + +✅ Attached LLDB to simulator process (). + + ├ Debug Session: + └ Status: Execution resumed after attach. + +Next steps: +1. Add breakpoint: xcodebuildmcp debugging add-breakpoint --file "..." --line 42 +2. View stack trace: xcodebuildmcp debugging stack +3. View variables: xcodebuildmcp debugging variables +``` + +**`add-breakpoint--success.txt`** — breakpoint set at file:line: +``` +🐛 Add Breakpoint + + File: ContentView.swift + Line: 42 + +✅ Breakpoint 1 set. + +Next steps: +1. Continue execution: xcodebuildmcp debugging continue +2. View stack trace: xcodebuildmcp debugging stack +3. View variables: xcodebuildmcp debugging variables +``` + +**`remove-breakpoint--success.txt`**: +``` +🐛 Remove Breakpoint + + Breakpoint: 1 + +✅ Breakpoint 1 removed. +``` + +**`continue--success.txt`**: +``` +🐛 Continue + +✅ Resumed debugger session. + +Next steps: +1. View stack trace: xcodebuildmcp debugging stack +2. View variables: xcodebuildmcp debugging variables +``` + +**`continue--error-no-session.txt`**: +``` +🐛 Continue + +❌ No active debug session. Provide debugSessionId or attach first. +``` + +**`detach--success.txt`**: +``` +🐛 Detach + +✅ Detached debugger session. +``` + +**`lldb-command--success.txt`** — raw LLDB output passed through: +``` +🐛 LLDB Command + + Command: po self + + +``` + +**`stack--success.txt`** — stack trace from paused process: +``` +🐛 Stack Trace + +* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + * frame #0: CalculatorApp`ContentView.body.getter at ContentView.swift:42 + frame #1: SwiftUI`ViewGraph.updateOutputs() + frame #2: SwiftUI`ViewRendererHost.render() +``` + +**`variables--success.txt`** — variable dump from current frame: +``` +🐛 Variables + +(CalculatorService) self = { + ├ display = "0" + ├ expressionDisplay = "" + ├ currentValue = 0 + ├ previousValue = 0 + └ currentOperation = nil +} +``` + +--- + +### ui-automation + +**`snapshot-ui--success.txt`** — accessibility tree with header prepended: +``` +🔍 Snapshot UI + + Simulator: + + +``` + +**`tap--error-no-simulator.txt`**: +``` +👆 Tap + + Simulator: + Position: (100, 100) + +❌ Failed to simulate tap: Simulator with UDID not found. +``` + +--- + +### utilities + +**`clean--success.txt`** — pipeline-rendered, review for unified UX consistency. diff --git a/docs/dev/INVESTIGATION_OUTPUT_FORMATTING_CONSISTENCY.md b/docs/dev/INVESTIGATION_OUTPUT_FORMATTING_CONSISTENCY.md new file mode 100644 index 00000000..f1371702 --- /dev/null +++ b/docs/dev/INVESTIGATION_OUTPUT_FORMATTING_CONSISTENCY.md @@ -0,0 +1,210 @@ +# Investigation: Output Formatting Consistency + +## Summary + +Two follow-up questions answered: (1) The three-renderer architecture was the **original implementation choice from day one** — the plan's "one renderer, two sinks" vision was never attempted because interactive CLI behavior requires state-machine logic incompatible with "dumb pipe" sinks. (2) The 6 holdout tools match their fixtures because they were **actually migrated to the pipeline** in commit `ac33b97f`, then **deliberately reverted** in commit `c0693a1d` (WIP). Fixtures were updated to match the reverted format. The snapshot harness validates final text only, not pipeline provenance. + +## Symptoms + +- Three separate renderers exist instead of the plan's "one renderer, two sinks" +- 6 tool files still manually construct ToolResponse objects +- These holdout tools have passing snapshot fixtures despite bypassing the pipeline + +## Investigation Log + +### Phase 1 — Why Three Renderers Instead of One? + +**Hypothesis:** The implementation started with one renderer and was later split into three. + +**Findings:** **Eliminated.** Git archaeology shows the three-renderer pattern was the original design from the first commit: + +**Commit `1374d3c2` (March 21)** — "Unify pipeline architecture, rendering, and output formatting" +- This is when `mcp-renderer.ts`, `cli-text-renderer.ts`, and `cli-jsonl-renderer.ts` were **first created** (confirmed via `git log --diff-filter=A`) +- The initial `index.ts` already had `resolveRenderers()` returning multiple renderer instances +- At this point, renderers only handled `XcodebuildEvent` types + +**Commit `ac33b97f` (March 25)** — "Migrate all tool output to unified PipelineEvent system" +- Upgraded all three renderers from `XcodebuildEvent` to generic `PipelineEvent` +- Added generic event type handlers (`header`, `status-line`, `section`, `detail-tree`, `table`, `file-ref`) +- The three-renderer architecture was preserved and extended, not questioned + +**Evidence:** The plan document's "one renderer, two sinks" was written as a forward-looking vision. The implementer chose separate renderers from the start because: + +1. **CLI text renderer requires state-machine logic** that can't be a "dumb pipe": + - Tracks `pendingTransientRuntimeLine` for spinner management (`cli-text-renderer.ts:46`) + - Tracks `hasDurableRuntimeContent` for flush decisions (`cli-text-renderer.ts:47`) + - Tracks `lastVisibleEventType` and `lastStatusLineLevel` for compact spacing (`cli-text-renderer.ts:48-49`) + - Uses `createCliProgressReporter()` for Clack spinner integration (`cli-text-renderer.ts:45`) + - Has conditional logic for `interactive` mode (`cli-text-renderer.ts:84-88` for `build-stage`, `cli-text-renderer.ts:93-105` for `status-line`) + +2. **MCP renderer has different semantics**: + - Buffers text parts as strings, returns `ToolResponseContent[]` (`mcp-renderer.ts:36-37, 147-152`) + - Applies session-level `suppressWarnings` (`mcp-renderer.ts:34`) + - Different spacing rules (e.g., `section` uses `\n\n` prefix at `mcp-renderer.ts:68`, CLI uses `writeSection()` which adds `\n`) + +3. **JSONL renderer bypasses text rendering entirely**: + - Serializes raw events as JSON: `process.stdout.write(JSON.stringify(event) + '\n')` (`cli-jsonl-renderer.ts`) + - Making this a "sink" of a text renderer doesn't make architectural sense + +4. **Shared formatting layer already exists**: + - `event-formatting.ts` contains all shared format functions (`formatHeaderEvent`, `formatStatusLineEvent`, `formatSectionEvent`, etc.) + - Both text renderers call the same format functions + - The difference is in orchestration (buffering vs streaming, transient handling, spacing), not in formatting + +**Conclusion:** The three-renderer architecture is a **deliberate pragmatic choice**. The "one renderer, two sinks" model from the plan was aspirational but not viable because CLI interactive behavior (spinners, transient lines, flush timing) requires active event-processing logic. The current design (shared formatters + separate renderers) is functionally equivalent to "shared formatting, runtime-specific orchestration." + +--- + +### Phase 2 — How Do Holdout Tools Match Fixtures? + +**Hypothesis:** The holdout tools were never migrated and their fixtures encode their original manual format. + +**Findings:** **Eliminated.** The holdout tools were actually migrated and then **reverted**. The git history tells the story clearly: + +#### Timeline + +1. **Commit `ac33b97f` (March 25)** — All tools migrated to pipeline: + - `get_sim_app_path.ts` used `toolResponse()`, `header()`, `statusLine()`, `detailTree()` + - `list_devices.ts` used pipeline events for all paths + - All 74 fixtures regenerated with pipeline-formatted output + - Fixture for `get-app-path--success.txt` had 2-space indented params, `detailTree` output, simple "App path resolved" message + +2. **Commit `c0693a1d` (March 28, WIP)** — Selective reversion: + - `get_sim_app_path.ts`: Replaced `toolResponse`/`header`/`statusLine`/`detailTree` with `formatToolPreflight` + manual `content: [{type: 'text'}]` + - `get_device_app_path.ts`: Same reversion pattern + - `get_mac_app_path.ts`: Same reversion pattern + - `list_devices.ts`: Added `renderGroupedDevices()` manual string builder + - `session_show_defaults.ts`: Added emoji to section titles, manual tree connectors + - `screenshot.ts`: Added manual content branches + - **Fixtures simultaneously updated** to match new manual output + +**Evidence from fixture diffs:** + +`simulator/get-app-path--success.txt` changed FROM (pipeline): +``` + Scheme: CalculatorApp (2-space indent, pipeline HeaderEvent) + └ App Path: ... (detailTree event) +✅ App path resolved (statusLine event) +``` + +TO (manual): +``` + Scheme: CalculatorApp (3-space indent, formatToolPreflight) +✅ Get app path successful (⏱️ ) (inline text with emoji) + └ App Path: ... (manual tree connector) +``` + +`device/list--success.txt` changed FROM (pipeline): +``` +🟢 Cameron's Apple Watch (per-device with detailTree) + ├ UDID: ... + ├ Model: Watch4,2 + ├ CPU Architecture: arm64_32 + └ Developer Mode: disabled +``` + +TO (manual): +``` +watchOS Devices: (grouped by platform) + ⌚️ [✓] Cameron's Apple Watch (emoji + availability marker) + OS: 26.3 + UDID: +``` + +#### Why do fixtures still pass? + +The snapshot test harness at `src/snapshot-tests/harness.ts` validates **final text output, not pipeline provenance**: + +1. **CLI path** (`invokeCli`, line 124): Spawns `node CLI_PATH workflow tool --json args`, captures stdout +2. **Direct path** (`invokeDirect`, line 141): Calls handler, extracts `ToolResponse.content` text + +For manual-text tools (not MCP-only, not stateful), the harness uses CLI invocation: +- Tool returns `ToolResponse` with manual `content[].text` +- `printToolResponse()` in `cli/output.ts` checks `isCompletePipelineStream(response)` — **false** for manual tools (no `_meta.pipelineStreamMode`) +- Falls through to `printToolResponseText()` which writes `content[].text` to stdout +- Harness captures stdout, normalizes via `normalize.ts`, compares to fixture via `expect(actual).toBe(expected)` in `fixture-io.ts` + +For pipeline tools: +- CLI text renderer streams formatted output to stdout during execution +- `printToolResponse()` sees `pipelineStreamMode: 'complete'` and **skips printing** (avoids double output) +- Harness captures the already-streamed stdout + +Both paths produce stdout text that gets compared to the fixture. The fixture encodes whatever text was actually produced, regardless of whether it came from the pipeline. + +**Conclusion:** The holdout tools pass their fixtures because the fixtures were updated to match the reverted manual format. The snapshot suite is a **final output contract test**, not a pipeline provenance test. + +--- + +### Phase 3 — Why Were Tools Reverted? + +**Assessment by category:** + +#### `get_sim_app_path`, `get_device_app_path`, `get_mac_app_path` — Expedient compromise + +The pipeline has all the primitives needed for these tools (`HeaderEvent`, `StatusLineEvent`, `DetailTreeEvent`, `NextStepsEvent`). The reverted format is not something the pipeline can't express — it just uses `formatToolPreflight` (3-space indent) instead of pipeline `HeaderEvent` (2-space indent), and inline emoji instead of renderer-owned formatting. + +This reads as a "preserve exact legacy wording/spacing quickly" decision, not a fundamental pipeline limitation. + +#### `list_devices` — Deliberate UX preference + +This tool has a purpose-built `renderGroupedDevices()` function that produces a grouped-by-platform layout with platform-specific emojis (`📱`, `⌚️`, `📺`, `🥽`) and availability markers (`[✓]`/`[✗]`). The pipeline version showed flat per-device `detailTree` output with hardware details (Model, Product Type, CPU Architecture). The grouped format is arguably better UX for scanning. + +That said, the pipeline's `section()` + structured lines could still express this layout. + +#### `swift_package_run`, `swift_package_test` — Defensive escape hatch, not reverted UX + +These are pipeline-first tools. The manual `content` branch is only hit on command failure: +```typescript +const response: ToolResponse = result.success + ? { content: [], isError: false } + : { content: [{ type: 'text', text: result.error || ... }], isError: true }; +``` + +And `errorFallbackPolicy: 'if-no-structured-diagnostics'` in `xcodebuild-output.ts` explicitly suppresses the raw fallback when structured diagnostics exist. The fixtures show pipeline-formatted output. These aren't really "holdouts" — they're pipeline tools with a safety net. + +--- + +## Root Cause + +### Q1: Three renderers +The three-renderer architecture was the **pragmatic original design**, not a deviation from the plan. The plan's "one renderer, two sinks" model doesn't account for the interactive state-machine behavior required by the CLI text renderer (spinners, transient/durable line management, test progress updates). The actual architecture — shared formatting helpers + runtime-specific renderers — achieves the plan's goal of consistent formatting while accommodating runtime differences. + +### Q2: Fixture matching +The holdout tools pass fixtures through a two-step mechanism: +1. Tools were migrated to the pipeline, then reverted to manual text +2. Fixtures were simultaneously updated to encode the manual output format +3. The snapshot harness compares final stdout text, not pipeline provenance + +The reversion was the wrong approach. The fixtures define the target output contract — if the pipeline at migration time couldn't produce the desired format, the pipeline should have been extended (new event types, new formatting functions), not bypassed. The tools were hand-crafted to match the fixtures instead of the pipeline being updated to produce them. + +Designed fixtures exist in `__fixtures_designed__/` that show an earlier target format. The actual `__fixtures__/` files represent the current target. Both should be producible by the pipeline. + +## Recommendations + +### Principle: fixtures define the contract, the pipeline must produce it + +The fixtures in `__fixtures__/` define the correct target output. When the pipeline can't produce a fixture's format, **extend the pipeline** (new event types, new formatting functions) — do not bypass the pipeline with manual text construction. + +### Re-migrate reverted tools by extending the pipeline + +1. **`get_sim_app_path`, `get_device_app_path`, `get_mac_app_path`** — The fixture format (3-space indent header, timing display, success message wording) may require updates to `formatHeaderEvent` or a new formatting variant. Extend the formatting layer to produce the fixture output, then convert tools back to `toolResponse()` with events. + +2. **`list_devices` success path** — The fixture defines a grouped-by-platform layout with platform-specific emojis and `[✓]/[✗]` availability markers. This likely requires new event types or formatting capabilities (e.g., a grouped device list event, or enriching `SectionEvent` with platform/availability metadata). Extend the pipeline to support this, then re-migrate the tool. + +3. **`swift_package_run/test` error fallback** — Route the error fallback through pipeline events instead of raw `content`. The `errorFallbackPolicy` mechanism should remain, but the fallback itself should be event-shaped. + +4. **`session_show_defaults`** — Use `detailTree()` events instead of manual tree connectors. Remove emoji from section titles (renderer should own emoji). + +5. **`screenshot`** — Remove manual content branches. For mixed text + image responses, extend the pipeline if needed. + +### Fix presentation leakage in migrated tools + +6. **`list_sims`** — Remove inline emoji and `✓`/`✗` markers from section content lines. These should come from the formatting layer or event type metadata. + +### Documentation (done) + +7. **`STRUCTURED_XCODEBUILD_EVENTS_PLAN.md`** — Updated: replaced "one renderer, two sinks" with actual "shared formatters + runtime-specific renderers" architecture. Checked off completed items. Documented remaining work with correct framing. + +### Prevent future drift + +8. **Consider a lint/test guard** — Add a check that tool files under `src/mcp/tools/` don't directly construct `content: [{ type: 'text' }]` objects. This would catch future regressions where tools bypass the pipeline. diff --git a/docs/dev/MANIFEST_FORMAT.md b/docs/dev/MANIFEST_FORMAT.md index 578a6a80..6a08b9c9 100644 --- a/docs/dev/MANIFEST_FORMAT.md +++ b/docs/dev/MANIFEST_FORMAT.md @@ -12,9 +12,13 @@ manifests/ │ ├── build_sim.yaml │ ├── list_sims.yaml │ └── ... -└── workflows/ # Workflow manifest files - ├── simulator.yaml - ├── device.yaml +├── workflows/ # Workflow manifest files +│ ├── simulator.yaml +│ ├── device.yaml +│ └── ... +└── resources/ # Resource manifest files + ├── simulators.yaml + ├── devices.yaml └── ... ``` @@ -247,9 +251,7 @@ At runtime, this resolves to: build/mcp/tools//.js ``` -The module must export either: -1. **Named exports** (preferred): `{ schema, handler }` -2. **Legacy default export**: `export default { schema, handler }` +The module must export named exports: `{ schema, handler }` Note: `name`, `description`, and `annotations` are defined in the YAML manifest, not the module. @@ -433,10 +435,83 @@ At startup, tools are registered dynamically from manifests: Key files: - `src/core/manifest/load-manifest.ts` - Manifest loading and caching -- `src/core/manifest/import-tool-module.ts` - Dynamic module imports +- `src/core/manifest/import-tool-module.ts` - Dynamic tool module imports +- `src/core/manifest/import-resource-module.ts` - Dynamic resource module imports - `src/utils/tool-registry.ts` - MCP server tool registration +- `src/core/resources.ts` - MCP server resource registration - `src/runtime/tool-catalog.ts` - CLI/daemon tool catalog building -- `src/visibility/exposure.ts` - Workflow/tool visibility filtering +- `src/visibility/exposure.ts` - Workflow/tool/resource visibility filtering + +## Resource Manifest Format + +Resource manifests define MCP resources exposed by the server. + +### Schema + +```yaml +# Required fields +id: string # Unique resource identifier (must match filename without .yaml) +module: string # Module path (see Module Path section) +name: string # MCP resource name +uri: string # Resource URI (e.g., xcodebuildmcp://simulators) +description: string # Resource description +mimeType: string # MIME type for the resource content + +# Optional fields +availability: # Per-runtime availability flags + mcp: boolean # Available via MCP server (default: true) +predicates: string[] # Predicate names for visibility filtering (default: []) +``` + +### Example: Basic Resource + +```yaml +id: simulators +module: mcp/resources/simulators +name: simulators +uri: xcodebuildmcp://simulators +description: Available iOS simulators with their UUIDs and states +mimeType: text/plain +``` + +### Example: Predicate-Gated Resource + +```yaml +id: xcode-ide-state +module: mcp/resources/xcode-ide-state +name: xcode-ide-state +uri: xcodebuildmcp://xcode-ide-state +description: "Current Xcode IDE selection (scheme and simulator) from Xcode's UI state" +mimeType: application/json +predicates: + - runningUnderXcodeAgent # Only exposed when running under Xcode +``` + +### Resource Fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `id` | string | Yes | - | Unique identifier, must match filename | +| `module` | string | Yes | - | Module path relative to `src/` (extensionless) | +| `name` | string | Yes | - | MCP resource name | +| `uri` | string | Yes | - | Resource URI | +| `description` | string | Yes | - | Resource description | +| `mimeType` | string | Yes | - | Content MIME type | +| `availability.mcp` | boolean | No | `true` | Available via MCP | +| `predicates` | string[] | No | `[]` | Visibility predicates (all must pass) | + +### Resource Module Contract + +Resource modules must export a named `handler` function: + +```typescript +// src/mcp/resources/simulators.ts +export async function handler(uri: URL): Promise<{ contents: Array<{ text: string }> }> { + // Implementation +} +``` + +Metadata (name, description, URI, mimeType) is defined in the YAML manifest, not the module. ## Creating a New Tool diff --git a/docs/dev/QUERY_TOOL_FORMAT_SPEC.md b/docs/dev/QUERY_TOOL_FORMAT_SPEC.md new file mode 100644 index 00000000..2f60e18a --- /dev/null +++ b/docs/dev/QUERY_TOOL_FORMAT_SPEC.md @@ -0,0 +1,123 @@ +# Query Tool Formatting Spec + +## Goal + +Make all xcodebuild query tools (list-schemes, show-build-settings, get-app-path variants) use the same visual UX as pipeline-backed build/test tools: front matter, structured errors, clean results, manifest-driven next steps. + +These tools do NOT need the full streaming pipeline (no parser, no run-state, no renderers). They run a single short-lived xcodebuild command and return a result. But they must share the same visual language. + +## Target output format + +### Happy path + +``` +🔍 List Schemes + + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Schemes: + - CalculatorApp + - CalculatorAppFeature + +Next steps: +1. Build for simulator: xcodebuildmcp simulator build ... +``` + +``` +🔍 Show Build Settings + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + + + +Next steps: +1. Build for macOS: ... +``` + +``` +🔍 Get App Path + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS Simulator + Simulator: iPhone 17 + + └ App Path: /path/to/CalculatorApp.app + +Next steps: +1. Get bundle ID: ... +``` + +### Sad path + +``` +🔍 Get App Path + + Scheme: NONEXISTENT + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Platform: iOS Simulator + +Errors (1): + + ✗ The workspace named "CalculatorApp" does not contain a scheme named "NONEXISTENT". + +❌ Query failed. +``` + +No raw xcodebuild noise (timestamps, PIDs, result bundle paths). No next steps on failure. + +## Implementation approach + +### Shared helper: `formatQueryPreflight` + +Extend `formatToolPreflight` in `src/utils/build-preflight.ts` to support query operations. Add operation types: `'List Schemes'`, `'Show Build Settings'`, `'Get App Path'`. Make `configuration` and `platform` optional (query tools may not have them). + +Use emoji `🔍` (U+1F50D) for all query operations. + +### Shared helper: `parseXcodebuildError` + +Create a small utility to extract clean error messages from raw xcodebuild stderr/output. Strip: +- Timestamp lines (`2026-03-21 13:42:...`) +- Result bundle lines (`Writing error result bundle to ...`) +- PID noise + +Keep only `xcodebuild: error: ` lines, cleaned to just ``. + +### Error formatting + +Use the same `Errors (N):` grouped block format with `✗` prefix. Reuse `formatGroupedCompilerErrors` or a lightweight equivalent. + +### Result formatting + +- `list_schemes`: List schemes as ` - SchemeName` lines under a `Schemes:` heading +- `show_build_settings`: Raw build settings output (already structured) +- `get_*_app_path`: Use the tree format (`└ App Path: /path/to/app`) matching the build-run-result footer + +### Next steps + +Continue using `nextStepParams` and let `postProcessToolResponse` resolve manifest templates. No change needed. + +### Error response + +On failure, return `isError: true` with no next steps (consistent with pipeline tools). + +## Tools to migrate + +1. `src/mcp/tools/project-discovery/list_schemes.ts` +2. `src/mcp/tools/project-discovery/show_build_settings.ts` +3. `src/mcp/tools/simulator/get_sim_app_path.ts` +4. `src/mcp/tools/macos/get_mac_app_path.ts` +5. `src/mcp/tools/device/get_device_app_path.ts` + +## Rules + +- No full pipeline (no startBuildPipeline, no createPendingXcodebuildResponse) +- Use formatToolPreflight (extended) for front matter +- Parse xcodebuild errors cleanly +- Strip raw xcodebuild noise from error output +- Use `✗` grouped error block for failures +- Use `❌ Query failed.` as the failure summary (not tool-specific messages) +- Next steps only on success +- Update existing tests to match new output format +- All tests must pass, no regressions diff --git a/docs/dev/RENDERING_PIPELINE.md b/docs/dev/RENDERING_PIPELINE.md new file mode 100644 index 00000000..de427272 --- /dev/null +++ b/docs/dev/RENDERING_PIPELINE.md @@ -0,0 +1,280 @@ +# Rendering Pipeline + +All tool output flows through a unified event-based rendering pipeline. Tools produce `PipelineEvent` objects. Renderers consume events and produce output appropriate for the active runtime (CLI text, CLI JSON, MCP). + +## Core Principle + +```mermaid +flowchart LR + T[Tool Handler] -->|PipelineEvent array| R[Renderers] + R -->|text| STDOUT[stdout] + R -->|json| STDOUT + R -->|content| MCP[ToolResponse.content] +``` + +Every piece of output — headers, status lines, detail trees, summaries, next-steps — is a pipeline event. Renderers are the **only** mechanism that produces output. There is no direct content mutation, text extraction, or replay. + +## Renderers + +Three renderers exist. Which are active depends on the runtime environment: + +| Renderer | Purpose | Writes to stdout? | +|----------|---------|-------------------| +| **MCP** | Accumulates formatted text into `ToolResponse.content` | No | +| **CLI Text** | Writes formatted, colored text to stdout in real-time | Yes | +| **CLI JSONL** | Writes one JSON object per event per line to stdout | Yes | + +### Renderer Activation + +Determined by `resolveRenderers()` in `src/utils/renderers/index.ts` based on environment variables set during bootstrap: + +```mermaid +flowchart TD + R[resolveRenderers] --> MCP[MCP Renderer
always created] + R --> CHECK{RUNTIME == cli
AND VERBOSE != 1?} + CHECK -->|no| DONE[Return MCP only] + CHECK -->|yes| FMT{OUTPUT_FORMAT?} + FMT -->|json| JSONL[+ CLI JSONL Renderer] + FMT -->|text| TEXT[+ CLI Text Renderer] +``` + +| Context | `XCODEBUILDMCP_RUNTIME` | `..._CLI_OUTPUT_FORMAT` | Active Renderers | +|---------|------------------------|------------------------|------------------| +| MCP server | `mcp` | — | MCP only | +| CLI `--output text` | `cli` | `text` | MCP + CLI Text | +| CLI `--output json` | `cli` | `json` | MCP + CLI JSONL | +| Daemon (internal) | `daemon` | — | MCP only | +| Verbose / test | any | any + `VERBOSE=1` | MCP only | + +The MCP renderer is **always** active. CLI renderers are additive. + +## Pipeline Flows + +### Flow 1: Non-Xcodebuild Tools (Immediate) + +Most tools (simulator management, project discovery, coverage, UI automation, etc.) produce all their events at once and return immediately. No real-time streaming. + +```mermaid +sequenceDiagram + participant TH as Tool Handler + participant TR as toolResponse() + participant CLR as CLI Renderer + participant MCR as MCP Renderer + participant PP as postProcessToolResponse() + participant PR as printToolResponse() + + TH->>TR: events[] + loop Each event + TR->>CLR: onEvent(event) + CLR->>CLR: write to stdout + TR->>MCR: onEvent(event) + MCR->>MCR: accumulate text + end + TR->>CLR: finalize() + TR->>MCR: finalize() + TR-->>TH: ToolResponse
{content, _meta.events, streamedCounts} + + TH-->>PP: response (with nextStepParams) + PP->>PP: resolve next-steps from manifest templates + PP->>PP: create NextStepsEvent + + Note over PP,MCR: emitNextStepsEvent() — same renderer types + PP->>CLR: onEvent(next-steps) + CLR->>CLR: write to stdout + PP->>MCR: onEvent(next-steps) + MCR->>MCR: accumulate text + PP->>CLR: finalize() + PP->>MCR: finalize() + PP->>PP: append MCP content + event to response + + PP-->>PR: final ToolResponse + PR->>PR: emit any remaining delta
(usually nothing) +``` + +### Flow 2: Xcodebuild Tools (Streaming) + +Build, test, and build-and-run tools use a long-lived pipeline that streams events in real-time as xcodebuild produces output. The pipeline stays open during execution and is finalized after the build completes. + +#### Build Execution Phase + +```mermaid +sequenceDiagram + participant XC as xcodebuild process + participant P as Event Parser + participant RS as RunState + participant CLR as CLI Renderer + participant MCR as MCP Renderer + + Note over XC,MCR: startBuildPipeline() creates pipeline with renderers + + loop stdout/stderr chunks in real-time + XC->>P: raw output + P->>RS: parsed PipelineEvent + RS->>CLR: onEvent(event) + CLR->>CLR: write to stdout
(progress, stages, errors) + RS->>MCR: onEvent(event) + MCR->>MCR: accumulate text + end + + Note over XC,MCR: xcodebuild exits — pipeline stays open +``` + +#### Finalization Phase + +```mermaid +sequenceDiagram + participant PP as postProcessToolResponse() + participant FP as finalizePendingXcodebuildResponse() + participant RS as RunState + participant CLR as CLI Renderer + participant MCR as MCP Renderer + participant PR as printToolResponse() + + PP->>FP: isPendingXcodebuild? yes + FP->>FP: create next-steps event + FP->>RS: finalize(tailEvents incl. next-steps) + + RS->>CLR: onEvent(summary) + CLR->>CLR: write summary to stdout + RS->>MCR: onEvent(summary) + + RS->>CLR: onEvent(detail-tree) + CLR->>CLR: write details to stdout + RS->>MCR: onEvent(detail-tree) + + RS->>CLR: onEvent(next-steps) + CLR->>CLR: write next-steps to stdout + RS->>MCR: onEvent(next-steps) + + FP->>CLR: finalize() + FP->>MCR: finalize() + FP-->>PP: ToolResponse
{mcpContent, events, streamedCounts} + + PP-->>PR: final ToolResponse + PR->>PR: emit any remaining delta
(usually nothing) +``` + +Key difference from immediate tools: the pipeline owns the renderer lifecycle. Events stream through renderers during execution. Next-steps are injected as tail events **before** finalization, so they flow through the same renderers in the same pass. + +### Flow 3: MCP Server Mode + +In MCP mode, only the MCP renderer is active. No CLI output. + +```mermaid +sequenceDiagram + participant AI as AI Model (MCP Client) + participant S as MCP Server + participant TH as Tool Handler + participant MCR as MCP Renderer + + AI->>S: call_tool request + S->>TH: handler(args) + TH->>MCR: events via toolResponse() + MCR->>MCR: accumulate text + TH-->>S: ToolResponse + S->>S: postProcessToolResponse()
emitNextStepsEvent() -> MCP renderer + S-->>AI: ToolResponse.content
(formatted text) +``` + +Next-steps go through `emitNextStepsEvent()` which creates a fresh MCP renderer, formats the event, and appends the content. The MCP renderer uses function-call format for next-steps (e.g., `install_app_sim({ simulatorId: "..." })`). + +### Flow 4: Daemon Mode + +Stateful tools run on a background daemon process. The daemon uses MCP-only rendering (no CLI output). The response travels over a Unix socket to the CLI process, which handles CLI output. + +```mermaid +sequenceDiagram + participant CLI as CLI Process + participant D as Daemon Process + participant MCR as MCP Renderer + participant PR as printToolResponse() + + CLI->>D: RPC request (Unix socket) + D->>D: tool.handler(args) + D->>MCR: events via toolResponse()
(MCP renderer only, no CLI) + D->>D: postProcessToolResponse()
(next-steps resolved) + D-->>CLI: ToolResponse (over socket) + + CLI->>CLI: postProcessToolResponse()
(applyTemplateNextSteps: false) + CLI->>PR: printToolResponse() + PR->>PR: print content to stdout +``` + +### Flow 5: CLI JSON Mode + +Events are emitted as JSONL (one JSON object per line) in real-time. + +```mermaid +sequenceDiagram + participant TH as Tool Handler + participant JR as CLI JSONL Renderer + participant MCR as MCP Renderer + + loop Each event from tool or xcodebuild + TH->>JR: onEvent(event) + JR->>JR: stdout.write(JSON.stringify(event) + newline) + TH->>MCR: onEvent(event) + end + + Note over TH,MCR: Next-steps appended via emitNextStepsEvent() + TH->>JR: onEvent(next-steps) + JR->>JR: stdout.write(JSON.stringify(nextStepsEvent) + newline) +``` + +Output example: +```jsonl +{"type":"header","timestamp":"...","operation":"Build","params":[...]} +{"type":"build-stage","timestamp":"...","stage":"COMPILING"} +{"type":"summary","timestamp":"...","status":"SUCCEEDED","durationMs":5200} +{"type":"next-steps","timestamp":"...","steps":[{"tool":"launch_app_sim","params":{...}}]} +``` + +## Event Types + +All events implement `PipelineEvent` (see `src/types/pipeline-events.ts`): + +| Event Type | Purpose | Example | +|-----------|---------|---------| +| `header` | Operation banner with params | "Build", scheme, workspace, derived data | +| `build-stage` | Build progress phase | Resolving packages, Compiling, Linking | +| `status-line` | Success/error/warning/info status | "Build succeeded", "App launched" | +| `section` | Titled block with detail lines | Failed test output, captured output | +| `detail-tree` | Key-value tree with branch characters | App path, bundle ID, process ID | +| `table` | Columnar data | Simulator list, device list | +| `file-ref` | File path reference | Build log path, debug log | +| `compiler-error` | Compiler diagnostic | Error message with file location | +| `compiler-warning` | Compiler warning | Warning message with file location | +| `test-failure` | Test failure diagnostic | Test name, assertion, location | +| `test-discovery` | Discovered test list | Test names, count | +| `test-progress` | Running test counts | Completed, failed, total | +| `summary` | Final operation summary | Succeeded/failed, duration, test counts | +| `next-steps` | Suggested follow-up actions | Tool names with params | + +## Key Files + +| File | Responsibility | +|------|---------------| +| `src/utils/tool-response.ts` | `toolResponse()` — streams events through renderers, returns response | +| `src/utils/renderers/index.ts` | `resolveRenderers()` — decides which renderers are active | +| `src/utils/renderers/mcp-renderer.ts` | Accumulates event text into `ToolResponse.content` | +| `src/utils/renderers/cli-text-renderer.ts` | Writes formatted text to stdout (supports interactive progress) | +| `src/utils/renderers/cli-jsonl-renderer.ts` | Writes JSON events to stdout | +| `src/utils/renderers/event-formatting.ts` | Canonical formatters for each event type | +| `src/utils/xcodebuild-pipeline.ts` | Long-lived pipeline for streaming builds | +| `src/utils/xcodebuild-output.ts` | Pending response creation and finalization | +| `src/utils/xcodebuild-run-state.ts` | Event ordering, deduplication, summary generation | +| `src/runtime/tool-invoker.ts` | Post-processing: next-steps resolution, `emitNextStepsEvent()` | +| `src/cli/output.ts` | `printToolResponse()` — prints remaining delta after renderers | +| `src/utils/tool-event-builders.ts` | Factory functions for creating event objects | + +## Design Rules + +1. **Events are the model.** All output is represented as `PipelineEvent` objects. Renderers are the only mechanism that turns events into text/JSON. + +2. **Renderers produce output.** No direct `process.stdout.write()` outside renderers. No text content mutation after rendering. + +3. **One pass per event.** Each event goes through renderers exactly once. No replay, no extraction, no re-rendering. + +4. **Next-steps are events.** A `next-steps` event is treated identically to any other event — it flows through renderers which format it according to their strategy. + +5. **`printToolResponse()` handles the delta.** After renderers have written streamed output, `printToolResponse()` only prints content items or events that were appended after the initial streaming pass (tracked by `streamedEventCount` / `streamedContentCount`). diff --git a/docs/dev/RENDERING_PIPELINE_REFACTOR.md b/docs/dev/RENDERING_PIPELINE_REFACTOR.md new file mode 100644 index 00000000..fb706daf --- /dev/null +++ b/docs/dev/RENDERING_PIPELINE_REFACTOR.md @@ -0,0 +1,382 @@ +# Rendering Pipeline Refactor Plan + +## Goal + +``` +events -> render(events, strategy) -> text -> output(target) +``` + +Three steps. Two render strategies (text, json). Two output targets (stdout, ToolResponse envelope). No special cases for next-steps. No `_meta` coordination. No replay. + +## Principles + +1. **Two render strategies**: text (human-readable) and json (JSONL). That's it. +2. **Rendering is data in, text out.** A renderer takes events and produces strings. It doesn't know about stdout or ToolResponse. +3. **Output target is post-render.** After rendering produces text, the caller decides: write to stdout (CLI) or wrap in ToolResponse (MCP). +4. **Streaming is incremental rendering.** Same renderer, called event-by-event instead of all at once. The sink receives chunks progressively. +5. **Daemon is lifecycle, not rendering.** Daemon keeps a process alive for stateful tools. It sends events over the wire. The CLI renders them locally. +6. **ToolResponse is MCP transport only.** Internal code never constructs, inspects, or mutates ToolResponse. It's built once at the MCP boundary. +7. **Next-steps are events.** They flow through the renderer like any other event. No second render pass. + +## Current State (problems) + +- Three "renderers" (MCP, CLI Text, CLI JSONL) when there should be two strategies +- `mcp-renderer.ts` and `cli-text-renderer.ts` use the same formatters from `event-formatting.ts` — they're the same strategy with different sinks +- `toolResponse()` renders AND constructs ToolResponse — mixing rendering with transport +- `emitNextStepsEvent()` creates a second set of renderers for next-steps +- `printToolResponse()` inspects `_meta`, calculates deltas, replays leftover output +- `resolveRenderers()` always creates MCP renderer even in CLI mode +- `ToolResponse` used as internal data structure throughout invoker, daemon, CLI +- `_meta` used as undocumented coordination channel (events, streamed counts, pending state) + +## Design + +### Internal result type + +Tools return events. Not ToolResponse. + +```typescript +// src/types/tool-result.ts + +interface ToolResult { + events: PipelineEvent[]; + isError?: boolean; + attachments?: ToolResponseContent[]; // non-event content (images only) + nextSteps?: NextStep[]; + nextStepParams?: NextStepParamsMap; +} + +interface PendingBuildResult { + kind: 'pending-build'; + started: StartedPipeline; + isError?: boolean; + emitSummary: boolean; + tailEvents: PipelineEvent[]; + fallbackContent: ToolResponseContent[]; + errorFallbackPolicy: 'always' | 'if-no-structured-diagnostics'; + includeBuildLogFileRef: boolean; + includeParserDebugFileRef: boolean; + meta?: Record; +} + +type ToolExecutionResult = ToolResult | PendingBuildResult; +``` + +`ToolResponse` stays in `src/types/common.ts` as the MCP SDK type. Internal code stops using it. + +### Render function + +Pure function. Events in, text out. + +```typescript +// src/rendering/render.ts + +type RenderStrategy = 'text' | 'json'; + +// Batch render — all events at once, returns complete output +function renderEvents(events: PipelineEvent[], strategy: RenderStrategy): string; + +// Incremental render — for streaming. Returns a session. +interface RenderSession { + push(event: PipelineEvent): string; // returns rendered text for this event + finalize(): string; // returns any buffered text (grouped diagnostics, summary) +} + +function createRenderSession(strategy: RenderStrategy): RenderSession; +``` + +**Text strategy**: reuses all existing formatters from `event-formatting.ts`. Handles diagnostic grouping, summary generation, transient/durable distinction. The `push()` return value is the rendered text for that event (may be empty for grouped events like compiler-error that are deferred until summary). + +**JSON strategy**: `push()` returns `JSON.stringify(event) + '\n'`. `finalize()` returns `''`. + +### Sink (output target) + +The caller decides what to do with the rendered text. This is not a class or interface — it's just what the boundary code does. + +**CLI text mode:** +```typescript +const session = createRenderSession('text'); +for (const event of result.events) { + const text = session.push(event); + if (text) process.stdout.write(formatCliTextLine(text) + '\n'); +} +const final = session.finalize(); +if (final) process.stdout.write(final); +``` + +**CLI json mode:** +```typescript +const session = createRenderSession('json'); +for (const event of result.events) { + process.stdout.write(session.push(event)); +} +``` + +**MCP boundary:** +```typescript +const text = renderEvents(result.events, 'text'); +const response: ToolResponse = { + content: [ + { type: 'text', text }, + ...(result.attachments ?? []), + ], + isError: result.isError || undefined, +}; +``` + +**Streaming (xcodebuild CLI):** +```typescript +const session = createRenderSession('text'); +// During build execution, pipeline calls emitEvent for each parsed event: +function emitEvent(event: PipelineEvent) { + const text = session.push(event); + if (text) process.stdout.write(formatCliTextLine(text) + '\n'); +} +// After build completes and next-steps resolved: +emitEvent(nextStepsEvent); +const final = session.finalize(); +if (final) process.stdout.write(final); +``` + +### Interactive progress (CLI text) + +The CLI text renderer currently has spinner/transient line behavior for build stages and test progress. This stays in the text strategy but the `push()` return distinguishes durable vs transient text: + +```typescript +interface TextRenderOp { + text: string; + transient?: boolean; // true = progress line that can be overwritten +} +``` + +The CLI stdout sink handles transient lines using the existing `CliProgressReporter`. The MCP sink ignores transient ops. This is the only place where the sink needs to know more than "here's a string". + +### Tool handler changes + +`toolResponse()` becomes a pure data constructor: + +```typescript +// src/utils/tool-response.ts +function toolResponse(events: PipelineEvent[], options?): ToolResult { + return { + events, + isError: detectError(events) || undefined, + nextStepParams: options?.nextStepParams, + }; +} +``` + +No rendering. No resolveRenderers(). No _meta. + +Handler signature in `src/runtime/types.ts`: +```typescript +handler: (params: Record) => Promise; +``` + +### Invoker flow + +```typescript +// src/runtime/tool-invoker.ts — simplified executeTool + +async executeTool(tool, args, opts): Promise { + const result = await tool.handler(args); + return finalizeResult(tool, result, this.catalog); +} +``` + +`finalizeResult()` replaces `postProcessToolResponse()`: +1. If pending build: finalize pipeline, get events +2. Resolve next-steps from manifest templates (existing logic, unchanged) +3. Push next-steps event to events array +4. Strip nextSteps/nextStepParams +5. Return `ToolResult` + +No rendering. No emitNextStepsEvent(). No second renderer pass. + +### CLI entry point + +```typescript +// src/cli/register-tool-commands.ts — simplified + +const strategy = outputFormat === 'json' ? 'json' : 'text'; +const session = createRenderSession(strategy); + +// For streaming tools, pass emitEvent into the invocation +const emitEvent = (event: PipelineEvent) => { + const rendered = session.push(event); + if (rendered) writeToStdout(rendered, strategy); +}; + +const result = await invoker.invokeDirect(tool, args, { + runtime: 'cli', + emitEvent, // xcodebuild pipeline uses this for live streaming +}); + +// Finalize (flushes grouped diagnostics, summary) +const finalText = session.finalize(); +if (finalText) writeToStdout(finalText, strategy); + +// Print non-event attachments (images) +printAttachments(result.attachments); + +if (result.isError) process.exitCode = 1; +``` + +`printToolResponse()` is deleted. Its job is done by the boundary code above. + +### MCP entry point + +```typescript +// src/utils/tool-registry.ts — simplified + +const result = await invoker.invoke(toolName, args, { runtime: 'mcp' }); +const text = renderEvents(result.events, 'text'); +return { + content: [ + { type: 'text', text }, + ...(result.attachments ?? []), + ], + isError: result.isError || undefined, +}; +``` + +### Daemon flow + +Daemon doesn't render. It runs the tool, collects events, sends them to CLI. + +**Daemon server:** +```typescript +const result = await invoker.invoke(toolName, args, { runtime: 'daemon' }); +return { events: result.events, attachments: result.attachments, isError: result.isError }; +``` + +**CLI after daemon response:** +```typescript +// Received events from daemon — render them locally +const session = createRenderSession(strategy); +for (const event of daemonResult.events) { + const text = session.push(event); + if (text) writeToStdout(text, strategy); +} +const final = session.finalize(); +if (final) writeToStdout(final, strategy); +printAttachments(daemonResult.attachments); +``` + +Same rendering code path as direct CLI invocation. Daemon is just transport. + +### Xcodebuild streaming + +The pipeline stops owning renderers. It accepts an `emitEvent` callback. + +```typescript +// src/utils/xcodebuild-pipeline.ts — key change + +interface PipelineOptions { + operation: XcodebuildOperation; + toolName: string; + params: Record; + minimumStage?: XcodebuildStage; + emitEvent?: (event: PipelineEvent) => void; // NEW: live event sink +} +``` + +When `emitEvent` is provided (CLI direct), events stream to stdout in real-time through the render session. When not provided (MCP, daemon), events are buffered and rendered after completion. + +Pipeline finalization returns events only: +```typescript +interface PipelineResult { + state: XcodebuildRunState; + events: PipelineEvent[]; +} +``` + +No `mcpContent`. No renderer finalization. The caller renders. + +### Next-steps format + +One canonical text format. No CLI-vs-MCP branching. + +Current MCP format is the canonical one: +``` +Next steps: +1. launch_app_sim({ simulatorId: "ABC-123", bundleId: "com.example.app" }) +2. stop_app_sim({ simulatorId: "ABC-123" }) +``` + +CLI command format (`xcodebuildmcp simulator launch-app-sim --simulator-id "..."`) becomes a presentation concern in the CLI sink layer if desired, not a rendering concern. Initially, use the canonical format everywhere. + +## What Gets Deleted + +| File/Function | Reason | +|---------------|--------| +| `src/utils/renderers/mcp-renderer.ts` | Replaced by text strategy + MCP boundary wrapping | +| `src/utils/renderers/cli-text-renderer.ts` | Replaced by text strategy + CLI stdout writing | +| `src/utils/renderers/cli-jsonl-renderer.ts` | Replaced by json strategy + CLI stdout writing | +| `src/utils/renderers/index.ts` (`resolveRenderers`) | No longer needed — strategy selected at boundary | +| `emitNextStepsEvent()` in tool-invoker.ts | Next-steps pushed to events before render | +| `printToolResponse()` complex logic | Boundary code handles output directly | +| `_meta.events`, `_meta.streamedEventCount`, `_meta.streamedContentCount` | No coordination channel needed | +| `_meta.pendingXcodebuild` | Typed `PendingBuildResult` instead | +| `suppressCliStream` option | No CLI rendering in toolResponse() to suppress | + +## What Stays + +| Component | Why | +|-----------|-----| +| `event-formatting.ts` | Pure formatters, shared by text strategy | +| `PipelineEvent` types | The event model is correct | +| `tool-event-builders.ts` | Event factory functions | +| `xcodebuild-event-parser.ts` | Parsing is not a rendering concern | +| `xcodebuild-run-state.ts` | Event ordering/dedup is not a rendering concern | +| `CliProgressReporter` | Interactive progress stays as a CLI sink concern | +| `terminal-output.ts` | CLI text coloring stays as a CLI sink concern | +| Next-step template resolution logic | Business logic, unchanged | + +## New Files + +| File | Purpose | +|------|---------| +| `src/types/tool-result.ts` | `ToolResult`, `PendingBuildResult`, `ToolExecutionResult` | +| `src/rendering/render.ts` | `renderEvents()`, `createRenderSession()`, `RenderSession` | + +Two new files. That's it. + +## Migration Order + +1. **Add `ToolResult` type** — additive, no existing code changes +2. **Add `renderEvents()` and `createRenderSession()`** — extract text strategy from existing `cli-text-renderer.ts` and `mcp-renderer.ts` (they use the same formatters). Add json strategy. Independently testable. +3. **Change `toolResponse()` to return `ToolResult`** — stop rendering, just store events. Update all call sites (mechanical type change). +4. **Change handler contract** to `Promise` in `types.ts` and `typed-tool-factory.ts`. Update tool modules. +5. **Replace `postProcessToolResponse` with `finalizeResult`** — push next-steps to events. Delete `emitNextStepsEvent()`. +6. **Refactor xcodebuild pipeline** — remove renderer ownership, accept `emitEvent` callback, return events only. Update pending result helpers. Update build/test tools. +7. **Switch CLI boundary** — create render session, pass `emitEvent`, delete `printToolResponse()` complex logic. +8. **Switch MCP boundary** — render at boundary, construct ToolResponse. +9. **Switch daemon protocol** — send events over wire, render locally on CLI. Bump protocol version. +10. **Delete old renderers** — `mcp-renderer.ts`, `cli-text-renderer.ts`, `cli-jsonl-renderer.ts`, `resolveRenderers()`. +11. **Update docs and tests.** + +This should land as one atomic branch. Mixed old/new paths recreate the complexity. + +## Daemon Protocol + +Bump `DAEMON_PROTOCOL_VERSION` to 2. Wire payload changes from: +```typescript +{ response: ToolResponse } +``` +to: +```typescript +{ events: PipelineEvent[], attachments?: ToolResponseContent[], isError?: boolean } +``` + +Old CLI + new daemon (or vice versa) fails fast with a restart instruction. + +## Risk + +- ~50 `toolResponse()` call sites need type changes (mechanical) +- Handler contract change touches `types.ts`, `typed-tool-factory.ts`, `tool-registry.ts`, all tool modules +- Daemon protocol bump requires atomic client+server update +- Next-steps text format change is user-visible +- Test churn is significant + +All of this is bounded and mechanical. The event model, parsing, formatting, and business logic are unchanged. diff --git a/docs/dev/STRUCTURED_XCODEBUILD_EVENTS_PLAN.md b/docs/dev/STRUCTURED_XCODEBUILD_EVENTS_PLAN.md new file mode 100644 index 00000000..3beed65e --- /dev/null +++ b/docs/dev/STRUCTURED_XCODEBUILD_EVENTS_PLAN.md @@ -0,0 +1,723 @@ +# Unified tool output pipeline + +## Goal + +Every tool in XcodeBuildMCP must produce its output through a single structured pipeline. No tool may construct its own formatted text. The pipeline owns all rendering, spacing, path formatting, and section structure. + +This applies to: + +- xcodebuild-backed tools (build, test, build & run, clean) +- query tools (list simulators, list schemes, discover projects, show build settings) +- action tools (set appearance, set location, boot simulator, install app) +- coverage tools (coverage report, file coverage) +- scaffolding tools (scaffold iOS project, scaffold macOS project) +- logging tools (start/stop log capture) +- debugging tools (attach, breakpoints, variables) +- UI automation tools (tap, swipe, type text, screenshot, snapshot UI) +- session tools (set defaults, show defaults, clear defaults) + +No exceptions. If a tool produces user-visible output, it goes through the pipeline. + +## Architecture principle + +Shared formatting, runtime-specific renderers. + +All renderers share a single set of formatting functions (`event-formatting.ts`) that define how each event type is converted to text. This is the single source of truth for output formatting. Each runtime has its own renderer that orchestrates those shared formatters according to its needs: + +- **MCP renderer** (`mcp-renderer.ts`): Buffers formatted text and returns it in `ToolResponse.content`. Applies session-level warning suppression. +- **CLI text renderer** (`cli-text-renderer.ts`): Writes formatted text to stdout as events arrive. In interactive TTY mode, uses a Clack spinner for transient status updates (build stages, progress). Manages durable vs transient line state. +- **CLI JSONL renderer** (`cli-jsonl-renderer.ts`): Serialises each event as one JSON line to stdout. Does not go through the text formatters. + +The renderers are not "dumb pipes" — the CLI text renderer in particular is a state machine that tracks transient lines, flush timing, and interactive spinner state. This is why the architecture uses separate renderer implementations rather than a single renderer with sink adapters. + +The key invariant is: **all text formatting lives in `event-formatting.ts`**. Renderers orchestrate when and how those formatters are called, but no renderer contains its own formatting logic. + +Runtime-specific rendering concerns: + +- CLI interactive mode: Clack spinner for transient status updates, durable flush rules before summary events +- Next steps syntax: CLI renders `xcodebuildmcp workflow tool --flag "value"`, MCP renders `tool_name({ param: "value" })`. This is a single parameterised formatting function. +- Warning suppression: session-level filter applied in MCP renderer before rendering. + +## Why this matters + +Without a unified pipeline, every tool re-invents: + +- spacing between sections (some add blank lines, some don't) +- file path formatting (some call `displayPath`, some don't) +- header/preflight structure (some use `formatToolPreflight`, some build strings manually) +- error formatting (some use icons, some use `[NOT COVERED]`, some use bare text) +- next steps rendering (some hardcode strings, some use the manifest) + +Every new tool or refactor re-introduces the same bugs. The pipeline makes these bugs structurally impossible. + +## Event model + +All tools emit structured events. The renderer converts events to formatted text. Tools never produce formatted text directly. + +### Generic tool events + +These events cover all non-xcodebuild tools: + +```ts +type ToolEvent = + | HeaderEvent // preflight block: operation name + params + | SectionEvent // titled group of content lines + | DetailTreeEvent // key/value pairs with tree connectors + | StatusLineEvent // single status message (success, error, info) + | FileRefEvent // a file path (always normalised) + | TableEvent // rows of structured data + | SummaryEvent // final outcome line + | NextStepsEvent // suggested follow-up actions + | XcodebuildEvent; // existing xcodebuild events (unchanged) +``` + +#### HeaderEvent + +Replaces `formatToolPreflight`. Every tool starts with a header. + +```ts +interface HeaderEvent { + type: 'header'; + operation: string; // e.g. 'File Coverage', 'List Simulators', 'Set Appearance' + params: Array<{ // rendered as indented key: value lines + label: string; + value: string; + }>; + timestamp: string; +} +``` + +The renderer owns: + +- the emoji (looked up from the operation name) +- the blank line after the heading +- the indentation of params +- the trailing blank line after the params block + +Tools cannot get the spacing wrong because they never produce it. + +#### SectionEvent + +A titled group of content lines with an optional icon. + +```ts +interface SectionEvent { + type: 'section'; + title: string; // e.g. 'Not Covered (7 functions, 22 lines)' + icon?: 'red-circle' | 'yellow-circle' | 'green-circle' | 'checkmark' | 'cross' | 'info'; + lines: string[]; // indented content lines + timestamp: string; +} +``` + +The renderer owns: + +- the icon-to-emoji mapping +- the blank line before and after each section +- the indentation of content lines + +#### DetailTreeEvent + +Key/value pairs rendered with tree connectors. + +```ts +interface DetailTreeEvent { + type: 'detail-tree'; + items: Array<{ label: string; value: string }>; + timestamp: string; +} +``` + +Rendered as: + +```text + ├ App Path: /path/to/app + └ Bundle ID: com.example.app +``` + +The renderer owns the connector characters and indentation. + +#### StatusLineEvent + +A single status message. + +```ts +interface StatusLineEvent { + type: 'status-line'; + level: 'success' | 'error' | 'info' | 'warning'; + message: string; + timestamp: string; +} +``` + +The renderer owns the emoji prefix based on level. + +#### FileRefEvent + +A file path that must be normalised. + +```ts +interface FileRefEvent { + type: 'file-ref'; + label?: string; // e.g. 'File' — rendered as "File: " + path: string; // raw absolute path from the tool + timestamp: string; +} +``` + +The renderer always runs the path through `displayPath()` (relative if under cwd, absolute otherwise). Tools cannot bypass this. + +#### TableEvent + +Rows of structured data grouped under an optional heading. + +```ts +interface TableEvent { + type: 'table'; + heading?: string; // e.g. 'iOS 18.5' + columns: string[]; // column names for alignment + rows: Array>; + timestamp: string; +} +``` + +The renderer owns column alignment and indentation. + +#### SummaryEvent (generic) + +A final outcome line for non-xcodebuild tools. Different from the xcodebuild `SummaryEvent` which includes test counts and duration. + +```ts +interface GenericSummaryEvent { + type: 'generic-summary'; + level: 'success' | 'error'; + message: string; + timestamp: string; +} +``` + +#### NextStepsEvent + +Unchanged from the existing model. Parameterised rendering for CLI vs MCP syntax. + +### Xcodebuild events + +The existing `XcodebuildEvent` union type is unchanged. Xcodebuild-backed tools continue to use: + +- `start` (replaces `HeaderEvent` for xcodebuild tools — the start event already contains the preflight) +- `status`, `warning`, `error`, `notice` +- `test-discovery`, `test-progress`, `test-failure` +- `summary` +- `next-steps` + +The xcodebuild event parser feeds these into the same pipeline. The renderer handles both generic tool events and xcodebuild events. + +## Pipeline architecture + +### For xcodebuild-backed tools (existing, unchanged) + +```text +tool logic + -> startBuildPipeline(...) + -> XcodebuildPipeline + -> parser + run-state + -> ordered XcodebuildEvent stream + -> renderer -> sink (stdout or buffer) +``` + +This path remains as-is. The xcodebuild parser, run-state layer, and event types do not change. + +### For all other tools (new) + +```text +tool logic + -> emits ToolEvent[] (or streams them) + -> renderer -> sink (stdout or buffer) +``` + +Simple tools emit events synchronously and return them. The pipeline renders them and routes to the appropriate sink. + +There is no parser or run-state layer for non-xcodebuild tools. They don't need one — they already have structured data. The pipeline is just: structured events -> renderer -> sink. + +### Mermaid diagram + +```mermaid +flowchart LR + subgraph "Xcodebuild tools" + A[Tool logic] --> B[XcodebuildPipeline] + B --> C[Event parser] + B --> D[Run-state] + C --> D + D --> E[PipelineEvent stream] + end + + subgraph "All other tools" + F[Tool logic] --> G[PipelineEvent array] + end + + E --> H[resolveRenderers] + G --> I[toolResponse] --> H + + H --> J[MCP renderer] + H --> K{CLI mode?} + + J --> L[Buffer → ToolResponse.content] + + K -->|text| M[CLI text renderer] + K -->|json| N[CLI JSONL renderer] + + M --> O[stdout - streaming text] + N --> P[stdout - streaming JSON] + + subgraph "Shared formatting" + Q[event-formatting.ts] + end + + J -.-> Q + M -.-> Q +``` + +### Renderer behaviour + +#### MCP renderer + +- Buffers all formatted text parts +- Returns as `ToolResponse.content` when the tool completes +- Applies session-level warning suppression +- Groups compiler errors, warnings, and test failures for batch rendering before summary + +#### CLI text renderer + +- Writes formatted text to stdout as events arrive +- In interactive TTY mode: uses Clack spinner for transient status events, tracks durable vs transient line state +- In non-interactive mode: writes all events as durable lines +- Groups compiler errors, warnings, and test failures for batch rendering before summary +- Tracks `lastVisibleEventType` for compact spacing between consecutive status lines + +#### CLI JSONL renderer + +- Serialises each event as one JSON line to stdout +- Does not go through the text formatters +- Available for all tools (events are the same union type) + +### Renderer resolution + +`resolveRenderers()` in `src/utils/renderers/index.ts` always creates the MCP renderer (for `ToolResponse.content`). If running in CLI mode, it also creates either the CLI text renderer or CLI JSONL renderer based on output format. + +`toolResponse()` in `src/utils/tool-response.ts` feeds events through all active renderers and extracts content from the MCP renderer. + +## Formatting contract + +One set of formatting functions. All renderers. + +```ts +// src/utils/renderers/event-formatting.ts +formatHeaderEvent(event: HeaderEvent): string; +formatBuildStageEvent(event: BuildStageEvent): string; +formatStatusLineEvent(event: StatusLineEvent): string; +formatSectionEvent(event: SectionEvent): string; +formatDetailTreeEvent(event: DetailTreeEvent): string; +formatTableEvent(event: TableEvent): string; +formatFileRefEvent(event: FileRefEvent): string; +formatSummaryEvent(event: SummaryEvent): string; +formatNextStepsEvent(event: NextStepsEvent, runtime: 'cli' | 'mcp'): string; +``` + +The formatting layer is the single source of truth for: + +- emoji selection per operation/level/icon +- spacing between sections (always one blank line) +- file path normalisation (always `displayPath()`) +- indentation depth (always 2 spaces for params, content lines) +- tree connector characters +- next steps formatting (parameterised by runtime) +- section ordering enforcement + +### Formatting rules enforced by the renderer + +These rules are not guidelines. They are enforced structurally because tools cannot produce formatted text. + +1. **Header always has a trailing blank line.** The renderer emits: blank line, emoji + operation, blank line, indented params, blank line. Every tool. No exceptions. + +2. **File paths are always normalised.** `FileRefEvent` paths always go through `displayPath()`. Xcodebuild diagnostic paths go through `formatDiagnosticFilePath()`. There is no code path where a raw absolute path reaches the output. + +3. **Sections are always separated by blank lines.** The renderer adds one blank line before each section. Tools cannot omit or double this. + +4. **Icons are always consistent.** The renderer maps `icon` enum values to emoji. Tools do not contain emoji characters. + +5. **Next steps are always last.** The renderer enforces ordering. Nothing renders after next steps. + +6. **Error messages follow the convention.** `Failed to : `. The renderer does not enforce this (it's a content concern), but the pipeline API makes it easy to follow. + +## How tools emit events + +### Simple action tools (e.g. set appearance) + +```ts +return toolResponse([ + header('Set Appearance', [ + { label: 'Simulator', value: simulatorId }, + ]), + statusLine('success', `Appearance set to ${mode} mode`), +]); +``` + +### Query tools (e.g. list simulators) + +```ts +return toolResponse([ + header('List Simulators'), + ...grouped.map(([runtime, devices]) => + table(runtime, ['Name', 'UUID', 'State'], + devices.map(d => ({ Name: d.name, UUID: d.udid, State: d.state })) + ) + ), + nextSteps([...]), +]); +``` + +### Coverage tools (e.g. file coverage) + +```ts +return toolResponse([ + header('File Coverage', [ + { label: 'xcresult', value: xcresultPath }, + { label: 'File', value: file }, + ]), + fileRef('File', entry.filePath), + statusLine('info', `Coverage: ${pct}% (${covered}/${total} lines)`), + section('Not Covered', notCoveredLines, { icon: 'red-circle', + title: `Not Covered (${count} functions, ${missedLines} lines)` }), + section('Partial Coverage', partialLines, { icon: 'yellow-circle', + title: `Partial Coverage (${count} functions)` }), + section('Full Coverage', [`${fullCount} functions — all at 100%`], { icon: 'green-circle', + title: `Full Coverage (${fullCount} functions) — all at 100%` }), + nextSteps([...]), +]); +``` + +### Xcodebuild tools + +These keep the existing parser and run-state layers (`startBuildPipeline()`, `executeXcodeBuildCommand()`, `createPendingXcodebuildResponse()`), but the run-state output gets mapped to `ToolEvent` types before reaching the renderer. The xcodebuild parser remains an ingestion layer — it just feeds into the unified event model instead of having its own rendering path. Streaming and Clack progress are preserved as CLI sink concerns. + +## Locked human-readable output contract + +The output structure for all tools follows the same rhythm: + +```text + + + : + : + + + + + + + +Next steps: +1. +2. +``` + +### For xcodebuild-backed tools + +The canonical examples are `build_run_macos` and `build_run_sim`. Their output contract is locked: + +Successful runs: + +1. front matter (header event / start event) +2. runtime state and durable diagnostics +3. summary +4. execution-derived footer (detail tree) +5. next steps + +Failed runs: + +1. front matter +2. runtime state and/or grouped diagnostics +3. summary + +Failed runs do not render next steps. + +### For non-xcodebuild tools + +Successful runs: + +1. header +2. body (sections, tables, file refs, status lines — tool-specific) +3. next steps (if applicable) + +Failed runs: + +1. header +2. error status line +3. no next steps + +### Example outputs + +#### Build (xcodebuild pipeline — existing) + +```text +🔨 Build + + Scheme: CalculatorApp + Workspace: example_projects/iOS_Calculator/CalculatorApp.xcworkspace + Configuration: Debug + Platform: iOS Simulator + Simulator: iPhone 17 + +✅ Build succeeded. (⏱️ 12.3s) + +Next steps: +1. Get built app path: xcodebuildmcp simulator get-app-path --scheme "CalculatorApp" +``` + +#### File Coverage (generic pipeline — new) + +```text +📊 File Coverage + + xcresult: /tmp/TestResults.xcresult + File: CalculatorService.swift + +File: example_projects/.../CalculatorService.swift +Coverage: 83.1% (157/189 lines) + +🔴 Not Covered (7 functions, 22 lines) + L159 CalculatorService.deleteLastDigit() — 0/16 lines + L58 implicit closure #2 in inputNumber(_:) — 0/1 lines + +🟡 Partial Coverage (4 functions) + L184 updateExpressionDisplay() — 80.0% (8/10 lines) + L195 formatNumber(_:) — 85.7% (18/21 lines) + +🟢 Full Coverage (28 functions) — all at 100% + +Next steps: +1. View overall coverage: xcodebuildmcp coverage get-coverage-report --xcresult-path "/tmp/TestResults.xcresult" +``` + +#### List Simulators (generic pipeline — new) + +```text +📱 List Simulators + +iOS 18.5: + iPhone 16 Pro A1B2C3D4-... Booted + iPhone 16 E5F6G7H8-... Shutdown + iPad Pro 13" I9J0K1L2-... Shutdown + +iOS 17.5: + iPhone 15 M3N4O5P6-... Shutdown + +Next steps: +1. Boot simulator: xcodebuildmcp simulator-management boot --simulator-id "UUID" +``` + +#### Set Appearance (generic pipeline — new) + +```text +🎨 Set Appearance + + Simulator: A1B2C3D4-E5F6-... + +✅ Appearance set to dark mode +``` + +#### Discover Projects (generic pipeline — new) + +```text +🔍 Discover Projects + + Search Path: . + +Workspaces: + example_projects/iOS_Calculator/CalculatorApp.xcworkspace + +Projects: + example_projects/iOS_Calculator/CalculatorApp.xcodeproj + +Next steps: +1. List schemes: xcodebuildmcp project-discovery list-schemes --workspace-path "example_projects/iOS_Calculator/CalculatorApp.xcworkspace" +``` + +## Xcodebuild pipeline specifics + +The existing xcodebuild pipeline architecture is preserved. This section documents it for reference. + +### Execution flow + +1. Tool calls `startBuildPipeline(...)` from `src/utils/xcodebuild-pipeline.ts` +2. Pipeline creates parser and run-state, emits initial `start` event +3. Raw stdout/stderr chunks feed into `createXcodebuildEventParser(...)` +4. Parser emits structured events into `createXcodebuildRunState(...)` +5. Tool-emitted events (post-build notices, errors) enter run-state through `pipeline.emitEvent(...)` +6. Run-state dedupes, orders, aggregates, forwards to the unified renderer +7. On finalize: summary + tail events + next-steps emitted in order + +### Canonical pattern + +```ts +const started = startBuildPipeline({ + operation: 'BUILD', + toolName: 'build_run_', + params: { scheme, configuration, platform, preflight: preflightText }, + message: preflightText, +}); + +const buildResult = await executeXcodeBuildCommand(..., started.pipeline); +if (buildResult.isError) { + return createPendingXcodebuildResponse(started, buildResult, { + errorFallbackPolicy: 'if-no-structured-diagnostics', + }); +} + +// Post-build steps: emit notices for progress, errors for failures +emitPipelineNotice(started, 'BUILD', 'Resolving app path', 'info', { + code: 'build-run-step', + data: { step: 'resolve-app-path', status: 'started' }, +}); + +// ... resolve, boot, install, launch ... + +return createPendingXcodebuildResponse( + started, + { content: [], isError: false, nextStepParams: { ... } }, + { + tailEvents: [{ + type: 'notice', + timestamp: new Date().toISOString(), + operation: 'BUILD', + level: 'success', + message: 'Build & Run complete', + code: 'build-run-result', + data: { scheme, platform, target, appPath, bundleId, launchState: 'requested' }, + }], + }, +); +``` + +### Pending response lifecycle + +1. Tool returns `createPendingXcodebuildResponse(started, response, options)` +2. `postProcessToolResponse` in `src/runtime/tool-invoker.ts` detects the pending state +3. Resolves manifest-driven next-step templates against `nextStepParams` +4. Calls `finalizePendingXcodebuildResponse` which finalizes the pipeline +5. Finalized content becomes `ToolResponse.content` + +### Post-build step notices + +Post-build steps use `notice` events with `code: 'build-run-step'`: + +Available step names (defined in `BuildRunStepName` in `src/types/xcodebuild-events.ts`): + +- `resolve-app-path` +- `resolve-simulator` +- `boot-simulator` +- `install-app` +- `extract-bundle-id` +- `launch-app` + +To add new steps: extend `BuildRunStepName` and add the label in `formatBuildRunStepLabel` in `src/utils/renderers/event-formatting.ts`. + +### Error message convention + +All post-build errors via `emitPipelineError` use: `Failed to : ` + +### All errors get grouped rendering + +All error events are batched and rendered as a single grouped section before the summary: + +- If any error has a file location: `Compiler Errors (N):` +- Otherwise: `Errors (N):` + +Each error: ` ✗ ` with optional ` ` and continuation lines. + +### Error event message field + +The `message` field must not include severity prefix. Correct: `"unterminated string literal"`. Wrong: `"error: unterminated string literal"`. The `rawLine` field preserves the original verbatim. + +## Implementation steps + +One canonical list. Checked items are done. Remaining items are work-in-progress. + +### Infrastructure (done) + +- [x] Define `PipelineEvent` union type in `src/types/pipeline-events.ts` (named `PipelineEvent`, not `ToolEvent`) +- [x] Define `toolResponse()` builder + helper functions: `header()`, `section()`, `statusLine()`, `fileRef()`, `table()`, `detailTree()`, `nextSteps()` in `src/utils/tool-event-builders.ts` +- [x] Build shared formatting layer in `src/utils/renderers/event-formatting.ts` +- [x] Build MCP renderer (`src/utils/renderers/mcp-renderer.ts`) — buffers formatted text for `ToolResponse.content` +- [x] Build CLI text renderer (`src/utils/renderers/cli-text-renderer.ts`) — streaming text to stdout with interactive spinner support +- [x] Preserve CLI JSONL renderer (`src/utils/renderers/cli-jsonl-renderer.ts`) for machine-readable output +- [x] Build `resolveRenderers()` orchestration in `src/utils/renderers/index.ts` +- [x] Build `toolResponse()` entry point in `src/utils/tool-response.ts` that feeds events through renderers +- [x] Migrate xcodebuild pipeline run-state to emit `PipelineEvent` types through renderers (preserve parser, run-state, streaming, Clack) +- [x] Write designed fixtures for all tools (`__fixtures_designed__/`) + +### Tool migration (mostly done) + +- [x] Migrate xcodebuild tools: `build_sim`, `build_device`, `build_macos`, `build_run_sim`, `build_run_device`, `build_run_macos` +- [x] Migrate simple action tools: `set_sim_appearance`, `set_sim_location`, `reset_sim_location`, `sim_statusbar`, `boot_sim`, `open_sim`, `stop_app_sim`, `stop_app_device`, `stop_mac_app`, `launch_app_sim`, `launch_app_device`, `launch_mac_app`, `install_app_sim`, `install_app_device` +- [x] Migrate most query tools: `list_sims`, `discover_projs`, `list_schemes`, `show_build_settings`, `get_app_bundle_id`, `get_mac_bundle_id` +- [x] Migrate coverage tools: `get_coverage_report`, `get_file_coverage` +- [x] Migrate scaffolding tools: `scaffold_ios_project`, `scaffold_macos_project` +- [x] Migrate session tools: `session_set_defaults`, `session_clear_defaults`, `session_use_defaults_profile` +- [x] Migrate logging tools: `start_sim_log_cap`, `stop_sim_log_cap`, `start_device_log_cap`, `stop_device_log_cap` +- [x] Migrate debugging tools: `debug_attach_sim`, `debug_breakpoint_add`, `debug_breakpoint_remove`, `debug_continue`, `debug_detach`, `debug_lldb_command`, `debug_stack`, `debug_variables` +- [x] Migrate UI automation tools: `snapshot_ui`, `tap`, `type_text`, `button`, `gesture`, `key_press`, `key_sequence`, `long_press`, `swipe`, `touch` +- [x] Migrate swift-package tools: `swift_package_build`, `swift_package_clean`, `swift_package_list`, `swift_package_stop` +- [x] Migrate xcode-ide tools: `xcode_ide_call_tool`, `xcode_ide_list_tools`, `xcode_tools_bridge_disconnect`, `xcode_tools_bridge_status`, `xcode_tools_bridge_sync`, `sync_xcode_defaults` +- [x] Migrate doctor tool + +### Remaining: tools that were migrated then reverted to manual text + +These tools were migrated to the pipeline in `ac33b97f` but reverted to manual `ToolResponse` construction in `c0693a1d`. The fixtures in `__fixtures__/` define the correct target output. The pipeline (renderers and/or event types) needs to be extended to produce that output — the tools should NOT hand-craft text to match fixtures. + +- [x] Re-migrate `get_sim_app_path` — extended `SectionEvent` with `blankLineAfterTitle`, added `extractQueryErrorMessages`, added `suppressCliStream` to `toolResponse()` for late-bound CLI next steps +- [x] Re-migrate `get_device_app_path` — same approach +- [x] Re-migrate `get_mac_app_path` — same approach +- [x] Re-migrate `list_devices` success path — uses `blankLineAfterTitle` sections for grouped-by-platform layout +- [x] Clean up `swift_package_run` error fallback — removed manual content, relies on pipeline-produced structured diagnostics +- [x] Clean up `swift_package_test` error fallback — same +- [ ] Re-migrate `session_show_defaults` — remove inline emoji from section titles, use `detailTree()` instead of manual tree connectors +- [ ] Re-migrate `screenshot` — remove manual content branches for base64 fallback + +### Remaining: presentation leakage in migrated tools + +These tools use `toolResponse()` but embed presentation details in event payloads that should be owned by the renderer: + +- [ ] `list_sims` — remove inline emoji and `✓`/`✗` markers from section content; these should come from the renderer or event type metadata +- [ ] `session_show_defaults` — use `detailTree()` events instead of `formatDetailLines()` manual tree connectors + +### Remaining: cleanup + +- [ ] Delete `formatToolPreflight` in `src/utils/build-preflight.ts` once all tools use pipeline `HeaderEvent` +- [ ] All snapshot tests pass against `__fixtures__/` (target output) +- [ ] Manual verification of CLI output for representative tools + +## Success criteria + +This work is successful when: + +- every tool emits structured events through the pipeline +- shared formatting functions in `event-formatting.ts` produce all formatted output +- CLI and MCP durable output are identical (CLI interactive mode may show transient spinner updates) +- file paths are always normalised — no tool can produce a raw absolute path +- spacing between sections is always correct — no tool can get it wrong +- the only way to add a new tool's output is to emit events — there is no escape hatch +- adding a new output format (e.g. markdown, HTML) requires only a new renderer, not touching any tool code +- all `__fixtures__/` snapshot tests pass with output produced by the pipeline, not by manual text construction + +## Design constraints + +- all text formatting lives in `event-formatting.ts` — renderers orchestrate, they do not contain formatting logic +- no formatted text construction inside tool logic +- no emoji characters inside tool logic (formatting layer owns the mapping) +- no `displayPath()` calls inside tool logic (formatting layer owns path normalisation) +- no spacing/indentation decisions inside tool logic (formatting layer owns layout) +- xcodebuild event parser and run-state layer are preserved — they work well and do not need to change +- CLI JSONL mode is preserved for all tools +- no attempt to make non-xcodebuild tools streamable initially — they complete fast enough that buffered rendering is fine +- if the pipeline cannot produce a fixture's target output, extend the pipeline (new event types, new formatting functions) — do not bypass the pipeline to match fixtures manually diff --git a/docs/dev/TESTING.md b/docs/dev/TESTING.md index 4e11f722..f8353430 100644 --- a/docs/dev/TESTING.md +++ b/docs/dev/TESTING.md @@ -63,7 +63,7 @@ XcodeBuildMCP follows a dependency-injection testing philosophy for external bou 2. **Real Coverage**: Tests verify actual user data flows 3. **Maintainability**: No brittle vitest mocks that break on implementation changes 4. **True Integration**: Catches integration bugs between layers -5. **Test Safety**: Default executors throw errors in test environment +5. **Test Safety**: A Vitest setup file installs blocking executor overrides for unit tests ### Automated Violation Checking @@ -1196,30 +1196,31 @@ This systematic approach ensures comprehensive, accurate testing using programma ### Common Issues -#### 1. "Real System Executor Detected" Error -**Symptoms**: Test fails with error about real system executor being used -**Cause**: Handler not receiving mock executor parameter -**Fix**: Ensure test passes createMockExecutor() to handler: +#### 1. "Noop Executor Called" Error +**Symptoms**: Test fails with `NOOP EXECUTOR CALLED` or `NOOP FILESYSTEM EXECUTOR CALLED` +**Cause**: The Vitest unit setup (`src/test-utils/vitest-executor-safety.setup.ts`) installs +blocking noop overrides for all unit tests. If a handler calls `getDefaultCommandExecutor()` or +`getDefaultFileSystemExecutor()` without an explicit test override, the noop throws. +**Fix**: Either inject a mock executor directly into the logic function, or use the override hooks: ```typescript -// ❌ WRONG -const result = await tool.handler(params); - -// ✅ CORRECT +// Option A: Direct injection into the logic function const mockExecutor = createMockExecutor({ success: true }); -const result = await tool.handler(params, mockExecutor); +const result = await toolLogic(params, mockExecutor); + +// Option B: Override hooks (for handler-level tests) +import { __setTestCommandExecutorOverride } from '../utils/command.ts'; +__setTestCommandExecutorOverride(createMockExecutor({ success: true })); +const result = await handler(params); ``` -#### 2. "Real Filesystem Executor Detected" Error -**Symptoms**: Test fails when trying to access file system -**Cause**: Handler not receiving mock file system executor -**Fix**: Pass createMockFileSystemExecutor(): +**Note**: The setup file only applies to `vitest.config.ts` (unit tests). Snapshot and smoke +tests use separate configs and are not affected. -```typescript -const mockCmd = createMockExecutor({ success: true }); -const mockFS = createMockFileSystemExecutor({ readFile: async () => 'content' }); -const result = await tool.handler(params, mockCmd, mockFS); -``` +#### 2. "Noop Interactive Spawner Called" Error +**Symptoms**: Test fails with `NOOP INTERACTIVE SPAWNER CALLED` +**Cause**: Same mechanism as above but for `getDefaultInteractiveSpawner()`. +**Fix**: Use `createMockInteractiveSpawner()` from `test-utils/mock-executors.ts`. #### 3. Handler Signature Errors **Symptoms**: TypeScript errors about handler parameters diff --git a/docs/dev/TOOL_DISCOVERY_LOGIC.md b/docs/dev/TOOL_DISCOVERY_LOGIC.md index 5008b8f8..a6054c89 100644 --- a/docs/dev/TOOL_DISCOVERY_LOGIC.md +++ b/docs/dev/TOOL_DISCOVERY_LOGIC.md @@ -9,21 +9,28 @@ It also documents the current and intended **visibility filtering** behavior (po ## Terminology -- **Workflow**: a directory under `src/mcp/tools//` containing an `index.ts` with workflow metadata and tool modules. -- **Tool**: a `PluginMeta` exported from a workflow module with `name`, `schema`, and `handler`. +- **Workflow**: a manifest entry in `manifests/workflows/.yaml` referencing tool IDs. +- **Tool**: a manifest entry in `manifests/tools/.yaml` with a module path; the module exports `{ schema, handler }`. +- **Resource**: a manifest entry in `manifests/resources/.yaml` with a module path; the module exports `{ handler }`. - **Workflow selection**: picking which workflows are active (coarse-grained inclusion). -- **Visibility filtering**: hiding specific tools even if their workflow is enabled (fine-grained exclusion). +- **Visibility filtering**: hiding specific tools/resources even if their workflow is enabled (fine-grained exclusion via predicates). - **Dynamic tools**: tools registered at runtime that do not come from static workflows (e.g. proxied Xcode Tools). -## Where workflows/tools come from (source of truth) +## Where workflows/tools/resources come from (source of truth) -Workflows are discovered via generated loaders in `src/core/generated-plugins.ts` (the `WORKFLOW_LOADERS` map). At runtime, `loadWorkflowGroups()` imports each workflow module via these loaders and collects tools from it (`src/core/plugin-registry.ts`). +YAML manifests in `manifests/` are the single source of truth for metadata: + +- `manifests/tools/*.yaml` define individual tools and their module paths +- `manifests/workflows/*.yaml` define workflow groupings that reference tool IDs +- `manifests/resources/*.yaml` define MCP resources and their module paths + +At runtime, `loadManifest()` reads all YAML files and returns a `ResolvedManifest` containing tools, workflows, and resources. Tool/resource code modules are dynamically imported via `importToolModule()` and `importResourceModule()`. Key properties of this design: -- Workflows are “discoverable” by enumerating `Object.keys(WORKFLOW_LOADERS)`. -- Tools within a workflow are whatever `index.ts` exports (excluding `workflow` itself). -- A single tool name can appear in multiple workflows (re-exports). This matters for workflow management and hiding. +- Workflows are discoverable by enumerating the manifest's workflow entries. +- Tools within a workflow are listed by tool ID references. +- A single tool can appear in multiple workflows (referenced by ID). This matters for workflow management and hiding. ## MCP server: registration pipeline diff --git a/docs/dev/simulator-test-benchmark.md b/docs/dev/simulator-test-benchmark.md new file mode 100644 index 00000000..368cdf18 --- /dev/null +++ b/docs/dev/simulator-test-benchmark.md @@ -0,0 +1,83 @@ +# Simulator test benchmark + +This benchmark compares XcodeBuildMCP's simulator test command against Flowdeck CLI using the Calculator example project in the current worktree. + +## Prerequisites + +- `npm install` +- `npm run build` +- `flowdeck` available on `PATH` +- An `iPhone 17 Pro` simulator installed +- `/usr/bin/script` available (required so both tools run under a PTY and stream live progress) + +## Command + +```bash +npm run bench:test-sim -- --iterations 1 --mode warm +``` + +Options: + +- `--iterations `: repeat both tools `n` times +- `--mode warm|cold`: reuse or clear benchmark-owned derived data before each run + +## Exact commands used + +XcodeBuildMCP: + +```bash +./build/cli.js simulator test --json '{"workspacePath":"/example_projects/iOS_Calculator/CalculatorApp.xcworkspace","scheme":"CalculatorApp","simulatorName":"iPhone 17 Pro","useLatestOS":true,"extraArgs":["-only-testing:CalculatorAppTests"],"progress":true,"derivedDataPath":"/derived-data-xcodebuildmcp"}' --output text +``` + +Flowdeck CLI: + +```bash +flowdeck test -w /example_projects/iOS_Calculator/CalculatorApp.xcworkspace -s CalculatorApp -S "iPhone 17 Pro" --only CalculatorAppTests --progress -d /derived-data-flowdeck +``` + +Both commands are executed through `/usr/bin/script -q /dev/null ...` so the benchmark measures the real TTY streaming path instead of a buffered pipe. + +## Output + +Artifacts are written to: + +```text +benchmarks/simulator-test// +``` + +Each run writes: + +- `summary.json` +- `xcodebuildmcp-run-*.stdout.txt` +- `xcodebuildmcp-run-*.stderr.txt` +- `flowdeck-run-*.stdout.txt` +- `flowdeck-run-*.stderr.txt` + +Captured metrics: + +- wall-clock duration +- time to first stdout +- time to first milestone output +- time to first streamed test progress output +- exit code + +Transcripts are normalized before saving: + +- ANSI escapes are stripped +- carriage returns are converted to newlines +- PTY control characters are removed + +## Manual compile-error fixture + +To manually compare compile-failure output styling against Flowdeck without keeping the example project permanently broken: + +```bash +cp example_projects/iOS_Calculator/manual-fixtures/CalculatorAppTests/CompileError.fixture.swift \ + example_projects/iOS_Calculator/CalculatorAppTests/CompileError.swift +``` + +Then rerun the simulator test command in both tools. When finished, remove the copied file: + +```bash +rm example_projects/iOS_Calculator/CalculatorAppTests/CompileError.swift +``` diff --git a/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift b/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift index d03531b4..08e33364 100644 --- a/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift +++ b/example_projects/iOS_Calculator/CalculatorApp/CalculatorApp.swift @@ -1,12 +1,27 @@ import SwiftUI +import OSLog import CalculatorAppFeature +private let logger = Logger(subsystem: "io.sentry.calculatorapp", category: "lifecycle") + @main struct CalculatorApp: App { + @Environment(\.scenePhase) private var scenePhase + var body: some Scene { WindowGroup { ContentView() } + .onChange(of: scenePhase) { _, newPhase in + switch newPhase { + case .active: + logger.info("Calculator app launched") + case .background: + logger.info("Calculator app terminated") + default: + break + } + } } } diff --git a/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift b/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift index 7d4c8eae..2350da96 100644 --- a/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift +++ b/example_projects/iOS_Calculator/CalculatorAppPackage/Sources/CalculatorAppFeature/ContentView.swift @@ -70,7 +70,7 @@ public struct ContentView: View { } private func handleButtonPress(_ button: String) { - print("[CalculatorApp] Button pressed: \(button)") + print("Key pressed = \(button)") // Process input through the input handler inputHandler.handleInput(button) diff --git a/example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift b/example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift index 4e359623..d0054bfe 100644 --- a/example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift +++ b/example_projects/iOS_Calculator/CalculatorAppTests/CalculatorAppTests.swift @@ -63,6 +63,17 @@ extension CalculatorAppTests { XCTAssertEqual(service.display, "8", "5 + 3 should equal 8") } + + func testAddition() throws { + let service = CalculatorService() + + service.inputNumber("5") + service.setOperation(.add) + service.inputNumber("3") + service.calculate() + + XCTAssertEqual(service.display, "8", "5 + 3 should equal 8") + } func testCalculatorServiceChainedOperations() throws { let service = CalculatorService() @@ -269,6 +280,13 @@ extension CalculatorAppTests { } } +final class IntentionalFailureTests: XCTestCase { + + func test() throws { + XCTAssertTrue(false, "This test should fail to verify error reporting") + } +} + // MARK: - Component Integration Tests extension CalculatorAppTests { diff --git a/example_projects/iOS_Calculator/manual-fixtures/CalculatorAppTests/CompileError.fixture.swift b/example_projects/iOS_Calculator/manual-fixtures/CalculatorAppTests/CompileError.fixture.swift new file mode 100644 index 00000000..65d1e1e1 --- /dev/null +++ b/example_projects/iOS_Calculator/manual-fixtures/CalculatorAppTests/CompileError.fixture.swift @@ -0,0 +1,3 @@ +import XCTest + +let compileErrorFixture: Int = "not an int" diff --git a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj index 2efd7d0b..23d6bf55 100644 --- a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -365,7 +365,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/example_projects/macOS/MCPTestTests/MCPTestTests.swift b/example_projects/macOS/MCPTestTests/MCPTestTests.swift index afce860a..be41aec1 100644 --- a/example_projects/macOS/MCPTestTests/MCPTestTests.swift +++ b/example_projects/macOS/MCPTestTests/MCPTestTests.swift @@ -1,16 +1,13 @@ -// -// MCPTestTests.swift -// MCPTestTests -// -// Created by Cameron on 15/12/2025. -// - import Testing struct MCPTestTests { - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. + @Test func appNameIsCorrect() async throws { + let expected = "MCPTest" + #expect(expected == "MCPTest") } + @Test func deliberateFailure() async throws { + #expect(1 == 2, "This test is designed to fail for snapshot testing") + } } diff --git a/example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift b/example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift new file mode 100644 index 00000000..9262029c --- /dev/null +++ b/example_projects/macOS/MCPTestTests/MCPTestsXCTests.swift @@ -0,0 +1,13 @@ +import XCTest + +final class MCPTestsXCTests: XCTestCase { + + func testAppNameIsCorrect() async throws { + let expected = "MCPTest" + XCTAssertTrue(expected == "MCPTest") + } + + func testDeliberateFailure() async throws { + XCTAssertTrue(1 == 2, "This test is designed to fail for snapshot testing") + } +} diff --git a/example_projects/spm/.xcodebuildmcp/config.yaml b/example_projects/spm/.xcodebuildmcp/config.yaml new file mode 100644 index 00000000..b28ad2ad --- /dev/null +++ b/example_projects/spm/.xcodebuildmcp/config.yaml @@ -0,0 +1,9 @@ +schemaVersion: 1 +enabledWorkflows: + - project-discovery + - swift-package +debug: false +sentryDisabled: true +sessionDefaults: + workspacePath: .swiftpm/xcode/package.xcworkspace + scheme: long-server diff --git a/example_projects/spm/Sources/quick-task/main.swift b/example_projects/spm/Sources/quick-task/main.swift index 1a22bb9e..76bb8dbd 100644 --- a/example_projects/spm/Sources/quick-task/main.swift +++ b/example_projects/spm/Sources/quick-task/main.swift @@ -33,4 +33,4 @@ struct QuickTask: AsyncParsableCommand { print("✅ Quick task completed successfully!") } } -} \ No newline at end of file +} diff --git a/example_projects/spm/Tests/TestLibTests/SimpleTests.swift b/example_projects/spm/Tests/TestLibTests/SimpleTests.swift index 27bf893f..e44d6bb5 100644 --- a/example_projects/spm/Tests/TestLibTests/SimpleTests.swift +++ b/example_projects/spm/Tests/TestLibTests/SimpleTests.swift @@ -1,4 +1,5 @@ import Testing +import XCTest @Test("Basic truth assertions") func basicTruthTest() { @@ -37,8 +38,22 @@ func arrayTest() { func optionalTest() { let someValue: Int? = 42 let nilValue: Int? = nil - + #expect(someValue != nil) #expect(nilValue == nil) #expect(someValue! == 42) } + +final class CalculatorAppTests: XCTestCase { + func testCalculatorServiceFailure() { + XCTAssertEqual(0, 999, "This test should fail - display should be 0, not 999") + } +} + +@Suite("This test should fail to verify error reporting") +struct IntentionalFailureSuite { + @Test("test") + func test() { + #expect(Bool(false), "Test failed") + } +} diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..e29304bf --- /dev/null +++ b/knip.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": [ + "scripts/check-code-patterns.js", + "scripts/probe-xcode-mcpbridge.ts", + "scripts/repro-mcp-parent-exit-helper.mjs", + "src/integrations/xcode-tools-bridge/__tests__/fixtures/fake-xcode-tools-server.mjs" + ], + "project": [ + "src/**/*.{ts,js,mjs}", + "scripts/**/*.{ts,js,mjs}" + ], + "ignoreBinaries": [ + "scripts/bundle-axe.sh", + "scripts/package-macos-portable.sh", + "scripts/verify-portable-install.sh", + "scripts/create-homebrew-formula.sh", + "pkg-pr-new", + "ISC", + "BSD-2-Clause", + "BSD-3-Clause", + "Apache-2.0", + "Unlicense", + "FSL-1.1-MIT" + ] +} diff --git a/local-research/monolithic-workflows-investigation.md b/local-research/monolithic-workflows-investigation.md new file mode 100644 index 00000000..9ead3099 --- /dev/null +++ b/local-research/monolithic-workflows-investigation.md @@ -0,0 +1,284 @@ +# Investigation: Monolithic Multi-Step Workflows in build_run_* Tools + +## Summary + +The claim is **valid but nuanced**. The three `build_run_*` orchestrators (`build_run_sim`, `build_run_device`, `build_run_macos`) are monolithic at the **orchestration layer** — each inlines the full workflow (build, resolve app path, boot/install/launch) in a single function. However, they already share significant **utility-level** infrastructure. The duplication is specifically between orchestrator inline logic and the corresponding standalone step-tool handlers, which implement the same commands independently. + +## Symptoms + +- `build_run_simLogic` is 549 lines, performing ~8 distinct steps inline +- `build_run_deviceLogic` is 357 lines, performing ~6 distinct steps inline +- `buildRunMacOSLogic` is 242 lines, performing ~5 distinct steps inline +- Each orchestrator duplicates command construction found in standalone step tools +- Step tools (`boot_sim`, `install_app_sim`, `launch_app_sim`, etc.) exist but are never called by orchestrators + +## Investigation Log + +### Phase 1 — Identifying the Orchestrators and Step Tools + +**Hypothesis:** The build_run_* files contain monolithic handlers that duplicate step-tool logic. + +**Findings:** Three orchestrators exist, each with corresponding standalone step tools: + +| Orchestrator | Standalone Step Tools | +|---|---| +| `build_run_sim.ts` | `build_sim.ts`, `boot_sim.ts`, `install_app_sim.ts`, `launch_app_sim.ts`, `get_sim_app_path.ts` | +| `build_run_device.ts` | `build_device.ts`, `install_app_device.ts`, `launch_app_device.ts`, `get_device_app_path.ts` | +| `build_run_macos.ts` | `build_macos.ts`, `launch_mac_app.ts`, `get_mac_app_path.ts` | + +**Conclusion:** Confirmed — orchestrators and step tools are fully independent modules with no handler-level composition. + +### Phase 2 — Concrete Duplication: Simulator Boot + +**Hypothesis:** Boot logic is duplicated between `build_run_sim.ts` and `boot_sim.ts`. + +**Evidence:** + +`boot_sim.ts` line 57: +```typescript +const command = ['xcrun', 'simctl', 'boot', params.simulatorId]; +const result = await executor(command, 'Boot Simulator', false); +``` + +`build_run_sim.ts` lines 283-288 (inline in the orchestrator): +```typescript +const bootResult = await executor( + ['xcrun', 'simctl', 'boot', simulatorId], + 'Boot Simulator', +); +``` + +Additionally, `build_run_sim.ts` lines 246-280 contains ~35 lines of simulator state checking logic (JSON parsing of `simctl list devices available --json`, iterating runtimes to find the target simulator by UUID, checking `state !== 'Booted'`) that has no equivalent in `boot_sim.ts` — the standalone tool assumes the caller knows the simulator needs booting. + +**Conclusion:** Confirmed duplication. The orchestrator also has **extra logic** not in the step tool (state checking before boot). + +### Phase 3 — Concrete Duplication: Simulator Install + +**Hypothesis:** Install logic is duplicated. + +**Evidence:** + +`install_app_sim.ts` line 73: +```typescript +const command = ['xcrun', 'simctl', 'install', params.simulatorId, params.appPath]; +const result = await executor(command, 'Install App in Simulator', false); +``` + +`build_run_sim.ts` lines 316-319 (inline): +```typescript +const installResult = await executor( + ['xcrun', 'simctl', 'install', simulatorId, appBundlePath], + 'Install App', +); +``` + +**Conclusion:** Confirmed — identical command, duplicated in both places. + +### Phase 4 — Concrete Duplication: Simulator Launch + +**Evidence:** + +`launch_app_sim.ts` lines 103-104: +```typescript +const command = ['xcrun', 'simctl', 'launch', simulatorId, params.bundleId]; +``` +Plus PID parsing at lines 113-114: +```typescript +const pidMatch = result.output?.match(/:\s*(\d+)\s*$/); +``` + +`build_run_sim.ts` lines 355-358: +```typescript +const launchResult = await executor( + ['xcrun', 'simctl', 'launch', simulatorId, bundleId], + 'Launch App', +); +``` +Plus PID parsing at lines 362-363: +```typescript +const pidMatch = launchResult.output?.match(/:\s*(\d+)\s*$/); +``` + +**Conclusion:** Confirmed — identical command and PID regex, duplicated. + +### Phase 5 — Concrete Duplication: Device Install + +**Evidence:** + +`install_app_device.ts` line 53: +```typescript +['xcrun', 'devicectl', 'device', 'install', 'app', '--device', deviceId, appPath] +``` + +`build_run_device.ts` line 203: +```typescript +['xcrun', 'devicectl', 'device', 'install', 'app', '--device', params.deviceId, appPath] +``` + +**Conclusion:** Confirmed — identical command. + +### Phase 6 — Concrete Duplication: Device Launch (Heaviest Duplication) + +**Evidence:** + +`launch_app_device.ts` lines 80-95: +```typescript +const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`); +const command = [ + 'xcrun', 'devicectl', 'device', 'process', 'launch', + '--device', deviceId, + '--json-output', tempJsonPath, + '--terminate-existing', +]; +if (params.env && Object.keys(params.env).length > 0) { + command.push('--environment-variables', JSON.stringify(params.env)); +} +command.push(bundleId); +``` +Plus JSON PID parsing at lines 104-112 and temp file cleanup at lines 113-115. + +`build_run_device.ts` lines 223-244: +```typescript +const tempJsonPath = join(fileSystemExecutor.tmpdir(), `launch-${Date.now()}.json`); +const command = [ + 'xcrun', 'devicectl', 'device', 'process', 'launch', + '--device', params.deviceId, + '--json-output', tempJsonPath, + '--terminate-existing', +]; +if (params.env && Object.keys(params.env).length > 0) { + command.push('--environment-variables', JSON.stringify(params.env)); +} +command.push(bundleId); +``` +Plus near-identical JSON PID parsing at lines 250-259 and cleanup at lines 260-262. + +**Conclusion:** Confirmed — this is the clearest case of near-verbatim duplication (~40 lines of identical logic). + +### Phase 7 — Concrete Duplication: macOS Launch + +**Evidence:** + +`launch_mac_app.ts` lines 43-68: +```typescript +const command = ['open', params.appPath]; +// ... launch ... +// Bundle ID extraction via defaults read +const plistResult = await executor( + ['/bin/sh', '-c', `defaults read "${params.appPath}/Contents/Info" CFBundleIdentifier`], + 'Extract Bundle ID', false, +); +// PID lookup via pgrep +const pgrepResult = await executor(['pgrep', '-x', appName], 'Get Process ID', false); +``` + +`build_run_macos.ts` lines 160-195: +```typescript +const launchResult = await executor(['open', appPath], 'Launch macOS App', false); +// ... same bundle ID extraction ... +const plistResult = await executor( + ['/bin/sh', '-c', `defaults read "${appPath}/Contents/Info" CFBundleIdentifier`], + 'Extract Bundle ID', false, +); +// ... same pgrep PID lookup ... +const pgrepResult = await executor(['pgrep', '-x', appName], 'Get Process ID', false); +``` + +**Conclusion:** Confirmed — same three-step pattern (open, defaults read, pgrep) duplicated. + +### Phase 8 — Existing Good Pattern: `handleTestLogic` + +The test tools (`test_sim.ts`, `test_device.ts`, `test_macos.ts`) demonstrate the better pattern already present in the codebase. + +`test_sim.ts` line 139: +```typescript +return handleTestLogic({ ...params, platform: inferred.platform }, executor, { + preflight: preflight ?? undefined, + toolName: 'test_sim', +}); +``` + +`handleTestLogic` lives in `src/utils/test-common.ts` (exported via `src/utils/test/index.ts`) and is shared across all three test tool handlers. Each tool does thin validation/platform inference, then delegates to the shared logic. + +**Conclusion:** The codebase already has a proven pattern for shared workflow logic. The build-run tools haven't adopted it yet. + +## What Is NOT Duplicated (Shared Utilities) + +To be fair, the orchestrators already share significant infrastructure: + +- `executeXcodeBuildCommand` — build command construction and execution +- `resolveAppPathFromBuildSettings` — app path resolution from xcodebuild settings +- `startBuildPipeline` / `createPendingXcodebuildResponse` — pipeline lifecycle +- `createBuildRunResultEvents` / `emitPipelineNotice` / `emitPipelineError` — structured events +- `extractBundleIdFromAppPath` — bundle ID extraction +- `inferPlatform` — simulator platform inference +- `determineSimulatorUuid` — simulator UUID resolution + +The duplication is specifically at the **step execution layer**: boot, install, launch commands and their response handling. + +## Root Cause + +The orchestrators were written as self-contained end-to-end workflows. The step tools were written as separate user-facing handlers. Neither calls the other. Both construct the same underlying commands independently. + +This is a classic "convenience wrapper vs granular API" problem — the orchestrators were likely written first (or in parallel) without extracting the step logic into reusable internal primitives. + +## Recommendations + +### Recommended Approach: Extract Internal Step Primitives + +Create pure internal helper functions (not tool handlers) that encapsulate each step's command construction, execution, and result parsing. Both orchestrators and step tools would then call these. + +**1. Simulator steps** — new file `src/utils/simulator-steps.ts`: +```typescript +export async function bootSimulatorIfNeeded(simulatorId: string, executor: CommandExecutor): Promise +export async function installAppOnSimulator(simulatorId: string, appPath: string, executor: CommandExecutor): Promise +export async function launchSimulatorApp(simulatorId: string, bundleId: string, executor: CommandExecutor): Promise +``` + +**2. Device steps** — new file `src/utils/device-steps.ts`: +```typescript +export async function installAppOnDevice(deviceId: string, appPath: string, executor: CommandExecutor): Promise +export async function launchAppOnDevice(deviceId: string, bundleId: string, env?: Record, fs?: FileSystemExecutor): Promise +``` + +**3. macOS steps** — new file `src/utils/macos-steps.ts`: +```typescript +export async function launchMacApp(appPath: string, args?: string[], executor: CommandExecutor): Promise +``` + +Then refactor: +- `build_run_sim.ts` → calls `bootSimulatorIfNeeded()`, `installAppOnSimulator()`, `launchSimulatorApp()` +- `boot_sim.ts` → calls `bootSimulatorIfNeeded()` (or just `bootSimulator()`) +- `install_app_sim.ts` → calls `installAppOnSimulator()` +- `launch_app_sim.ts` → calls `launchSimulatorApp()` +- Same pattern for device and macOS tools + +### Why NOT "Tool Calls Tool" + +The tool handlers mix validation, session-default handling, response formatting, and next-step metadata. Making orchestrators call step-tool handlers would be clumsy because: +- Tool handlers return `ToolResponse` with formatted events — the orchestrator would need to unwrap and re-wrap +- Schema validation would run redundantly +- Error handling and pipeline eventing would conflict + +Internal primitives that return simple result types are the clean separation. + +### Alternative: `handleBuildRunLogic` (Like `handleTestLogic`) + +A more aggressive refactor would extract a single `handleBuildRunLogic` shared function (analogous to `handleTestLogic` for tests) that all three orchestrators delegate to. This would require parameterizing the platform-specific steps (boot/install/launch) but could eliminate even more duplication in the build → resolve-path → run pipeline. + +## Preventive Measures + +- When adding new multi-step workflow tools, extract step logic into `src/utils/*-steps.ts` first, then compose in both the orchestrator and the individual step-tool handlers +- Consider adding a lint rule or code review checklist item: "Does this tool duplicate command logic from another tool?" +- The `handleTestLogic` pattern is the gold standard in this codebase — reference it when designing new shared workflows + +## Estimated Impact + +| File | Current Lines | Estimated Reduction | +|---|---|---| +| `build_run_sim.ts` | 549 | ~120-150 lines (boot/install/launch/state-check blocks) | +| `build_run_device.ts` | 357 | ~60-80 lines (install/launch blocks) | +| `build_run_macos.ts` | 242 | ~30-40 lines (launch/bundleid/pid blocks) | +| Step tools (6 files) | ~705 total | ~50-80 lines (delegating to shared primitives) | + +Total: ~260-350 lines of duplicated logic consolidated into ~100-150 lines of shared step primitives. diff --git a/local-research/pipeline-coupling-audit.md b/local-research/pipeline-coupling-audit.md new file mode 100644 index 00000000..d3dfdde1 --- /dev/null +++ b/local-research/pipeline-coupling-audit.md @@ -0,0 +1,121 @@ +# Investigation: xcodebuild-pipeline.ts coupling audit + +## Summary + +The claim that `xcodebuild-pipeline.ts` should be split into a generic `ToolOutputPipeline` and an xcodebuild-specific event parser is **partially valid in diagnosis but wrong in prescription**. The architecture is already split at the correct seam — `toolResponse()` serves as the generic event rendering path (212 call sites), while `xcodebuild-pipeline.ts` is a purpose-built streaming build/test parser (19 call sites). No non-build tool needs a generic streaming pipeline. The real issues are naming and type-boundary clarity, not missing infrastructure. + +## Symptoms / Original Claim + +> "xcodebuild-pipeline.ts is coupled to xcodebuild - The pipeline should be split into a generic ToolOutputPipeline (events + renderers) and an xcodebuild-specific event parser, so non-build tools can use the same rendering." + +## Investigation Log + +### Phase 1 — Identifying the coupling + +**Hypothesis:** The pipeline is tightly coupled to xcodebuild specifics. + +**Findings:** Confirmed. Six concrete coupling points: + +1. **API shape** — `createXcodebuildPipeline()` (`xcodebuild-pipeline.ts:168`) takes `operation: XcodebuildOperation` (`'BUILD' | 'TEST'`) and `minimumStage?: XcodebuildStage` as required params. + +2. **Hard-wired components** — The factory always creates `createXcodebuildEventParser()` (line 179) and `createXcodebuildRunState()` (line 173). No way to inject alternative parsers or state managers. + +3. **Build-specific finalization** — `finalize()` (lines 194–244) flushes the xcodebuild parser, injects build log file refs via `injectBuildLogIntoTailEvents()`, emits parser debug warnings, and exposes `xcresultPath`. + +4. **Build-specific header builder** — `startBuildPipeline()` (lines 155–166) and `buildHeaderParams()` (lines 104–139) know about Scheme, Workspace, Project, Simulator, Device, Architecture, xcresult, etc. + +5. **Renderer naming** — The renderer interface is `XcodebuildRenderer` (`renderers/index.ts:8`) despite consuming generic `PipelineEvent`s that all tools use. + +6. **Mixed event union** — `pipeline-events.ts` defines generic canonical events (lines 27–86: `header`, `status-line`, `summary`, `section`, `detail-tree`, `table`, `file-ref`, `next-steps`) alongside xcodebuild-specific events (lines 88–148: `build-stage`, `compiler-warning`, `compiler-error`, `test-discovery`, `test-progress`, `test-failure`) in a single union with no type-level boundary. + +**Evidence:** All line numbers verified by direct file reads. + +**Conclusion:** Coupling is real and confirmed. + +### Phase 2 — Does a generic layer already exist? + +**Hypothesis:** The codebase already has generic rendering infrastructure that non-build tools use. + +**Findings:** Confirmed. The generic layer is `toolResponse()` + `tool-event-builders.ts`: + +1. **`toolResponse()`** (`tool-response.ts:11–39`) implements the exact pattern a generic pipeline would: resolve renderers → fan out events → finalize → collect MCP content. It handles all event types including xcodebuild-specific ones. + +2. **`tool-event-builders.ts`** (88 lines) builds only generic canonical events: `header`, `section`, `statusLine`, `fileRef`, `table`, `detailTree`, `nextSteps`. + +3. **Usage ratio** — In `src/mcp/tools/`: **212 calls to `toolResponse()`** vs **19 references to pipeline functions**. The overwhelming majority of tools already use the generic path. + +4. **Non-build tool patterns** — Tools like `debug_attach_sim.ts` and `start_device_log_cap.ts` build static event arrays and call `toolResponse()`. Even `start_device_log_cap`, which manages a long-running subprocess, handles its own output buffering without needing streaming pipeline infrastructure. + +**Conclusion:** The generic rendering layer exists and is the dominant pattern. + +### Phase 3 — Would non-build tools benefit from a generic streaming pipeline? + +**Hypothesis:** Non-build tools could benefit from a `ToolOutputPipeline`. + +**Findings:** No current evidence of need: + +1. **Zero non-build tools** use the streaming pipeline. +2. **No non-build tool** requires parser/state/stage tracking. +3. **`start_device_log_cap.ts`** is the closest candidate (long-running subprocess with stdout/stderr handling), but it manages output via direct stream handlers and log files — it does not need event parsing, stage progression, or summary synthesis. +4. The pipeline is also used by `swift_package_build.ts`, `swift_package_run.ts`, and `swift_package_test.ts` — but these are effectively build/test tools that happen to use `swift` CLI instead of `xcodebuild`. They reuse the xcodebuild parser opportunistically since the output formats overlap (compiler diagnostics, test results, etc.). + +**Conclusion:** No current consumer pressure for a generic streaming pipeline. The pipeline's scope is build/test toolchain output, not arbitrary subprocess streaming. + +## Root Cause + +The claim conflates two separate concerns: + +1. **"Non-build tools can't use the same rendering"** — This is false. They already do, via `toolResponse()` which uses the same renderer registry and event formatting as the pipeline. + +2. **"The pipeline should be generic"** — This would be premature abstraction. The pipeline's value is specifically in its xcodebuild/swift-toolchain parsing, state tracking, diagnostic dedup, and build log management. Making it generic would strip out its useful specificity without gaining any consumers. + +The real issues are cosmetic/type-level: +- `XcodebuildRenderer` is misnamed (it handles all event types) +- `PipelineEvent` mixes generic and domain-specific types without a type boundary +- The pipeline name slightly understates its actual scope (it handles `swift build/test/run` too, not just `xcodebuild`) + +## Recommendations + +### Do now (low-effort, high-clarity) + +1. **Rename `XcodebuildRenderer` → `PipelineRenderer`** in `src/utils/renderers/index.ts:8` and all references. This interface consumes generic `PipelineEvent`s and is used by both the pipeline and `toolResponse()`. + +2. **Split event types at the type level** in `src/types/pipeline-events.ts`: + ```typescript + // Generic events usable by any tool + type CommonPipelineEvent = + | HeaderEvent | StatusLineEvent | SummaryEvent | SectionEvent + | DetailTreeEvent | TableEvent | FileRefEvent | NextStepsEvent; + + // Build/test-specific events + type BuildTestPipelineEvent = + | BuildStageEvent | CompilerWarningEvent | CompilerErrorEvent + | TestDiscoveryEvent | TestProgressEvent | TestFailureEvent; + + // Full union (backward compatible) + type PipelineEvent = CommonPipelineEvent | BuildTestPipelineEvent; + ``` + This makes the boundary explicit without breaking any runtime code. + +### Maybe do later (if duplication grows) + +3. **Extract a tiny render-session helper** from the duplicated pattern between `toolResponse()` and `createXcodebuildPipeline()`: + - Both call `resolveRenderers()` + - Both fan out events to renderers + - Both call `renderer.finalize()` + - Both collect `mcpRenderer.getContent()` + + A ~20-line helper could eliminate this duplication if more entry points emerge. + +4. **Consider renaming the pipeline** to `BuildOutputPipeline` or `BuildTestPipeline` to reflect that it handles `swift build/test/run` output too, not just `xcodebuild`. + +### Do not do + +5. **Do not build a generic `ToolOutputPipeline`** — there are zero consumers that need it. The `toolResponse()` function already serves the generic use case. + +6. **Do not split parser/state/finalize into abstract interfaces** — there is only one implementation and no foreseeable second one. + +## Preventive Measures + +- When adding new streaming subprocess tools, evaluate whether they need the build pipeline's features (stage tracking, diagnostic dedup, summary synthesis). If not, `toolResponse()` with event builders is sufficient. +- If a second streaming parser ever emerges, *that* is the time to extract common infrastructure from the pipeline. diff --git a/local-research/rendering-pipeline-remaining-cleanup.md b/local-research/rendering-pipeline-remaining-cleanup.md new file mode 100644 index 00000000..a0c88f2e --- /dev/null +++ b/local-research/rendering-pipeline-remaining-cleanup.md @@ -0,0 +1,36 @@ +# Rendering Pipeline Refactor — Remaining Cleanup + +## Completed +- Render session module (src/rendering/) +- ToolHandlerContext via AsyncLocalStorage +- All 77 tool handlers emit via ctx +- Factory dual-mode (void → session, ToolResponse → passthrough) +- Pipeline inline finalization (pending pattern eliminated) +- CLI boundary re-renders via CLI text renderer +- MCP boundary creates session +- Snapshot normalizer stabilized for doctor output + +## Remaining Cleanup (technical debt) + +### 1. Remove hybrid toolResponse() usage from migrated tools +~40 tool handlers still call `toolResponse()` inside `withErrorHandling` mapError callbacks +or inner async functions, then extract events from `_meta.events` to re-emit through ctx. +These should be fully converted to direct ctx.emit() calls. + +### 2. Remove ToolResponse type from tool handler signatures +Once hybrid usage is removed, the `Promise` return types +can become `Promise` and the ToolResponse import can be removed. + +### 3. Daemon protocol v2 +Send `{ events, attachments, isError }` over the wire instead of ToolResponse. +CLI renders locally. Requires protocol version bump. + +### 4. Delete dead renderers +Once toolResponse() is removed from all tool handlers and the pipeline +no longer uses resolveRenderers() fallback: +- Delete src/utils/renderers/cli-jsonl-renderer.ts (used only by resolveRenderers) +- Potentially simplify renderers/index.ts + +### 5. Encapsulate ToolResponse +Move ToolResponse type out of common.ts, make it module-private to the MCP +boundary (tool-registry.ts) and the daemon protocol. diff --git a/local-research/xcodebuild-command-builder-investigation.md b/local-research/xcodebuild-command-builder-investigation.md new file mode 100644 index 00000000..05d92602 --- /dev/null +++ b/local-research/xcodebuild-command-builder-investigation.md @@ -0,0 +1,157 @@ +# Investigation: XcodebuildCommandBuilder Claim + +## Summary + +The claim that "xcodebuild command construction argument building is scattered across tools" and that "a XcodebuildCommandBuilder with a fluent API would centralize this" is **partially true but overstated**. The core build/test path is already well centralized via `executeXcodeBuildCommand`. The real duplication is limited to a few `-showBuildSettings` query tools. A fluent builder would be over-engineering — targeted consolidation of the `get_*_app_path` tools is the right fix. + +## Symptoms Under Investigation + +- Claim: xcodebuild argument building is scattered across tools +- Claim: A XcodebuildCommandBuilder with a fluent API would centralize this + +## Investigation Log + +### Phase 1 — Quantifying the Scope + +**Hypothesis:** xcodebuild command construction exists in many files across the codebase. + +**Findings:** 456 matches for "xcodebuild" across `src/` (excluding tests). However, many are imports, log messages, and type references — not command construction. + +**Actual command construction sites (files that build `xcodebuild` argument arrays):** + +| File | Command Type | Centralized? | +|------|-------------|--------------| +| `src/utils/build-utils.ts:82-171` | build/test/build-for-testing/test-without-building | Yes — this IS the center | +| `src/utils/app-path-resolver.ts:63-93` | -showBuildSettings (app path lookup) | Yes — secondary center | +| `src/mcp/tools/simulator/get_sim_app_path.ts:143-156` | -showBuildSettings | No — inline duplicate | +| `src/mcp/tools/device/get_device_app_path.ts:102-116` | -showBuildSettings | No — inline duplicate | +| `src/mcp/tools/macos/get_mac_app_path.ts:94-112` | -showBuildSettings | No — inline duplicate | +| `src/mcp/tools/utilities/clean.ts:127-153` | clean action | No — inline (partially justified) | +| `src/mcp/tools/project-discovery/list_schemes.ts:52-55` | -list | No — inline (justified) | +| `src/mcp/tools/project-discovery/show_build_settings.ts:69-74` | -showBuildSettings | No — inline (justified) | +| `src/utils/platform-detection.ts:63-79` | -showBuildSettings (platform inference) | No — inline (justified) | +| `src/utils/xcode-state-watcher.ts:53-60` | -showBuildSettings -skipPackageUpdates | No — inline (justified) | +| `src/utils/sentry.ts` | -version | Peripheral diagnostic | +| `src/mcp/tools/doctor/lib/doctor.deps.ts:152` | -version | Peripheral diagnostic | + +**Conclusion:** 12 sites total. 2 are centralized. 3 are clear duplicates. 5 are local-but-justified. 2 are peripheral. + +### Phase 2 — Evaluating Existing Centralization + +**Hypothesis:** `executeXcodeBuildCommand` already centralizes the most important path. + +**Evidence:** + +`executeXcodeBuildCommand` (build-utils.ts:29-261) handles: +- Project/workspace selection with path resolution (lines 82-92) +- Scheme, configuration, `-skipMacroValidation` (lines 94-96) +- Full destination logic for all platforms: simulator by ID/name, macOS with arch, device by ID, generic (lines 98-134) +- Test-specific flags: `COMPILER_INDEX_STORE_ENABLE`, `ONLY_ACTIVE_ARCH`, `-packageCachePath` (lines 141-149) +- derivedDataPath, extraArgs (lines 151-157) +- Build action appended last (line 159) +- xcodemake fallback logic (lines 162-190) +- cwd set to project directory (line 194) + +**Callers (6 build/test tools) do NO argument construction** — they pass `SharedBuildParams` + `PlatformBuildOptions` objects and `executeXcodeBuildCommand` handles everything. Example from `build_sim.ts`: + +```typescript +const sharedBuildParams = { ...params, configuration }; +const platformOptions = { platform: detectedPlatform, simulatorName, simulatorId, useLatestOS, logPrefix }; +const buildResult = await executeXcodeBuildCommand(sharedBuildParams, platformOptions, ...); +``` + +**Conclusion: Confirmed.** The highest-volume, most important xcodebuild construction path is already centralized correctly. + +`resolveAppPathFromBuildSettings` (app-path-resolver.ts:60-100) is a secondary center for `-showBuildSettings` queries used by build-run flows. It handles project/workspace, scheme, config, destination, derivedDataPath, extraArgs, cwd — essentially the same shared arg pattern. + +### Phase 3 — The Real Duplication: `get_*_app_path` Tools + +**Hypothesis:** The three `get_*_app_path` tools duplicate `resolveAppPathFromBuildSettings`. + +**Evidence — behavioral drift across the three tools:** + +| Behavior | `get_sim_app_path.ts` | `get_device_app_path.ts` | `get_mac_app_path.ts` | `resolveAppPathFromBuildSettings` | +|----------|----------------------|-------------------------|-----------------------|----------------------------------| +| Resolves paths to absolute | No | Yes (line 103-106) | No | Yes | +| Sets cwd | No | Yes (line 118-121) | No | Yes | +| Always adds -destination | Yes | Yes | Only when arch provided | Yes | +| Handles derivedDataPath | No | No | Yes (line 104-106) | Yes | +| Handles extraArgs | No | No | Yes (line 108-110) | Yes | + +This drift is the strongest evidence that the duplication is harmful — the tools have silently diverged in path resolution and cwd handling. `get_sim_app_path.ts` doesn't resolve relative paths or set cwd, while `get_device_app_path.ts` does. This is almost certainly unintentional. + +All three could delegate to `resolveAppPathFromBuildSettings` (or a slightly extended version) instead of inline construction. + +### Phase 4 — Adjacent Duplication: `clean.ts` + +**Hypothesis:** `clean.ts` duplicates `executeXcodeBuildCommand`. + +**Evidence:** `clean.ts` (lines 127-153) builds: +- project/workspace with path resolution +- scheme, configuration +- destination via `constructDestinationString` +- derivedDataPath, extraArgs +- `clean` action + +This overlaps ~80% with `executeXcodeBuildCommand`. However, `executeXcodeBuildCommand` includes xcodemake logic, test-specific flags, and build pipeline integration that `clean` should NOT inherit. + +**Notable issue:** `clean.ts` line 115: `const scheme = params.scheme ?? '';` followed by `command.push('-scheme', scheme)` — this can emit `-scheme ""` which is suboptimal. A shared helper would prevent this kind of drift. + +**Conclusion:** Merging into `executeXcodeBuildCommand` would be wrong. But extracting a small shared helper for the common "resolve paths + append project/workspace/scheme/config/destination/derivedData/extraArgs" pattern would reduce this risk. + +### Phase 5 — Intentionally Local Builders + +**Hypothesis:** Discovery/inspection commands are local for good reasons. + +**Evidence:** +- `list_schemes.ts`: Only needs `-list` + project/workspace (2 args). Minimal surface. +- `show_build_settings.ts`: Only needs `-showBuildSettings` + project/workspace + scheme (3 args). Minimal surface. +- `platform-detection.ts`: Needs `-showBuildSettings -scheme` + project/workspace, but arg ORDER differs (scheme before project). Intentional for specific parsing needs. +- `xcode-state-watcher.ts`: Needs `-showBuildSettings -scheme -skipPackageUpdates` + optional project/workspace. The `-skipPackageUpdates` is unique to this use case. + +**Conclusion:** These are different enough in semantics that forcing them through a universal builder would add complexity without reducing bugs. The shared surface (project/workspace toggle) is 2-4 lines — not worth abstracting. + +## Root Cause Analysis + +The claim is **partially valid but the proposed solution is wrong**. + +**What's true:** +- 3 `get_*_app_path` tools duplicate `-showBuildSettings` arg construction that already exists in `resolveAppPathFromBuildSettings` +- This duplication has caused behavioral drift (path resolution, cwd handling) +- `clean.ts` shares ~80% of its arg construction with `executeXcodeBuildCommand` + +**What's overstated:** +- The core build/test path (6 callers) is already centralized in `executeXcodeBuildCommand` +- Discovery/inspection tools are intentionally local with minimal shared surface +- Peripheral `-version` checks are trivial + +**What's wrong about the proposed fix:** +- A `XcodebuildCommandBuilder` with a fluent API would need to handle: build, test, build-for-testing, test-without-building, clean, -showBuildSettings, -list, -version, xcodemake fallback, test-specific flags, pipeline integration — all of which have different requirements +- This would create a god-object that's harder to understand than the current focused abstractions +- The current architecture of `executeXcodeBuildCommand` (action center) + `resolveAppPathFromBuildSettings` (query center) is a better decomposition + +## Recommendations + +### 1. Consolidate `get_*_app_path` tools onto `resolveAppPathFromBuildSettings` (HIGH VALUE) +- `get_sim_app_path.ts`, `get_device_app_path.ts`, `get_mac_app_path.ts` should delegate command construction to `resolveAppPathFromBuildSettings` or a slight extension of it +- This fixes the behavioral drift (path resolution, cwd) and removes ~60 lines of duplicated arg construction +- May need to extend `resolveAppPathFromBuildSettings` to support simulator destination strings (currently only handles generic/device destinations) + +### 2. Optionally extract a tiny shared helper for common args (LOW-MEDIUM VALUE) +A small function like: +```typescript +function resolveXcodebuildPaths(params: { projectPath?: string; workspacePath?: string }) { + // resolve to absolute, return { projectPath, workspacePath, projectDir } +} +``` +This could be reused by `clean.ts` and `resolveAppPathFromBuildSettings` to reduce the path resolution duplication. But this is minor — only worth doing if you're already touching these files. + +### 3. Do NOT build a XcodebuildCommandBuilder (RECOMMENDATION: SKIP) +- The current architecture is already well-decomposed +- A fluent builder would be over-engineering for the actual duplication that exists +- The fix is consolidation of 3 tools onto an existing abstraction, not a new abstraction + +## Preventive Measures + +- When adding new tools that run `xcodebuild -showBuildSettings`, check if `resolveAppPathFromBuildSettings` can be reused first +- The existing `SharedBuildParams` + `PlatformBuildOptions` type pattern works well — continue using it for new build actions diff --git a/manifests/resources/devices.yaml b/manifests/resources/devices.yaml new file mode 100644 index 00000000..0f52840c --- /dev/null +++ b/manifests/resources/devices.yaml @@ -0,0 +1,6 @@ +id: devices +module: mcp/resources/devices +name: devices +uri: xcodebuildmcp://devices +description: Connected physical Apple devices with their UUIDs, names, and connection status +mimeType: text/plain diff --git a/manifests/resources/doctor.yaml b/manifests/resources/doctor.yaml new file mode 100644 index 00000000..08c01bd4 --- /dev/null +++ b/manifests/resources/doctor.yaml @@ -0,0 +1,6 @@ +id: doctor +module: mcp/resources/doctor +name: doctor +uri: xcodebuildmcp://doctor +description: Comprehensive development environment diagnostic information and configuration status +mimeType: text/plain diff --git a/manifests/resources/session-status.yaml b/manifests/resources/session-status.yaml new file mode 100644 index 00000000..8f77bc18 --- /dev/null +++ b/manifests/resources/session-status.yaml @@ -0,0 +1,6 @@ +id: session-status +module: mcp/resources/session-status +name: session-status +uri: xcodebuildmcp://session-status +description: Runtime session state for log capture and debugging +mimeType: application/json diff --git a/manifests/resources/simulators.yaml b/manifests/resources/simulators.yaml new file mode 100644 index 00000000..9ab4dc02 --- /dev/null +++ b/manifests/resources/simulators.yaml @@ -0,0 +1,6 @@ +id: simulators +module: mcp/resources/simulators +name: simulators +uri: xcodebuildmcp://simulators +description: Available iOS simulators with their UUIDs and states +mimeType: text/plain diff --git a/manifests/resources/xcode-ide-state.yaml b/manifests/resources/xcode-ide-state.yaml new file mode 100644 index 00000000..75295bfd --- /dev/null +++ b/manifests/resources/xcode-ide-state.yaml @@ -0,0 +1,8 @@ +id: xcode-ide-state +module: mcp/resources/xcode-ide-state +name: xcode-ide-state +uri: xcodebuildmcp://xcode-ide-state +description: "Current Xcode IDE selection (scheme and simulator) from Xcode's UI state" +mimeType: application/json +predicates: + - runningUnderXcodeAgent diff --git a/manifests/tools/boot_sim.yaml b/manifests/tools/boot_sim.yaml index d43d90ce..7099c068 100644 --- a/manifests/tools/boot_sim.yaml +++ b/manifests/tools/boot_sim.yaml @@ -13,15 +13,18 @@ nextSteps: - label: Open the Simulator app (makes it visible) toolId: open_sim priority: 1 + when: success - label: Install an app toolId: install_app_sim params: simulatorId: SIMULATOR_UUID appPath: PATH_TO_YOUR_APP priority: 2 + when: success - label: Launch an app toolId: launch_app_sim params: simulatorId: SIMULATOR_UUID bundleId: YOUR_APP_BUNDLE_ID priority: 3 + when: success diff --git a/manifests/tools/build_device.yaml b/manifests/tools/build_device.yaml index 40e0506b..38633e37 100644 --- a/manifests/tools/build_device.yaml +++ b/manifests/tools/build_device.yaml @@ -11,3 +11,8 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built device app path + toolId: get_device_app_path + priority: 1 + when: success diff --git a/manifests/tools/build_macos.yaml b/manifests/tools/build_macos.yaml index fc2d8126..71cacfa6 100644 --- a/manifests/tools/build_macos.yaml +++ b/manifests/tools/build_macos.yaml @@ -11,3 +11,8 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built macOS app path + toolId: get_mac_app_path + priority: 1 + when: success diff --git a/manifests/tools/build_run_device.yaml b/manifests/tools/build_run_device.yaml index 4c9ecd4f..3a112eac 100644 --- a/manifests/tools/build_run_device.yaml +++ b/manifests/tools/build_run_device.yaml @@ -3,7 +3,7 @@ module: mcp/tools/device/build_run_device names: mcp: build_run_device cli: build-and-run -description: Build, install, and launch on physical device. Preferred single-step run tool when defaults are set. +description: Build, install, and launch on physical device. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. predicates: - hideWhenXcodeAgentMode annotations: @@ -15,3 +15,4 @@ nextSteps: - label: Stop app on device toolId: stop_app_device priority: 1 + when: success diff --git a/manifests/tools/build_run_macos.yaml b/manifests/tools/build_run_macos.yaml index 932b81f4..4c4c7672 100644 --- a/manifests/tools/build_run_macos.yaml +++ b/manifests/tools/build_run_macos.yaml @@ -11,3 +11,7 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Interact with the launched app in the foreground + priority: 1 + when: success diff --git a/manifests/tools/build_run_sim.yaml b/manifests/tools/build_run_sim.yaml index f53938e1..57bef390 100644 --- a/manifests/tools/build_run_sim.yaml +++ b/manifests/tools/build_run_sim.yaml @@ -3,7 +3,7 @@ module: mcp/tools/simulator/build_run_sim names: mcp: build_run_sim cli: build-and-run -description: Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Preferred single-step run tool when defaults are set. +description: Build, install, and launch on iOS Simulator; boots simulator and attempts to open Simulator.app as needed. Runtime logs are captured automatically and the log file path is included in the response. Preferred single-step run tool when defaults are set. predicates: - hideWhenXcodeAgentMode annotations: @@ -15,3 +15,4 @@ nextSteps: - label: Stop app in simulator toolId: stop_app_sim priority: 1 + when: success diff --git a/manifests/tools/build_sim.yaml b/manifests/tools/build_sim.yaml index 0a83cf95..62ac7947 100644 --- a/manifests/tools/build_sim.yaml +++ b/manifests/tools/build_sim.yaml @@ -11,3 +11,8 @@ annotations: readOnlyHint: false destructiveHint: false openWorldHint: false +nextSteps: + - label: Get built app path in simulator derived data + toolId: get_sim_app_path + priority: 1 + when: success diff --git a/manifests/tools/debug_attach_sim.yaml b/manifests/tools/debug_attach_sim.yaml index 4b93de09..7e284243 100644 --- a/manifests/tools/debug_attach_sim.yaml +++ b/manifests/tools/debug_attach_sim.yaml @@ -15,9 +15,12 @@ nextSteps: - label: Add a breakpoint toolId: debug_breakpoint_add priority: 1 + when: success - label: Continue execution toolId: debug_continue priority: 2 + when: success - label: Show call stack toolId: debug_stack priority: 3 + when: success diff --git a/manifests/tools/discover_projs.yaml b/manifests/tools/discover_projs.yaml index 735af420..c152a9d0 100644 --- a/manifests/tools/discover_projs.yaml +++ b/manifests/tools/discover_projs.yaml @@ -13,6 +13,8 @@ nextSteps: - label: Save discovered project/workspace as session defaults toolId: session_set_defaults priority: 1 + when: success - label: Build and run once defaults are set toolId: build_run_sim priority: 2 + when: success diff --git a/manifests/tools/get_app_bundle_id.yaml b/manifests/tools/get_app_bundle_id.yaml index 2bbe327b..66e397fb 100644 --- a/manifests/tools/get_app_bundle_id.yaml +++ b/manifests/tools/get_app_bundle_id.yaml @@ -13,12 +13,16 @@ nextSteps: - label: Install on simulator toolId: install_app_sim priority: 1 + when: success - label: Launch on simulator toolId: launch_app_sim priority: 2 + when: success - label: Install on device toolId: install_app_device priority: 3 + when: success - label: Launch on device toolId: launch_app_device priority: 4 + when: success diff --git a/manifests/tools/get_coverage_report.yaml b/manifests/tools/get_coverage_report.yaml index 6eadcec0..4c5674eb 100644 --- a/manifests/tools/get_coverage_report.yaml +++ b/manifests/tools/get_coverage_report.yaml @@ -13,3 +13,4 @@ nextSteps: - label: View file-level coverage toolId: get_file_coverage priority: 1 + when: success diff --git a/manifests/tools/get_device_app_path.yaml b/manifests/tools/get_device_app_path.yaml index 8d786924..80a86319 100644 --- a/manifests/tools/get_device_app_path.yaml +++ b/manifests/tools/get_device_app_path.yaml @@ -13,9 +13,12 @@ nextSteps: - label: Get bundle ID toolId: get_app_bundle_id priority: 1 + when: success - label: Install app on device toolId: install_app_device priority: 2 + when: success - label: Launch app on device toolId: launch_app_device priority: 3 + when: success diff --git a/manifests/tools/get_file_coverage.yaml b/manifests/tools/get_file_coverage.yaml index 90ce1cfc..1fe98892 100644 --- a/manifests/tools/get_file_coverage.yaml +++ b/manifests/tools/get_file_coverage.yaml @@ -13,3 +13,4 @@ nextSteps: - label: View overall coverage toolId: get_coverage_report priority: 1 + when: success diff --git a/manifests/tools/get_mac_app_path.yaml b/manifests/tools/get_mac_app_path.yaml index 7599a4c1..285a8e27 100644 --- a/manifests/tools/get_mac_app_path.yaml +++ b/manifests/tools/get_mac_app_path.yaml @@ -13,6 +13,8 @@ nextSteps: - label: Get bundle ID toolId: get_mac_bundle_id priority: 1 + when: success - label: Launch app toolId: launch_mac_app priority: 2 + when: success diff --git a/manifests/tools/get_mac_bundle_id.yaml b/manifests/tools/get_mac_bundle_id.yaml index f9684734..9853ad78 100644 --- a/manifests/tools/get_mac_bundle_id.yaml +++ b/manifests/tools/get_mac_bundle_id.yaml @@ -13,6 +13,8 @@ nextSteps: - label: Launch the app toolId: launch_mac_app priority: 1 + when: success - label: Build again toolId: build_macos priority: 2 + when: success diff --git a/manifests/tools/get_sim_app_path.yaml b/manifests/tools/get_sim_app_path.yaml index 7199eaff..b4283da0 100644 --- a/manifests/tools/get_sim_app_path.yaml +++ b/manifests/tools/get_sim_app_path.yaml @@ -13,12 +13,16 @@ nextSteps: - label: Get bundle ID toolId: get_app_bundle_id priority: 1 + when: success - label: Boot simulator toolId: boot_sim priority: 2 + when: success - label: Install app toolId: install_app_sim priority: 3 + when: success - label: Launch app toolId: launch_app_sim priority: 4 + when: success diff --git a/manifests/tools/install_app_sim.yaml b/manifests/tools/install_app_sim.yaml index 2e61517e..ac7e82f6 100644 --- a/manifests/tools/install_app_sim.yaml +++ b/manifests/tools/install_app_sim.yaml @@ -13,6 +13,8 @@ nextSteps: - label: Open the Simulator app toolId: open_sim priority: 1 + when: success - label: Launch the app toolId: launch_app_sim priority: 2 + when: success diff --git a/manifests/tools/launch_app_device.yaml b/manifests/tools/launch_app_device.yaml index ef3f123e..90563c5b 100644 --- a/manifests/tools/launch_app_device.yaml +++ b/manifests/tools/launch_app_device.yaml @@ -13,3 +13,4 @@ nextSteps: - label: Stop the app toolId: stop_app_device priority: 1 + when: success diff --git a/manifests/tools/launch_app_sim.yaml b/manifests/tools/launch_app_sim.yaml index c30e1c6c..8a3857ad 100644 --- a/manifests/tools/launch_app_sim.yaml +++ b/manifests/tools/launch_app_sim.yaml @@ -3,7 +3,7 @@ module: mcp/tools/simulator/launch_app_sim names: mcp: launch_app_sim cli: launch-app -description: Launch app on simulator. +description: Launch app on simulator. Runtime logs are captured automatically and the log file path is included in the response. annotations: title: Launch App Simulator readOnlyHint: false @@ -13,3 +13,8 @@ nextSteps: - label: Open Simulator app to see it toolId: open_sim priority: 1 + when: success + - label: Stop app in simulator + toolId: stop_app_sim + priority: 2 + when: success diff --git a/manifests/tools/list_devices.yaml b/manifests/tools/list_devices.yaml index a8db4f43..6181fab8 100644 --- a/manifests/tools/list_devices.yaml +++ b/manifests/tools/list_devices.yaml @@ -13,9 +13,12 @@ nextSteps: - label: Build for device toolId: build_device priority: 1 + when: success - label: Run tests on device toolId: test_device priority: 2 + when: success - label: Get app path toolId: get_device_app_path priority: 3 + when: success diff --git a/manifests/tools/list_schemes.yaml b/manifests/tools/list_schemes.yaml index 27dd2f10..88ace0c7 100644 --- a/manifests/tools/list_schemes.yaml +++ b/manifests/tools/list_schemes.yaml @@ -13,12 +13,16 @@ nextSteps: - label: Build for macOS toolId: build_macos priority: 1 + when: success - label: Build and run on iOS Simulator (default for run intent) toolId: build_run_sim priority: 2 + when: success - label: Build for iOS Simulator (compile-only) toolId: build_sim priority: 3 + when: success - label: Show build settings toolId: show_build_settings priority: 4 + when: success diff --git a/manifests/tools/list_sims.yaml b/manifests/tools/list_sims.yaml index a1309932..3c0137d6 100644 --- a/manifests/tools/list_sims.yaml +++ b/manifests/tools/list_sims.yaml @@ -15,15 +15,18 @@ nextSteps: params: simulatorId: UUID_FROM_ABOVE priority: 1 + when: success - label: Open the simulator UI toolId: open_sim priority: 2 + when: success - label: Build for simulator toolId: build_sim params: scheme: YOUR_SCHEME simulatorId: UUID_FROM_ABOVE priority: 3 + when: success - label: Get app path toolId: get_sim_app_path params: @@ -31,3 +34,4 @@ nextSteps: platform: iOS Simulator simulatorId: UUID_FROM_ABOVE priority: 4 + when: success diff --git a/manifests/tools/open_sim.yaml b/manifests/tools/open_sim.yaml index d707c2e2..57420b22 100644 --- a/manifests/tools/open_sim.yaml +++ b/manifests/tools/open_sim.yaml @@ -15,3 +15,4 @@ nextSteps: params: simulatorId: UUID_FROM_LIST_SIMS priority: 1 + when: success diff --git a/manifests/tools/record_sim_video.yaml b/manifests/tools/record_sim_video.yaml index da927fbc..f082885e 100644 --- a/manifests/tools/record_sim_video.yaml +++ b/manifests/tools/record_sim_video.yaml @@ -15,3 +15,4 @@ nextSteps: - label: Stop and save the recording toolId: record_sim_video priority: 1 + when: success diff --git a/manifests/tools/scaffold_ios_project.yaml b/manifests/tools/scaffold_ios_project.yaml index fd361656..03bf7dcf 100644 --- a/manifests/tools/scaffold_ios_project.yaml +++ b/manifests/tools/scaffold_ios_project.yaml @@ -13,9 +13,12 @@ annotations: openWorldHint: false nextSteps: - label: "Important: Before working on the project make sure to read the README.md file in the workspace root directory." + when: success - label: Build for simulator toolId: build_sim priority: 1 + when: success - label: Build and run on simulator toolId: build_run_sim priority: 2 + when: success diff --git a/manifests/tools/scaffold_macos_project.yaml b/manifests/tools/scaffold_macos_project.yaml index 36ad8f45..f8aedb15 100644 --- a/manifests/tools/scaffold_macos_project.yaml +++ b/manifests/tools/scaffold_macos_project.yaml @@ -13,9 +13,12 @@ annotations: openWorldHint: false nextSteps: - label: "Important: Before working on the project make sure to read the README.md file in the workspace root directory." + when: success - label: Build for macOS toolId: build_macos priority: 1 + when: success - label: Build & Run on macOS toolId: build_run_macos priority: 2 + when: success diff --git a/manifests/tools/show_build_settings.yaml b/manifests/tools/show_build_settings.yaml index c6c3620d..1b16dbc7 100644 --- a/manifests/tools/show_build_settings.yaml +++ b/manifests/tools/show_build_settings.yaml @@ -15,9 +15,12 @@ nextSteps: - label: Build for macOS toolId: build_macos priority: 1 + when: success - label: Build for iOS Simulator toolId: build_sim priority: 2 + when: success - label: List schemes toolId: list_schemes priority: 3 + when: success diff --git a/manifests/tools/snapshot_ui.yaml b/manifests/tools/snapshot_ui.yaml index cdac6fd2..2d361134 100644 --- a/manifests/tools/snapshot_ui.yaml +++ b/manifests/tools/snapshot_ui.yaml @@ -9,16 +9,19 @@ nextSteps: toolId: snapshot_ui params: simulatorId: SIMULATOR_UUID + when: success - label: Tap on element toolId: tap params: simulatorId: SIMULATOR_UUID x: 0 y: 0 + when: success - label: Take screenshot for verification toolId: screenshot params: simulatorId: SIMULATOR_UUID + when: success annotations: title: Snapshot UI readOnlyHint: true diff --git a/package-lock.json b/package-lock.json index aaef391f..3cc3bc34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,13 @@ "dependencies": { "@clack/prompts": "^1.0.1", "@modelcontextprotocol/sdk": "^1.27.1", - "@sentry/cli": "^3.1.0", "@sentry/node": "^10.43.0", "bplist-parser": "^0.3.2", "chokidar": "^5.0.0", - "glob": "^13.0.6", "uuid": "^11.1.0", "yaml": "^2.4.5", "yargs": "^17.7.2", + "yargs-parser": "^22.0.0", "zod": "^4.0.0" }, "bin": { @@ -26,28 +25,23 @@ "xcodebuildmcp-doctor": "build/doctor-cli.js" }, "devDependencies": { - "@bacons/xcode": "^1.0.0-alpha.24", - "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", "@types/chokidar": "^1.7.5", "@types/node": "^22.13.6", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^9.23.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.5", - "playwright": "^1.53.0", + "glob": "^13.0.6", + "knip": "^5.88.0", "prettier": "3.6.2", - "ts-node": "^10.9.2", "tsup": "^8.5.0", "tsx": "^4.20.4", "typescript": "^5.8.2", "typescript-eslint": "^8.28.0", - "vitest": "^3.2.4", - "xcode": "^3.0.1" + "vitest": "^3.2.4" } }, "node_modules/@ampproject/remapping": { @@ -114,28 +108,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bacons/xcode": { - "version": "1.0.0-alpha.25", - "resolved": "https://registry.npmjs.org/@bacons/xcode/-/xcode-1.0.0-alpha.25.tgz", - "integrity": "sha512-HE/2UXkIFrKq/ZvxvB8b1OIk47Nf+jXDYJsAVfSoxCu3pNW/Zrws3ad/HbB/wWYb+bDvr4PD2wfGuNcTRbUQNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@expo/plist": "^0.0.18", - "debug": "^4.3.4", - "uuid": "^8.3.2" - } - }, - "node_modules/@bacons/xcode/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -167,28 +139,41 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -761,18 +746,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@expo/plist": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.0.18.tgz", - "integrity": "sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "~0.7.0", - "base64-js": "^1.2.3", - "xmlbuilder": "^14.0.0" - } - }, "node_modules/@fastify/otel": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@fastify/otel/-/otel-0.16.0.tgz", @@ -1070,6 +1043,25 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1636,6 +1628,289 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", + "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", + "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", + "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", + "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", + "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", + "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", + "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", + "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", + "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", + "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.19.1.tgz", + "integrity": "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.19.1.tgz", + "integrity": "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.19.1.tgz", + "integrity": "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.19.1.tgz", + "integrity": "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.19.1.tgz", + "integrity": "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.19.1.tgz", + "integrity": "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.19.1.tgz", + "integrity": "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.19.1.tgz", + "integrity": "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.19.1.tgz", + "integrity": "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.19.1.tgz", + "integrity": "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2058,170 +2333,6 @@ "win32" ] }, - "node_modules/@sentry/cli": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.1.0.tgz", - "integrity": "sha512-ngnx6E8XjXpg1uzma45INfKCS8yurb/fl3cZdXTCa2wmek8b4N6WIlmOlTKFTBrV54OauF6mloJxAlpuzoQR6g==", - "hasInstallScript": true, - "license": "FSL-1.1-MIT", - "dependencies": { - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "undici": "^6.22.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "@sentry/cli-darwin": "3.1.0", - "@sentry/cli-linux-arm": "3.1.0", - "@sentry/cli-linux-arm64": "3.1.0", - "@sentry/cli-linux-i686": "3.1.0", - "@sentry/cli-linux-x64": "3.1.0", - "@sentry/cli-win32-arm64": "3.1.0", - "@sentry/cli-win32-i686": "3.1.0", - "@sentry/cli-win32-x64": "3.1.0" - } - }, - "node_modules/@sentry/cli-darwin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.1.0.tgz", - "integrity": "sha512-xT1WlCHenGGO29Lq/wKaIthdqZzNzZhlPs7dXrzlBx9DyA2Jnl0g7WEau0oWi8GyJGVRXCJMiCydR//Tb5qVwA==", - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-linux-arm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.1.0.tgz", - "integrity": "sha512-kbP3/8/Ct/Jbm569KDXbFIyMyPypIegObvIT7LdSsfdYSZdBd396GV7vUpSGKiLUVVN0xjn8OqQ48AVGfjmuMg==", - "cpu": [ - "arm" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.1.0.tgz", - "integrity": "sha512-Jm/iHLKiHxrZYlAq2tT07amiegEVCOAQT9Unilr6djjcZzS2tcI9ThSRQvjP9tFpFRKop+NyNGE3XHXf69r00g==", - "cpu": [ - "arm64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.1.0.tgz", - "integrity": "sha512-f/PK/EGK5vFOy7LC4Riwb+BEE20Nk7RbEFEMjvRq26DpETCrZYUGlbpIKvJFKOaUmr79aAkFCA/EjJiYfcQP2Q==", - "cpu": [ - "x86", - "ia32" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.1.0.tgz", - "integrity": "sha512-T+v8x1ujhixZrOrH0sVhsW6uLwK4n0WS+B+5xV46WqUKe32cbYotursp2y53ROjgat8SQDGeP/VnC0Qa3Y2fEA==", - "cpu": [ - "x64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-win32-arm64": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.1.0.tgz", - "integrity": "sha512-2DIPq6aW2DC34EDC9J0xwD+9BpFnKdFGdIcQUZMS+5pXlU6V7o8wpZxZAM8TdYNmsPkkQGKp7Dhl/arWpvNgrw==", - "cpu": [ - "arm64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.1.0.tgz", - "integrity": "sha512-2NuywEiiZn6xJ1yAV2xjv/nuHiy6kZU5XR3RSAIrPdEZD1nBoMsH/gB2FufQw58Ziz/7otFcX+vtGpJjbIT5mQ==", - "cpu": [ - "x86", - "ia32" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.1.0.tgz", - "integrity": "sha512-Ip405Yqdrr+l9TImsZOJz6c9Nb4zvXcmtOIBKLHc9cowpfXfmlqsHbDp7Xh4+k4L0uLr9i+8ilgQ6ypcuF4UCg==", - "cpu": [ - "x64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@sentry/core": { "version": "10.43.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.43.0.tgz", @@ -2357,33 +2468,16 @@ "@opentelemetry/semantic-conventions": "^1.39.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@types/chai": { "version": "5.2.2", @@ -2962,17 +3056,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", - "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", - "deprecated": "this version is no longer supported, please update to at least 0.8.*", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -3017,19 +3100,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -3121,13 +3191,6 @@ "dev": true, "license": "MIT" }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3151,38 +3214,17 @@ "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.29", - "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.29", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/big-integer": { @@ -3218,16 +3260,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/bplist-creator": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", - "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "stream-buffers": "2.2.x" - } - }, "node_modules/bplist-parser": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", @@ -3579,13 +3611,6 @@ "node": ">= 0.10" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3643,16 +3668,6 @@ "node": ">= 0.8" } }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4225,6 +4240,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -4342,6 +4367,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4453,6 +4494,7 @@ "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -4483,6 +4525,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4492,6 +4535,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4504,6 +4548,7 @@ "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.5" @@ -4830,6 +4875,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -4906,6 +4961,75 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "5.88.1", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.88.1.tgz", + "integrity": "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "^1.2.3", + "fast-glob": "^3.3.3", + "formatly": "^0.3.0", + "jiti": "^2.6.0", + "minimist": "^1.2.8", + "oxc-resolver": "^11.19.1", + "picocolors": "^1.1.1", + "picomatch": "^4.0.1", + "smol-toml": "^1.5.2", + "strip-json-comments": "5.0.3", + "unbash": "^2.2.0", + "yaml": "^2.8.2", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4 <7" + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4991,6 +5115,7 @@ "version": "11.3.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5034,13 +5159,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5129,10 +5247,21 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -5280,6 +5409,38 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-resolver": { + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", + "integrity": "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.19.1", + "@oxc-resolver/binding-android-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-x64": "11.19.1", + "@oxc-resolver/binding-freebsd-x64": "11.19.1", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", + "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-musl": "11.19.1", + "@oxc-resolver/binding-openharmony-arm64": "11.19.1", + "@oxc-resolver/binding-wasm32-wasi": "11.19.1", + "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", + "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", + "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5364,6 +5525,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -5484,73 +5646,6 @@ "pathe": "^2.0.1" } }, - "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.55.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/plist/node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/plist/node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5701,15 +5796,6 @@ "node": ">=6.0.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5723,12 +5809,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6154,31 +6234,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-plist": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", - "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bplist-creator": "0.1.0", - "bplist-parser": "0.3.1", - "plist": "^3.0.5" - } - }, - "node_modules/simple-plist/node_modules/bplist-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", - "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "big-integer": "1.6.x" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/sirv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", @@ -6200,6 +6255,19 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -6276,16 +6344,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stream-buffers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -6817,49 +6875,13 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } + "license": "0BSD", + "optional": true }, "node_modules/tsup": { "version": "8.5.0", @@ -7061,13 +7083,14 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", - "license": "MIT", + "node_modules/unbash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-2.2.0.tgz", + "integrity": "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=18.17" + "node": ">=14" } }, "node_modules/undici-types": { @@ -7108,13 +7131,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7354,6 +7370,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7497,40 +7523,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/xcode": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", - "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "simple-plist": "^1.1.0", - "uuid": "^7.0.3" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/xcode/node_modules/uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/xmlbuilder": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", - "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -7583,12 +7575,12 @@ } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "license": "ISC", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs/node_modules/ansi-regex": { @@ -7632,14 +7624,13 @@ "node": ">=8" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=12" } }, "node_modules/yocto-queue": { diff --git a/package.json b/package.json index 97d5d190..b0c8159e 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,14 @@ "docs:update": "npx tsx scripts/update-tools-docs.ts", "docs:update:dry-run": "npx tsx scripts/update-tools-docs.ts --dry-run --verbose", "docs:check": "node scripts/check-docs-cli-commands.js", + "bench:test-sim": "npx tsx scripts/benchmark-simulator-test.ts", + "capture:xcodebuild": "npx tsx scripts/capture-xcodebuild-wrapper.ts", "license:report": "node scripts/generate-third-party-package-licenses.mjs", "license:check": "npx -y license-checker --production --onlyAllow 'MIT;ISC;BSD-2-Clause;BSD-3-Clause;Apache-2.0;Unlicense;FSL-1.1-MIT'", + "knip": "knip", "test": "vitest run", + "test:snapshot": "npm run build && node build/cli.js daemon stop 2>/dev/null; vitest run --config vitest.snapshot.config.ts", + "test:snapshot:update": "npm run build && node build/cli.js daemon stop 2>/dev/null; UPDATE_SNAPSHOTS=1 vitest run --config vitest.snapshot.config.ts", "test:smoke": "npm run build && vitest run --config vitest.smoke.config.ts", "test:watch": "vitest", "test:ui": "vitest --ui", @@ -76,7 +81,6 @@ "dependencies": { "@clack/prompts": "^1.0.1", "@modelcontextprotocol/sdk": "^1.27.1", - "@sentry/cli": "^3.1.0", "@sentry/node": "^10.43.0", "bplist-parser": "^0.3.2", "chokidar": "^5.0.0", @@ -84,30 +88,26 @@ "uuid": "^11.1.0", "yaml": "^2.4.5", "yargs": "^17.7.2", + "yargs-parser": "^22.0.0", "zod": "^4.0.0" }, "devDependencies": { - "@bacons/xcode": "^1.0.0-alpha.24", - "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.23.0", "@types/chokidar": "^1.7.5", "@types/node": "^22.13.6", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.28.0", - "@typescript-eslint/parser": "^8.28.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^9.23.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.5", - "playwright": "^1.53.0", + "glob": "^13.0.6", + "knip": "^5.88.0", "prettier": "3.6.2", - "ts-node": "^10.9.2", "tsup": "^8.5.0", "tsx": "^4.20.4", "typescript": "^5.8.2", "typescript-eslint": "^8.28.0", - "vitest": "^3.2.4", - "xcode": "^3.0.1" + "vitest": "^3.2.4" } } diff --git a/scripts/benchmark-simulator-test.ts b/scripts/benchmark-simulator-test.ts new file mode 100644 index 00000000..18486e24 --- /dev/null +++ b/scripts/benchmark-simulator-test.ts @@ -0,0 +1,264 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { spawn } from 'node:child_process'; + +type BenchmarkMode = 'warm' | 'cold'; + +type BenchmarkTool = 'xcodebuildmcp' | 'flowdeck'; + +interface RunMetrics { + tool: BenchmarkTool; + iteration: number; + exitCode: number | null; + wallClockMs: number; + firstStdoutMs: number | null; + firstMilestoneMs: number | null; + startupToFirstStreamedTestProgressMs: number | null; + stdoutPath: string; + stderrPath: string; +} + +interface RunCommandParams { + tool: BenchmarkTool; + command: string; + args: string[]; + cwd: string; + artifactPrefix: string; + milestonePattern: RegExp; + streamedTestProgressPattern: RegExp; +} + +function parseArgs(): { iterations: number; mode: BenchmarkMode } { + const args = process.argv.slice(2); + let iterations = 1; + let mode: BenchmarkMode = 'warm'; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + if (argument === '--iterations') { + iterations = Number(args[index + 1] ?? '1'); + index += 1; + continue; + } + if (argument === '--mode') { + const nextMode = args[index + 1] ?? 'warm'; + if (nextMode === 'warm' || nextMode === 'cold') { + mode = nextMode; + } + index += 1; + } + } + + return { iterations, mode }; +} + +function stripAnsi(text: string): string { + return text.replace(/\u001B\[[0-9;]*[A-Za-z]/gu, ''); +} + +function isSpinnerFrame(line: string): boolean { + return ['◒', '◐', '◓', '◑', '│'].includes(line); +} + +function normalizeTerminalTranscript(text: string): string { + const cleaned = stripAnsi(text).replace(/\r/gu, '\n').replace(/[\u0004\u0008]/gu, ''); + const lines = cleaned.split('\n'); + const normalizedLines: string[] = []; + let joinedCharacterRun = ''; + + const flushCharacterRun = (): void => { + const line = joinedCharacterRun.trim(); + if (line) { + normalizedLines.push(line); + } + joinedCharacterRun = ''; + }; + + for (const rawLine of lines) { + const line = rawLine.trimEnd(); + const trimmed = line.trim(); + + if (!trimmed || isSpinnerFrame(trimmed)) { + continue; + } + + if (trimmed.length === 1 || /^[.()0-9,]+$/u.test(trimmed)) { + joinedCharacterRun += trimmed; + continue; + } + + flushCharacterRun(); + normalizedLines.push(trimmed); + } + + flushCharacterRun(); + return normalizedLines.join('\n'); +} + +async function ensureScriptAvailable(): Promise { + await new Promise((resolve, reject) => { + const child = spawn('/usr/bin/script', ['-q', '/dev/null', 'true'], { + stdio: 'ignore', + }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`/usr/bin/script exited with code ${code ?? 'unknown'}`)); + }); + }); +} + +async function runCommand(params: RunCommandParams): Promise { + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + const start = performance.now(); + let firstStdoutMs: number | null = null; + let firstMilestoneMs: number | null = null; + let startupToFirstStreamedTestProgressMs: number | null = null; + let normalizedStdout = ''; + + const child = spawn('/usr/bin/script', ['-q', '/dev/null', params.command, ...params.args], { + cwd: params.cwd, + env: { + ...process.env, + NO_COLOR: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + child.stdout.on('data', (chunk: Buffer) => { + if (firstStdoutMs === null) { + firstStdoutMs = performance.now() - start; + } + + const text = chunk.toString(); + stdoutChunks.push(text); + normalizedStdout += normalizeTerminalTranscript(text); + + if (firstMilestoneMs === null && params.milestonePattern.test(normalizedStdout)) { + firstMilestoneMs = performance.now() - start; + } + + if ( + startupToFirstStreamedTestProgressMs === null && + params.streamedTestProgressPattern.test(normalizedStdout) + ) { + startupToFirstStreamedTestProgressMs = performance.now() - start; + } + }); + + child.stderr.on('data', (chunk: Buffer) => { + stderrChunks.push(chunk.toString()); + }); + + const exitCode = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', resolve); + }); + + const stdoutPath = `${params.artifactPrefix}.stdout.txt`; + const stderrPath = `${params.artifactPrefix}.stderr.txt`; + await writeFile(stdoutPath, normalizeTerminalTranscript(stdoutChunks.join(''))); + await writeFile(stderrPath, normalizeTerminalTranscript(stderrChunks.join(''))); + + return { + tool: params.tool, + iteration: 0, + exitCode, + wallClockMs: performance.now() - start, + firstStdoutMs, + firstMilestoneMs, + startupToFirstStreamedTestProgressMs, + stdoutPath, + stderrPath, + }; +} + +async function main(): Promise { + const { iterations, mode } = parseArgs(); + await ensureScriptAvailable(); + + const repoRoot = process.cwd(); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const outputDir = path.join(repoRoot, 'benchmarks', 'simulator-test', timestamp); + await mkdir(outputDir, { recursive: true }); + + const workspacePath = path.join(repoRoot, 'example_projects', 'iOS_Calculator', 'CalculatorApp.xcworkspace'); + const xcodebuildmcpDerivedDataPath = path.join(outputDir, 'derived-data-xcodebuildmcp'); + const flowdeckDerivedDataPath = path.join(outputDir, 'derived-data-flowdeck'); + const xcodebuildmcpPayload = JSON.stringify({ + workspacePath, + scheme: 'CalculatorApp', + simulatorName: 'iPhone 17 Pro', + useLatestOS: true, + extraArgs: ['-only-testing:CalculatorAppTests'], + progress: true, + derivedDataPath: xcodebuildmcpDerivedDataPath, + }); + + const results: RunMetrics[] = []; + + for (let iteration = 1; iteration <= iterations; iteration += 1) { + if (mode === 'cold') { + await rm(xcodebuildmcpDerivedDataPath, { recursive: true, force: true }); + await rm(flowdeckDerivedDataPath, { recursive: true, force: true }); + } + + const xcodebuildmcpResult = await runCommand({ + tool: 'xcodebuildmcp', + command: './build/cli.js', + args: ['simulator', 'test', '--json', xcodebuildmcpPayload, '--output', 'text'], + cwd: repoRoot, + artifactPrefix: path.join(outputDir, `xcodebuildmcp-run-${iteration}`), + milestonePattern: /📦\s*Resolving\s*packages|🛠️\s*Compiling|🧪\s*(?:Starting\s*tests|Running\s*tests)/u, + streamedTestProgressPattern: /🧪\s*(?:Starting\s*tests|Running\s*tests)/u, + }); + xcodebuildmcpResult.iteration = iteration; + results.push(xcodebuildmcpResult); + + const flowdeckResult = await runCommand({ + tool: 'flowdeck', + command: 'flowdeck', + args: [ + 'test', + '-w', + workspacePath, + '-s', + 'CalculatorApp', + '-S', + 'iPhone 17 Pro', + '--only', + 'CalculatorAppTests', + '--progress', + '-d', + flowdeckDerivedDataPath, + ], + cwd: repoRoot, + artifactPrefix: path.join(outputDir, `flowdeck-run-${iteration}`), + milestonePattern: /Resolving Package Graph|Compiling\.\.\.|Running tests/u, + streamedTestProgressPattern: /Running tests/u, + }); + flowdeckResult.iteration = iteration; + results.push(flowdeckResult); + } + + const summary = { + generatedAt: new Date().toISOString(), + mode, + iterations, + workspacePath, + results, + }; + + await writeFile(path.join(outputDir, 'summary.json'), JSON.stringify(summary, null, 2)); + process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/scripts/capture-xcodebuild-wrapper.ts b/scripts/capture-xcodebuild-wrapper.ts new file mode 100644 index 00000000..209f0975 --- /dev/null +++ b/scripts/capture-xcodebuild-wrapper.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env tsx + +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { spawn, spawnSync } from 'node:child_process'; + +interface WrapperCaptureRecord { + timestamp: string; + cwd: string; + argv: string[]; +} + +function parseArgs(): string[] { + const forwardedArgs = process.argv.slice(2); + if (forwardedArgs.length === 0) { + throw new Error('Usage: npm run capture:xcodebuild -- [args...]'); + } + + return forwardedArgs[0] === '--' ? forwardedArgs.slice(1) : forwardedArgs; +} + +function resolveRealXcodebuild(): string { + const result = spawnSync('xcrun', ['-f', 'xcodebuild'], { encoding: 'utf8' }); + if (result.status !== 0) { + throw new Error(result.stderr.trim() || 'Unable to resolve xcodebuild via xcrun'); + } + + const resolvedPath = result.stdout.trim(); + if (!resolvedPath) { + throw new Error('xcrun returned an empty xcodebuild path'); + } + + return resolvedPath; +} + +async function createWrapperScript(wrapperDir: string): Promise { + const wrapperPath = path.join(wrapperDir, 'xcodebuild'); + const script = `#!/usr/bin/env node +const { appendFileSync } = require('node:fs'); +const { spawn } = require('node:child_process'); + +const logPath = process.env.XCODEBUILD_WRAPPER_LOG_PATH; +const realPath = process.env.XCODEBUILD_WRAPPER_REAL_PATH; + +if (!logPath || !realPath) { + process.stderr.write('xcodebuild wrapper is missing required environment variables\\n'); + process.exit(1); +} + +appendFileSync( + logPath, + JSON.stringify({ + timestamp: new Date().toISOString(), + cwd: process.cwd(), + argv: process.argv.slice(2), + }) + '\\n', +); + +const child = spawn(realPath, process.argv.slice(2), { stdio: 'inherit' }); +child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 1); +}); +child.on('error', (error) => { + process.stderr.write(String(error) + '\\n'); + process.exit(1); +}); +`; + + await writeFile(wrapperPath, script, { mode: 0o755 }); + await chmod(wrapperPath, 0o755); + return wrapperPath; +} + +async function main(): Promise { + const command = parseArgs(); + const realXcodebuildPath = resolveRealXcodebuild(); + const tempRoot = await mkdtemp(path.join(tmpdir(), 'xcodebuild-wrapper-')); + const wrapperDir = path.join(tempRoot, 'bin'); + const logDir = path.join(process.cwd(), 'benchmarks', 'xcodebuild-wrapper'); + const logPath = path.join(logDir, `${new Date().toISOString().replace(/[:.]/gu, '-')}.jsonl`); + + await mkdir(wrapperDir, { recursive: true }); + await mkdir(logDir, { recursive: true }); + await createWrapperScript(wrapperDir); + + const child = spawn(command[0]!, command.slice(1), { + cwd: process.cwd(), + env: { + ...process.env, + PATH: `${wrapperDir}:${process.env.PATH ?? ''}`, + XCODEBUILD_WRAPPER_LOG_PATH: logPath, + XCODEBUILD_WRAPPER_REAL_PATH: realXcodebuildPath, + }, + stdio: 'inherit', + }); + + const exitCode = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', (code) => resolve(code ?? 1)); + }); + + const recordsText = await readFile(logPath, 'utf8').catch(() => ''); + const records = recordsText + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line) as WrapperCaptureRecord); + + process.stdout.write(`\nCaptured ${records.length} xcodebuild invocation(s)\n`); + process.stdout.write(`Log: ${logPath}\n`); + for (const [index, record] of records.entries()) { + process.stdout.write(`\n#${index + 1} ${record.timestamp}\n`); + process.stdout.write(`cwd: ${record.cwd}\n`); + process.stdout.write(`argv: ${record.argv.join(' ')}\n`); + } + + await rm(tempRoot, { recursive: true, force: true }); + process.exitCode = exitCode; +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/scripts/copy-build-assets.js b/scripts/copy-build-assets.js deleted file mode 100644 index b1c14038..00000000 --- a/scripts/copy-build-assets.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -/** - * Post-build script to copy assets and set permissions. - * Called after tsc compilation to prepare the build directory. - */ - -import { chmodSync, existsSync, copyFileSync, mkdirSync } from 'fs'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const projectRoot = join(__dirname, '..'); - -// Set executable permissions for entry points -const executables = ['build/cli.js', 'build/doctor-cli.js', 'build/daemon.js']; - -for (const file of executables) { - const fullPath = join(projectRoot, file); - if (existsSync(fullPath)) { - chmodSync(fullPath, '755'); - console.log(` Set executable: ${file}`); - } -} - -// Copy tools-manifest.json to build directory (for backward compatibility) -// This can be removed once Phase 7 is complete -const toolsManifestSrc = join(projectRoot, 'build', 'tools-manifest.json'); -if (existsSync(toolsManifestSrc)) { - console.log(' tools-manifest.json already in build/'); -} - -console.log('✅ Build assets copied successfully'); diff --git a/src/test-utils/vitest-executor-safety.setup.ts b/src/test-utils/vitest-executor-safety.setup.ts new file mode 100644 index 00000000..5e282364 --- /dev/null +++ b/src/test-utils/vitest-executor-safety.setup.ts @@ -0,0 +1,34 @@ +/** + * Vitest unit-test setup: installs blocking executor/spawner overrides. + * + * This ensures unit tests fail fast if they accidentally reach a real system + * executor, filesystem, or interactive spawner without explicit mock injection. + * + * Only loaded by vitest.config.ts (unit tests). Snapshot and smoke configs + * intentionally do NOT load this file. + */ + +import { beforeEach, afterEach } from 'vitest'; +import { + __setTestCommandExecutorOverride, + __setTestFileSystemExecutorOverride, + __clearTestExecutorOverrides, + __setTestInteractiveSpawnerOverride, + __clearTestInteractiveSpawnerOverride, +} from '../utils/execution/index.ts'; +import { + createNoopExecutor, + createNoopFileSystemExecutor, + createNoopInteractiveSpawner, +} from './mock-executors.ts'; + +beforeEach(() => { + __setTestCommandExecutorOverride(createNoopExecutor()); + __setTestFileSystemExecutorOverride(createNoopFileSystemExecutor()); + __setTestInteractiveSpawnerOverride(createNoopInteractiveSpawner()); +}); + +afterEach(() => { + __clearTestExecutorOverrides(); + __clearTestInteractiveSpawnerOverride(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 87ac5a2e..1f449cc8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { environment: 'node', globals: true, + setupFiles: ['src/test-utils/vitest-executor-safety.setup.ts'], include: [ 'src/**/__tests__/**/*.test.ts', // Only __tests__ directories ], @@ -21,6 +22,7 @@ export default defineConfig({ '**/__pycache__/**', '**/dist/**', 'src/smoke-tests/**', + 'src/snapshot-tests/**', ], pool: 'threads', poolOptions: { diff --git a/vitest.flowdeck.config.ts b/vitest.flowdeck.config.ts new file mode 100644 index 00000000..5ff94a51 --- /dev/null +++ b/vitest.flowdeck.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['src/snapshot-tests/__tests__/**/*.flowdeck.test.ts'], + pool: 'forks', + poolOptions: { + forks: { + maxForks: 1, + }, + }, + testTimeout: 120000, + hookTimeout: 120000, + teardownTimeout: 10000, + }, + resolve: { + alias: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + }, +}); diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts new file mode 100644 index 00000000..88e795f1 --- /dev/null +++ b/vitest.snapshot.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['src/snapshot-tests/__tests__/**/*.test.ts'], + exclude: ['src/snapshot-tests/__tests__/**/*.flowdeck.test.ts'], + pool: 'forks', + poolOptions: { + forks: { + maxForks: 1, + }, + }, + env: { + NODE_OPTIONS: '--max-old-space-size=4096', + }, + testTimeout: 120000, + hookTimeout: 120000, + teardownTimeout: 10000, + }, + resolve: { + alias: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + }, +}); From d4cf7d206c2e8cb85f79f8afc1f0ed42fd6ba91d Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 9 Apr 2026 08:45:32 +0100 Subject: [PATCH 2/3] chore: remove local-research directory Internal research notes that shouldn't be committed to the repository. --- .../monolithic-workflows-investigation.md | 284 ------------------ local-research/pipeline-coupling-audit.md | 121 -------- .../rendering-pipeline-remaining-cleanup.md | 36 --- ...codebuild-command-builder-investigation.md | 157 ---------- 4 files changed, 598 deletions(-) delete mode 100644 local-research/monolithic-workflows-investigation.md delete mode 100644 local-research/pipeline-coupling-audit.md delete mode 100644 local-research/rendering-pipeline-remaining-cleanup.md delete mode 100644 local-research/xcodebuild-command-builder-investigation.md diff --git a/local-research/monolithic-workflows-investigation.md b/local-research/monolithic-workflows-investigation.md deleted file mode 100644 index 9ead3099..00000000 --- a/local-research/monolithic-workflows-investigation.md +++ /dev/null @@ -1,284 +0,0 @@ -# Investigation: Monolithic Multi-Step Workflows in build_run_* Tools - -## Summary - -The claim is **valid but nuanced**. The three `build_run_*` orchestrators (`build_run_sim`, `build_run_device`, `build_run_macos`) are monolithic at the **orchestration layer** — each inlines the full workflow (build, resolve app path, boot/install/launch) in a single function. However, they already share significant **utility-level** infrastructure. The duplication is specifically between orchestrator inline logic and the corresponding standalone step-tool handlers, which implement the same commands independently. - -## Symptoms - -- `build_run_simLogic` is 549 lines, performing ~8 distinct steps inline -- `build_run_deviceLogic` is 357 lines, performing ~6 distinct steps inline -- `buildRunMacOSLogic` is 242 lines, performing ~5 distinct steps inline -- Each orchestrator duplicates command construction found in standalone step tools -- Step tools (`boot_sim`, `install_app_sim`, `launch_app_sim`, etc.) exist but are never called by orchestrators - -## Investigation Log - -### Phase 1 — Identifying the Orchestrators and Step Tools - -**Hypothesis:** The build_run_* files contain monolithic handlers that duplicate step-tool logic. - -**Findings:** Three orchestrators exist, each with corresponding standalone step tools: - -| Orchestrator | Standalone Step Tools | -|---|---| -| `build_run_sim.ts` | `build_sim.ts`, `boot_sim.ts`, `install_app_sim.ts`, `launch_app_sim.ts`, `get_sim_app_path.ts` | -| `build_run_device.ts` | `build_device.ts`, `install_app_device.ts`, `launch_app_device.ts`, `get_device_app_path.ts` | -| `build_run_macos.ts` | `build_macos.ts`, `launch_mac_app.ts`, `get_mac_app_path.ts` | - -**Conclusion:** Confirmed — orchestrators and step tools are fully independent modules with no handler-level composition. - -### Phase 2 — Concrete Duplication: Simulator Boot - -**Hypothesis:** Boot logic is duplicated between `build_run_sim.ts` and `boot_sim.ts`. - -**Evidence:** - -`boot_sim.ts` line 57: -```typescript -const command = ['xcrun', 'simctl', 'boot', params.simulatorId]; -const result = await executor(command, 'Boot Simulator', false); -``` - -`build_run_sim.ts` lines 283-288 (inline in the orchestrator): -```typescript -const bootResult = await executor( - ['xcrun', 'simctl', 'boot', simulatorId], - 'Boot Simulator', -); -``` - -Additionally, `build_run_sim.ts` lines 246-280 contains ~35 lines of simulator state checking logic (JSON parsing of `simctl list devices available --json`, iterating runtimes to find the target simulator by UUID, checking `state !== 'Booted'`) that has no equivalent in `boot_sim.ts` — the standalone tool assumes the caller knows the simulator needs booting. - -**Conclusion:** Confirmed duplication. The orchestrator also has **extra logic** not in the step tool (state checking before boot). - -### Phase 3 — Concrete Duplication: Simulator Install - -**Hypothesis:** Install logic is duplicated. - -**Evidence:** - -`install_app_sim.ts` line 73: -```typescript -const command = ['xcrun', 'simctl', 'install', params.simulatorId, params.appPath]; -const result = await executor(command, 'Install App in Simulator', false); -``` - -`build_run_sim.ts` lines 316-319 (inline): -```typescript -const installResult = await executor( - ['xcrun', 'simctl', 'install', simulatorId, appBundlePath], - 'Install App', -); -``` - -**Conclusion:** Confirmed — identical command, duplicated in both places. - -### Phase 4 — Concrete Duplication: Simulator Launch - -**Evidence:** - -`launch_app_sim.ts` lines 103-104: -```typescript -const command = ['xcrun', 'simctl', 'launch', simulatorId, params.bundleId]; -``` -Plus PID parsing at lines 113-114: -```typescript -const pidMatch = result.output?.match(/:\s*(\d+)\s*$/); -``` - -`build_run_sim.ts` lines 355-358: -```typescript -const launchResult = await executor( - ['xcrun', 'simctl', 'launch', simulatorId, bundleId], - 'Launch App', -); -``` -Plus PID parsing at lines 362-363: -```typescript -const pidMatch = launchResult.output?.match(/:\s*(\d+)\s*$/); -``` - -**Conclusion:** Confirmed — identical command and PID regex, duplicated. - -### Phase 5 — Concrete Duplication: Device Install - -**Evidence:** - -`install_app_device.ts` line 53: -```typescript -['xcrun', 'devicectl', 'device', 'install', 'app', '--device', deviceId, appPath] -``` - -`build_run_device.ts` line 203: -```typescript -['xcrun', 'devicectl', 'device', 'install', 'app', '--device', params.deviceId, appPath] -``` - -**Conclusion:** Confirmed — identical command. - -### Phase 6 — Concrete Duplication: Device Launch (Heaviest Duplication) - -**Evidence:** - -`launch_app_device.ts` lines 80-95: -```typescript -const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`); -const command = [ - 'xcrun', 'devicectl', 'device', 'process', 'launch', - '--device', deviceId, - '--json-output', tempJsonPath, - '--terminate-existing', -]; -if (params.env && Object.keys(params.env).length > 0) { - command.push('--environment-variables', JSON.stringify(params.env)); -} -command.push(bundleId); -``` -Plus JSON PID parsing at lines 104-112 and temp file cleanup at lines 113-115. - -`build_run_device.ts` lines 223-244: -```typescript -const tempJsonPath = join(fileSystemExecutor.tmpdir(), `launch-${Date.now()}.json`); -const command = [ - 'xcrun', 'devicectl', 'device', 'process', 'launch', - '--device', params.deviceId, - '--json-output', tempJsonPath, - '--terminate-existing', -]; -if (params.env && Object.keys(params.env).length > 0) { - command.push('--environment-variables', JSON.stringify(params.env)); -} -command.push(bundleId); -``` -Plus near-identical JSON PID parsing at lines 250-259 and cleanup at lines 260-262. - -**Conclusion:** Confirmed — this is the clearest case of near-verbatim duplication (~40 lines of identical logic). - -### Phase 7 — Concrete Duplication: macOS Launch - -**Evidence:** - -`launch_mac_app.ts` lines 43-68: -```typescript -const command = ['open', params.appPath]; -// ... launch ... -// Bundle ID extraction via defaults read -const plistResult = await executor( - ['/bin/sh', '-c', `defaults read "${params.appPath}/Contents/Info" CFBundleIdentifier`], - 'Extract Bundle ID', false, -); -// PID lookup via pgrep -const pgrepResult = await executor(['pgrep', '-x', appName], 'Get Process ID', false); -``` - -`build_run_macos.ts` lines 160-195: -```typescript -const launchResult = await executor(['open', appPath], 'Launch macOS App', false); -// ... same bundle ID extraction ... -const plistResult = await executor( - ['/bin/sh', '-c', `defaults read "${appPath}/Contents/Info" CFBundleIdentifier`], - 'Extract Bundle ID', false, -); -// ... same pgrep PID lookup ... -const pgrepResult = await executor(['pgrep', '-x', appName], 'Get Process ID', false); -``` - -**Conclusion:** Confirmed — same three-step pattern (open, defaults read, pgrep) duplicated. - -### Phase 8 — Existing Good Pattern: `handleTestLogic` - -The test tools (`test_sim.ts`, `test_device.ts`, `test_macos.ts`) demonstrate the better pattern already present in the codebase. - -`test_sim.ts` line 139: -```typescript -return handleTestLogic({ ...params, platform: inferred.platform }, executor, { - preflight: preflight ?? undefined, - toolName: 'test_sim', -}); -``` - -`handleTestLogic` lives in `src/utils/test-common.ts` (exported via `src/utils/test/index.ts`) and is shared across all three test tool handlers. Each tool does thin validation/platform inference, then delegates to the shared logic. - -**Conclusion:** The codebase already has a proven pattern for shared workflow logic. The build-run tools haven't adopted it yet. - -## What Is NOT Duplicated (Shared Utilities) - -To be fair, the orchestrators already share significant infrastructure: - -- `executeXcodeBuildCommand` — build command construction and execution -- `resolveAppPathFromBuildSettings` — app path resolution from xcodebuild settings -- `startBuildPipeline` / `createPendingXcodebuildResponse` — pipeline lifecycle -- `createBuildRunResultEvents` / `emitPipelineNotice` / `emitPipelineError` — structured events -- `extractBundleIdFromAppPath` — bundle ID extraction -- `inferPlatform` — simulator platform inference -- `determineSimulatorUuid` — simulator UUID resolution - -The duplication is specifically at the **step execution layer**: boot, install, launch commands and their response handling. - -## Root Cause - -The orchestrators were written as self-contained end-to-end workflows. The step tools were written as separate user-facing handlers. Neither calls the other. Both construct the same underlying commands independently. - -This is a classic "convenience wrapper vs granular API" problem — the orchestrators were likely written first (or in parallel) without extracting the step logic into reusable internal primitives. - -## Recommendations - -### Recommended Approach: Extract Internal Step Primitives - -Create pure internal helper functions (not tool handlers) that encapsulate each step's command construction, execution, and result parsing. Both orchestrators and step tools would then call these. - -**1. Simulator steps** — new file `src/utils/simulator-steps.ts`: -```typescript -export async function bootSimulatorIfNeeded(simulatorId: string, executor: CommandExecutor): Promise -export async function installAppOnSimulator(simulatorId: string, appPath: string, executor: CommandExecutor): Promise -export async function launchSimulatorApp(simulatorId: string, bundleId: string, executor: CommandExecutor): Promise -``` - -**2. Device steps** — new file `src/utils/device-steps.ts`: -```typescript -export async function installAppOnDevice(deviceId: string, appPath: string, executor: CommandExecutor): Promise -export async function launchAppOnDevice(deviceId: string, bundleId: string, env?: Record, fs?: FileSystemExecutor): Promise -``` - -**3. macOS steps** — new file `src/utils/macos-steps.ts`: -```typescript -export async function launchMacApp(appPath: string, args?: string[], executor: CommandExecutor): Promise -``` - -Then refactor: -- `build_run_sim.ts` → calls `bootSimulatorIfNeeded()`, `installAppOnSimulator()`, `launchSimulatorApp()` -- `boot_sim.ts` → calls `bootSimulatorIfNeeded()` (or just `bootSimulator()`) -- `install_app_sim.ts` → calls `installAppOnSimulator()` -- `launch_app_sim.ts` → calls `launchSimulatorApp()` -- Same pattern for device and macOS tools - -### Why NOT "Tool Calls Tool" - -The tool handlers mix validation, session-default handling, response formatting, and next-step metadata. Making orchestrators call step-tool handlers would be clumsy because: -- Tool handlers return `ToolResponse` with formatted events — the orchestrator would need to unwrap and re-wrap -- Schema validation would run redundantly -- Error handling and pipeline eventing would conflict - -Internal primitives that return simple result types are the clean separation. - -### Alternative: `handleBuildRunLogic` (Like `handleTestLogic`) - -A more aggressive refactor would extract a single `handleBuildRunLogic` shared function (analogous to `handleTestLogic` for tests) that all three orchestrators delegate to. This would require parameterizing the platform-specific steps (boot/install/launch) but could eliminate even more duplication in the build → resolve-path → run pipeline. - -## Preventive Measures - -- When adding new multi-step workflow tools, extract step logic into `src/utils/*-steps.ts` first, then compose in both the orchestrator and the individual step-tool handlers -- Consider adding a lint rule or code review checklist item: "Does this tool duplicate command logic from another tool?" -- The `handleTestLogic` pattern is the gold standard in this codebase — reference it when designing new shared workflows - -## Estimated Impact - -| File | Current Lines | Estimated Reduction | -|---|---|---| -| `build_run_sim.ts` | 549 | ~120-150 lines (boot/install/launch/state-check blocks) | -| `build_run_device.ts` | 357 | ~60-80 lines (install/launch blocks) | -| `build_run_macos.ts` | 242 | ~30-40 lines (launch/bundleid/pid blocks) | -| Step tools (6 files) | ~705 total | ~50-80 lines (delegating to shared primitives) | - -Total: ~260-350 lines of duplicated logic consolidated into ~100-150 lines of shared step primitives. diff --git a/local-research/pipeline-coupling-audit.md b/local-research/pipeline-coupling-audit.md deleted file mode 100644 index d3dfdde1..00000000 --- a/local-research/pipeline-coupling-audit.md +++ /dev/null @@ -1,121 +0,0 @@ -# Investigation: xcodebuild-pipeline.ts coupling audit - -## Summary - -The claim that `xcodebuild-pipeline.ts` should be split into a generic `ToolOutputPipeline` and an xcodebuild-specific event parser is **partially valid in diagnosis but wrong in prescription**. The architecture is already split at the correct seam — `toolResponse()` serves as the generic event rendering path (212 call sites), while `xcodebuild-pipeline.ts` is a purpose-built streaming build/test parser (19 call sites). No non-build tool needs a generic streaming pipeline. The real issues are naming and type-boundary clarity, not missing infrastructure. - -## Symptoms / Original Claim - -> "xcodebuild-pipeline.ts is coupled to xcodebuild - The pipeline should be split into a generic ToolOutputPipeline (events + renderers) and an xcodebuild-specific event parser, so non-build tools can use the same rendering." - -## Investigation Log - -### Phase 1 — Identifying the coupling - -**Hypothesis:** The pipeline is tightly coupled to xcodebuild specifics. - -**Findings:** Confirmed. Six concrete coupling points: - -1. **API shape** — `createXcodebuildPipeline()` (`xcodebuild-pipeline.ts:168`) takes `operation: XcodebuildOperation` (`'BUILD' | 'TEST'`) and `minimumStage?: XcodebuildStage` as required params. - -2. **Hard-wired components** — The factory always creates `createXcodebuildEventParser()` (line 179) and `createXcodebuildRunState()` (line 173). No way to inject alternative parsers or state managers. - -3. **Build-specific finalization** — `finalize()` (lines 194–244) flushes the xcodebuild parser, injects build log file refs via `injectBuildLogIntoTailEvents()`, emits parser debug warnings, and exposes `xcresultPath`. - -4. **Build-specific header builder** — `startBuildPipeline()` (lines 155–166) and `buildHeaderParams()` (lines 104–139) know about Scheme, Workspace, Project, Simulator, Device, Architecture, xcresult, etc. - -5. **Renderer naming** — The renderer interface is `XcodebuildRenderer` (`renderers/index.ts:8`) despite consuming generic `PipelineEvent`s that all tools use. - -6. **Mixed event union** — `pipeline-events.ts` defines generic canonical events (lines 27–86: `header`, `status-line`, `summary`, `section`, `detail-tree`, `table`, `file-ref`, `next-steps`) alongside xcodebuild-specific events (lines 88–148: `build-stage`, `compiler-warning`, `compiler-error`, `test-discovery`, `test-progress`, `test-failure`) in a single union with no type-level boundary. - -**Evidence:** All line numbers verified by direct file reads. - -**Conclusion:** Coupling is real and confirmed. - -### Phase 2 — Does a generic layer already exist? - -**Hypothesis:** The codebase already has generic rendering infrastructure that non-build tools use. - -**Findings:** Confirmed. The generic layer is `toolResponse()` + `tool-event-builders.ts`: - -1. **`toolResponse()`** (`tool-response.ts:11–39`) implements the exact pattern a generic pipeline would: resolve renderers → fan out events → finalize → collect MCP content. It handles all event types including xcodebuild-specific ones. - -2. **`tool-event-builders.ts`** (88 lines) builds only generic canonical events: `header`, `section`, `statusLine`, `fileRef`, `table`, `detailTree`, `nextSteps`. - -3. **Usage ratio** — In `src/mcp/tools/`: **212 calls to `toolResponse()`** vs **19 references to pipeline functions**. The overwhelming majority of tools already use the generic path. - -4. **Non-build tool patterns** — Tools like `debug_attach_sim.ts` and `start_device_log_cap.ts` build static event arrays and call `toolResponse()`. Even `start_device_log_cap`, which manages a long-running subprocess, handles its own output buffering without needing streaming pipeline infrastructure. - -**Conclusion:** The generic rendering layer exists and is the dominant pattern. - -### Phase 3 — Would non-build tools benefit from a generic streaming pipeline? - -**Hypothesis:** Non-build tools could benefit from a `ToolOutputPipeline`. - -**Findings:** No current evidence of need: - -1. **Zero non-build tools** use the streaming pipeline. -2. **No non-build tool** requires parser/state/stage tracking. -3. **`start_device_log_cap.ts`** is the closest candidate (long-running subprocess with stdout/stderr handling), but it manages output via direct stream handlers and log files — it does not need event parsing, stage progression, or summary synthesis. -4. The pipeline is also used by `swift_package_build.ts`, `swift_package_run.ts`, and `swift_package_test.ts` — but these are effectively build/test tools that happen to use `swift` CLI instead of `xcodebuild`. They reuse the xcodebuild parser opportunistically since the output formats overlap (compiler diagnostics, test results, etc.). - -**Conclusion:** No current consumer pressure for a generic streaming pipeline. The pipeline's scope is build/test toolchain output, not arbitrary subprocess streaming. - -## Root Cause - -The claim conflates two separate concerns: - -1. **"Non-build tools can't use the same rendering"** — This is false. They already do, via `toolResponse()` which uses the same renderer registry and event formatting as the pipeline. - -2. **"The pipeline should be generic"** — This would be premature abstraction. The pipeline's value is specifically in its xcodebuild/swift-toolchain parsing, state tracking, diagnostic dedup, and build log management. Making it generic would strip out its useful specificity without gaining any consumers. - -The real issues are cosmetic/type-level: -- `XcodebuildRenderer` is misnamed (it handles all event types) -- `PipelineEvent` mixes generic and domain-specific types without a type boundary -- The pipeline name slightly understates its actual scope (it handles `swift build/test/run` too, not just `xcodebuild`) - -## Recommendations - -### Do now (low-effort, high-clarity) - -1. **Rename `XcodebuildRenderer` → `PipelineRenderer`** in `src/utils/renderers/index.ts:8` and all references. This interface consumes generic `PipelineEvent`s and is used by both the pipeline and `toolResponse()`. - -2. **Split event types at the type level** in `src/types/pipeline-events.ts`: - ```typescript - // Generic events usable by any tool - type CommonPipelineEvent = - | HeaderEvent | StatusLineEvent | SummaryEvent | SectionEvent - | DetailTreeEvent | TableEvent | FileRefEvent | NextStepsEvent; - - // Build/test-specific events - type BuildTestPipelineEvent = - | BuildStageEvent | CompilerWarningEvent | CompilerErrorEvent - | TestDiscoveryEvent | TestProgressEvent | TestFailureEvent; - - // Full union (backward compatible) - type PipelineEvent = CommonPipelineEvent | BuildTestPipelineEvent; - ``` - This makes the boundary explicit without breaking any runtime code. - -### Maybe do later (if duplication grows) - -3. **Extract a tiny render-session helper** from the duplicated pattern between `toolResponse()` and `createXcodebuildPipeline()`: - - Both call `resolveRenderers()` - - Both fan out events to renderers - - Both call `renderer.finalize()` - - Both collect `mcpRenderer.getContent()` - - A ~20-line helper could eliminate this duplication if more entry points emerge. - -4. **Consider renaming the pipeline** to `BuildOutputPipeline` or `BuildTestPipeline` to reflect that it handles `swift build/test/run` output too, not just `xcodebuild`. - -### Do not do - -5. **Do not build a generic `ToolOutputPipeline`** — there are zero consumers that need it. The `toolResponse()` function already serves the generic use case. - -6. **Do not split parser/state/finalize into abstract interfaces** — there is only one implementation and no foreseeable second one. - -## Preventive Measures - -- When adding new streaming subprocess tools, evaluate whether they need the build pipeline's features (stage tracking, diagnostic dedup, summary synthesis). If not, `toolResponse()` with event builders is sufficient. -- If a second streaming parser ever emerges, *that* is the time to extract common infrastructure from the pipeline. diff --git a/local-research/rendering-pipeline-remaining-cleanup.md b/local-research/rendering-pipeline-remaining-cleanup.md deleted file mode 100644 index a0c88f2e..00000000 --- a/local-research/rendering-pipeline-remaining-cleanup.md +++ /dev/null @@ -1,36 +0,0 @@ -# Rendering Pipeline Refactor — Remaining Cleanup - -## Completed -- Render session module (src/rendering/) -- ToolHandlerContext via AsyncLocalStorage -- All 77 tool handlers emit via ctx -- Factory dual-mode (void → session, ToolResponse → passthrough) -- Pipeline inline finalization (pending pattern eliminated) -- CLI boundary re-renders via CLI text renderer -- MCP boundary creates session -- Snapshot normalizer stabilized for doctor output - -## Remaining Cleanup (technical debt) - -### 1. Remove hybrid toolResponse() usage from migrated tools -~40 tool handlers still call `toolResponse()` inside `withErrorHandling` mapError callbacks -or inner async functions, then extract events from `_meta.events` to re-emit through ctx. -These should be fully converted to direct ctx.emit() calls. - -### 2. Remove ToolResponse type from tool handler signatures -Once hybrid usage is removed, the `Promise` return types -can become `Promise` and the ToolResponse import can be removed. - -### 3. Daemon protocol v2 -Send `{ events, attachments, isError }` over the wire instead of ToolResponse. -CLI renders locally. Requires protocol version bump. - -### 4. Delete dead renderers -Once toolResponse() is removed from all tool handlers and the pipeline -no longer uses resolveRenderers() fallback: -- Delete src/utils/renderers/cli-jsonl-renderer.ts (used only by resolveRenderers) -- Potentially simplify renderers/index.ts - -### 5. Encapsulate ToolResponse -Move ToolResponse type out of common.ts, make it module-private to the MCP -boundary (tool-registry.ts) and the daemon protocol. diff --git a/local-research/xcodebuild-command-builder-investigation.md b/local-research/xcodebuild-command-builder-investigation.md deleted file mode 100644 index 05d92602..00000000 --- a/local-research/xcodebuild-command-builder-investigation.md +++ /dev/null @@ -1,157 +0,0 @@ -# Investigation: XcodebuildCommandBuilder Claim - -## Summary - -The claim that "xcodebuild command construction argument building is scattered across tools" and that "a XcodebuildCommandBuilder with a fluent API would centralize this" is **partially true but overstated**. The core build/test path is already well centralized via `executeXcodeBuildCommand`. The real duplication is limited to a few `-showBuildSettings` query tools. A fluent builder would be over-engineering — targeted consolidation of the `get_*_app_path` tools is the right fix. - -## Symptoms Under Investigation - -- Claim: xcodebuild argument building is scattered across tools -- Claim: A XcodebuildCommandBuilder with a fluent API would centralize this - -## Investigation Log - -### Phase 1 — Quantifying the Scope - -**Hypothesis:** xcodebuild command construction exists in many files across the codebase. - -**Findings:** 456 matches for "xcodebuild" across `src/` (excluding tests). However, many are imports, log messages, and type references — not command construction. - -**Actual command construction sites (files that build `xcodebuild` argument arrays):** - -| File | Command Type | Centralized? | -|------|-------------|--------------| -| `src/utils/build-utils.ts:82-171` | build/test/build-for-testing/test-without-building | Yes — this IS the center | -| `src/utils/app-path-resolver.ts:63-93` | -showBuildSettings (app path lookup) | Yes — secondary center | -| `src/mcp/tools/simulator/get_sim_app_path.ts:143-156` | -showBuildSettings | No — inline duplicate | -| `src/mcp/tools/device/get_device_app_path.ts:102-116` | -showBuildSettings | No — inline duplicate | -| `src/mcp/tools/macos/get_mac_app_path.ts:94-112` | -showBuildSettings | No — inline duplicate | -| `src/mcp/tools/utilities/clean.ts:127-153` | clean action | No — inline (partially justified) | -| `src/mcp/tools/project-discovery/list_schemes.ts:52-55` | -list | No — inline (justified) | -| `src/mcp/tools/project-discovery/show_build_settings.ts:69-74` | -showBuildSettings | No — inline (justified) | -| `src/utils/platform-detection.ts:63-79` | -showBuildSettings (platform inference) | No — inline (justified) | -| `src/utils/xcode-state-watcher.ts:53-60` | -showBuildSettings -skipPackageUpdates | No — inline (justified) | -| `src/utils/sentry.ts` | -version | Peripheral diagnostic | -| `src/mcp/tools/doctor/lib/doctor.deps.ts:152` | -version | Peripheral diagnostic | - -**Conclusion:** 12 sites total. 2 are centralized. 3 are clear duplicates. 5 are local-but-justified. 2 are peripheral. - -### Phase 2 — Evaluating Existing Centralization - -**Hypothesis:** `executeXcodeBuildCommand` already centralizes the most important path. - -**Evidence:** - -`executeXcodeBuildCommand` (build-utils.ts:29-261) handles: -- Project/workspace selection with path resolution (lines 82-92) -- Scheme, configuration, `-skipMacroValidation` (lines 94-96) -- Full destination logic for all platforms: simulator by ID/name, macOS with arch, device by ID, generic (lines 98-134) -- Test-specific flags: `COMPILER_INDEX_STORE_ENABLE`, `ONLY_ACTIVE_ARCH`, `-packageCachePath` (lines 141-149) -- derivedDataPath, extraArgs (lines 151-157) -- Build action appended last (line 159) -- xcodemake fallback logic (lines 162-190) -- cwd set to project directory (line 194) - -**Callers (6 build/test tools) do NO argument construction** — they pass `SharedBuildParams` + `PlatformBuildOptions` objects and `executeXcodeBuildCommand` handles everything. Example from `build_sim.ts`: - -```typescript -const sharedBuildParams = { ...params, configuration }; -const platformOptions = { platform: detectedPlatform, simulatorName, simulatorId, useLatestOS, logPrefix }; -const buildResult = await executeXcodeBuildCommand(sharedBuildParams, platformOptions, ...); -``` - -**Conclusion: Confirmed.** The highest-volume, most important xcodebuild construction path is already centralized correctly. - -`resolveAppPathFromBuildSettings` (app-path-resolver.ts:60-100) is a secondary center for `-showBuildSettings` queries used by build-run flows. It handles project/workspace, scheme, config, destination, derivedDataPath, extraArgs, cwd — essentially the same shared arg pattern. - -### Phase 3 — The Real Duplication: `get_*_app_path` Tools - -**Hypothesis:** The three `get_*_app_path` tools duplicate `resolveAppPathFromBuildSettings`. - -**Evidence — behavioral drift across the three tools:** - -| Behavior | `get_sim_app_path.ts` | `get_device_app_path.ts` | `get_mac_app_path.ts` | `resolveAppPathFromBuildSettings` | -|----------|----------------------|-------------------------|-----------------------|----------------------------------| -| Resolves paths to absolute | No | Yes (line 103-106) | No | Yes | -| Sets cwd | No | Yes (line 118-121) | No | Yes | -| Always adds -destination | Yes | Yes | Only when arch provided | Yes | -| Handles derivedDataPath | No | No | Yes (line 104-106) | Yes | -| Handles extraArgs | No | No | Yes (line 108-110) | Yes | - -This drift is the strongest evidence that the duplication is harmful — the tools have silently diverged in path resolution and cwd handling. `get_sim_app_path.ts` doesn't resolve relative paths or set cwd, while `get_device_app_path.ts` does. This is almost certainly unintentional. - -All three could delegate to `resolveAppPathFromBuildSettings` (or a slightly extended version) instead of inline construction. - -### Phase 4 — Adjacent Duplication: `clean.ts` - -**Hypothesis:** `clean.ts` duplicates `executeXcodeBuildCommand`. - -**Evidence:** `clean.ts` (lines 127-153) builds: -- project/workspace with path resolution -- scheme, configuration -- destination via `constructDestinationString` -- derivedDataPath, extraArgs -- `clean` action - -This overlaps ~80% with `executeXcodeBuildCommand`. However, `executeXcodeBuildCommand` includes xcodemake logic, test-specific flags, and build pipeline integration that `clean` should NOT inherit. - -**Notable issue:** `clean.ts` line 115: `const scheme = params.scheme ?? '';` followed by `command.push('-scheme', scheme)` — this can emit `-scheme ""` which is suboptimal. A shared helper would prevent this kind of drift. - -**Conclusion:** Merging into `executeXcodeBuildCommand` would be wrong. But extracting a small shared helper for the common "resolve paths + append project/workspace/scheme/config/destination/derivedData/extraArgs" pattern would reduce this risk. - -### Phase 5 — Intentionally Local Builders - -**Hypothesis:** Discovery/inspection commands are local for good reasons. - -**Evidence:** -- `list_schemes.ts`: Only needs `-list` + project/workspace (2 args). Minimal surface. -- `show_build_settings.ts`: Only needs `-showBuildSettings` + project/workspace + scheme (3 args). Minimal surface. -- `platform-detection.ts`: Needs `-showBuildSettings -scheme` + project/workspace, but arg ORDER differs (scheme before project). Intentional for specific parsing needs. -- `xcode-state-watcher.ts`: Needs `-showBuildSettings -scheme -skipPackageUpdates` + optional project/workspace. The `-skipPackageUpdates` is unique to this use case. - -**Conclusion:** These are different enough in semantics that forcing them through a universal builder would add complexity without reducing bugs. The shared surface (project/workspace toggle) is 2-4 lines — not worth abstracting. - -## Root Cause Analysis - -The claim is **partially valid but the proposed solution is wrong**. - -**What's true:** -- 3 `get_*_app_path` tools duplicate `-showBuildSettings` arg construction that already exists in `resolveAppPathFromBuildSettings` -- This duplication has caused behavioral drift (path resolution, cwd handling) -- `clean.ts` shares ~80% of its arg construction with `executeXcodeBuildCommand` - -**What's overstated:** -- The core build/test path (6 callers) is already centralized in `executeXcodeBuildCommand` -- Discovery/inspection tools are intentionally local with minimal shared surface -- Peripheral `-version` checks are trivial - -**What's wrong about the proposed fix:** -- A `XcodebuildCommandBuilder` with a fluent API would need to handle: build, test, build-for-testing, test-without-building, clean, -showBuildSettings, -list, -version, xcodemake fallback, test-specific flags, pipeline integration — all of which have different requirements -- This would create a god-object that's harder to understand than the current focused abstractions -- The current architecture of `executeXcodeBuildCommand` (action center) + `resolveAppPathFromBuildSettings` (query center) is a better decomposition - -## Recommendations - -### 1. Consolidate `get_*_app_path` tools onto `resolveAppPathFromBuildSettings` (HIGH VALUE) -- `get_sim_app_path.ts`, `get_device_app_path.ts`, `get_mac_app_path.ts` should delegate command construction to `resolveAppPathFromBuildSettings` or a slight extension of it -- This fixes the behavioral drift (path resolution, cwd) and removes ~60 lines of duplicated arg construction -- May need to extend `resolveAppPathFromBuildSettings` to support simulator destination strings (currently only handles generic/device destinations) - -### 2. Optionally extract a tiny shared helper for common args (LOW-MEDIUM VALUE) -A small function like: -```typescript -function resolveXcodebuildPaths(params: { projectPath?: string; workspacePath?: string }) { - // resolve to absolute, return { projectPath, workspacePath, projectDir } -} -``` -This could be reused by `clean.ts` and `resolveAppPathFromBuildSettings` to reduce the path resolution duplication. But this is minor — only worth doing if you're already touching these files. - -### 3. Do NOT build a XcodebuildCommandBuilder (RECOMMENDATION: SKIP) -- The current architecture is already well-decomposed -- A fluent builder would be over-engineering for the actual duplication that exists -- The fix is consolidation of 3 tools onto an existing abstraction, not a new abstraction - -## Preventive Measures - -- When adding new tools that run `xcodebuild -showBuildSettings`, check if `resolveAppPathFromBuildSettings` can be reused first -- The existing `SharedBuildParams` + `PlatformBuildOptions` type pattern works well — continue using it for new build actions From ab9afb8f024b7668d728db7c442d630bc7ceb4f9 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 9 Apr 2026 23:32:01 +0100 Subject: [PATCH 3/3] fix: use unique bundle ID for macOS MCPTest example project Changed from io.sentry.calculatorapp (which collides with the iOS Calculator example) to io.sentry.MCPTest.macOS. --- example_projects/macOS/MCPTest.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj index 23d6bf55..23b25555 100644 --- a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest.macOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -365,7 +365,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest.macOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0;