Skip to content

Conversation

@cameroncooke
Copy link
Collaborator

@cameroncooke cameroncooke commented Aug 15, 2025

Summary

This PR introduces a major architectural refactoring to resolve persistent "module is already linked" errors in CI tests. It establishes clear boundaries between server infrastructure, test utilities, and pure utilities, preventing test code from importing and initializing production server components like Sentry.

Beyond fixing the CI failures, this change significantly improves code quality by eliminating over 580 lines of duplicated code, standardizing environment variables, and resolving numerous TypeScript and ESLint errors.

Background/Details

Since the Sentry v10 integration, our CI pipeline has been plagued by 74 "module is already linked" errors, occurring exclusively on the Ubuntu runner. The root cause was an architectural flaw where our central src/utils/index.ts barrel file exported both test utilities and production server code.

This created a problematic import chain:
Testssrc/utils/index.tssrc/server/ code → Sentry Instrumentation

As a result, test suites inadvertently initialized the entire server stack, leading to module loading conflicts within Vitest's vmThreads environment on Linux.

Solution

This PR implements a robust, multi-faceted solution:

  1. Architectural Separation:

    • src/server/: A new dedicated directory for all server-side infrastructure, including tool-registry.ts, dynamic-tools.ts, generated-plugins.ts, and the new server bootstrap logic.
    • src/test-utils/: A new directory for test-only code. It contains pure mock executors and test execution logic (test-execution.ts) that have no dependencies on the production server.
    • src/utils/: This directory has been purified to contain only side-effect-free, context-agnostic utilities. It no longer exports any server or test-specific code.
  2. Lazy Sentry Integration & Async Server Bootstrap:

    • The server now bootstraps asynchronously (createServer in src/server/bootstrap.ts).
    • Sentry is loaded lazily using a dynamic import() in src/server/instrumentation.ts, ensuring it is only imported in production environments and never by tests.
    • The Sentry opt-out environment variable has been standardized to XCODEBUILDMCP_SENTRY_DISABLED.
  3. DRY Principle & Code Refactoring:

    • src/utils/axe-command.ts: A new shared utility was created to handle all axe command executions. This eliminated ~580 lines of duplicated logic across 10+ UI testing tools.
    • src/utils/system-info.ts: A new shared module was created to centralize system/environment discovery logic, now used by both the doctor tool and Sentry instrumentation.
  4. Code Quality and Build Fixes:

    • Resolved 3 TypeScript compilation errors.
    • Fixed 16 ESLint violations related to unsafe types and other best practices.
    • Updated eslint.config.js to correctly ignore all generated-*.ts files, preventing linting errors in auto-generated code.

Testing

  • CI Verification: The primary validation is the successful resolution of all 74 "module is already linked" errors. The CI pipeline now passes consistently on Ubuntu runners.
  • Manual Regression Testing: Performed comprehensive end-to-end testing of all 59 MCP tools using the Reloaderoo inspector client. This verified that the extensive refactoring did not introduce any regressions in tool functionality.
  • Unit Test Updates: Updated unit tests, particularly for the doctor tool, to align with the new dependency injection patterns and module structure.

Notes

This is a foundational architectural improvement that not only fixes a critical CI issue but also makes the codebase more modular, maintainable, and resilient against future contamination between test and production code paths.

Summary by CodeRabbit

  • New Features

    • Simulator tools accept simulatorName in addition to UUID; unified simulator launch accepts either.
  • Refactor

    • Several simulator and macOS tool names consolidated/renamed; redundant launch variant removed.
  • Documentation

    • Streamlined tools list and counts (59 canonical, 81 total); removed legacy "Key Changes" section and updated examples/last-updated metadata.
  • Tests

    • New deterministic mock executor/file-system utilities added; tests updated to use them.
  • Chores

    • Removed legacy verification documents.

@cameroncooke
Copy link
Collaborator Author

Cursor review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Important

Review skipped

More than 25% of the files skipped due to max files limit. The review is being skipped to prevent a low-quality review.

83 files out of 190 files are above the max files limit of 100. Please upgrade to Pro plan to get higher limits.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Consolidates executor and filesystem types into focused modules, adds deterministic test mock executors, refactors many imports to new focused facades, streamlines TOOLS.md and generator, removes verification artifacts, and hardens/enhances the LLM-driven discover_tools control flow and sanitization.

Changes

Cohort / File(s) Summary
Docs consolidation
docs/TOOLS.md, scripts/update-tools-docs.ts
Tool counts/names updated, “Key Changes (v1.11+)” removed, generator no longer emits that section, trailing newline trimmed; TOOLS.md metadata updated.
New test utilities
src/test-utils/mock-executors.ts
Added deterministic mock CommandExecutor/FileSystemExecutor factories (noop, command-matching, overrides) and mock environment detector for tests.
Executor & FS typed contracts
src/utils/CommandExecutor.ts, src/utils/FileSystemExecutor.ts
Added typed public interfaces for CommandExecutor/CommandResponse and FileSystemExecutor (type-only modules, no runtime logic).
Execution facade & re-exports
src/utils/execution/index.ts, src/utils/command.ts, src/utils/index.ts
New focused facade re-exporting default executors and types; src/utils/command.ts now re-exports types; src/utils/index.ts adds executor exports and includes deprecation header; mock helpers removed from the old barrel.
Focused facades & response/logging facades
src/utils/logging/index.ts, src/utils/responses/index.ts, src/utils/validation/index.ts, src/utils/axe/index.ts, src/utils/template/index.ts, src/utils/version/index.ts, src/utils/plugin-registry/index.ts, src/utils/log-capture/index.ts, src/utils/test/index.ts
Added multiple small facade entrypoints that re-export focused utilities (logging, responses, validation, axe, template, version, plugin-registry, log-capture, test).
Logger runtime change
src/utils/logger.ts
Lazy-loads Sentry via createRequire, adds test-environment suppression, safe withSentry wrapper; preserves console behavior.
LLM-driven discovery logic
src/mcp/tools/discovery/discover_projs.ts, src/mcp/tools/discovery/discover_tools.ts
discover_toolsLogic hardened: input sanitization, LLM config, robust parsing, dependency-injectable workflow retrieval/enabling, better error handling and user messages.
Many production import refactors (focused facades)
src/mcp/** (numerous tool/resource modules)
Replaced barrel imports with focused facades and type-only executor imports across many mcp/tool and resource modules (logging, execution, responses, validation, build, axe helpers). No functional changes besides import/module boundaries.
Filesystem executor import adjustments
src/utils/validation.ts, src/utils/template-manager.ts, various mcp modules
FileSystemExecutor now imported from src/utils/FileSystemExecutor.ts (separated from previous barrel).
Tests import updates
src/**/__tests__/*, src/utils/__tests__/*
Tests updated to import mocks from src/test-utils/mock-executors.ts instead of legacy helpers; mostly path/formatting changes only.
Verification artifacts removed
TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md, TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md.bak
Deleted verification report and its backup; doc artifacts removed.
ESLint rule
eslint.config.js
Added no-restricted-imports rule to prohibit barrel imports from utils/index.js and guide developers to focused facades.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant discover_toolsLogic
  participant Sanitizer as sanitizeTaskDescription
  participant LLM
  participant Parser as parseLLMResponse
  participant Enabler as enableWorkflows
  participant Server

  User->>discover_toolsLogic: request with task_description
  discover_toolsLogic->>Sanitizer: sanitize input
  Sanitizer-->>discover_toolsLogic: sanitized text
  discover_toolsLogic->>LLM: send prompt (configurable maxTokens/temperature)
  LLM-->>discover_toolsLogic: sampling result
  discover_toolsLogic->>Parser: extract and validate JSON list
  Parser-->>discover_toolsLogic: workflow list / error
  discover_toolsLogic->>Enabler: enableWorkflows(server, selections)
  Enabler-->>discover_toolsLogic: success
  discover_toolsLogic-->>User: success message (or error guidance)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I hop through code in morning light,
Types sorted neat, imports polite.
Mocks in pockets, tests do cheer,
Docs trimmed tiny, pathways clear.
A rabbit's nibble — tidy, bright. 🐇"

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/module-import-bug

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@claude
Copy link
Contributor

claude bot commented Aug 15, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new directory structure (src/server/, src/test-utils/, src/utils/) creates clear boundaries between production server code, testing utilities, and pure utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Lazy Loading: The Sentry integration uses dynamic imports (await import('@sentry/node')) to prevent module loading in test environments

2. DRY Principle Implementation

  • axe-command.ts: Brilliant consolidation eliminating ~580 lines of duplicated code across UI testing tools. The shared executeAxeCommand function with proper error handling is a significant improvement
  • system-info.ts: Centralizes system/environment discovery logic, reducing duplication between doctor tool and Sentry instrumentation

3. Testing Architecture Improvements

  • Mock Executors: src/test-utils/mock-executors.ts provides clean, dependency-free testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy, avoiding vi.mock() anti-patterns
  • Call Tracking: Mock executors include sophisticated call tracking for test assertions

4. Code Quality Fixes

  • ESLint Configuration: Line 9 correctly ignores all generated-*.ts files, preventing linting errors in auto-generated code
  • Environment Variables: Standardized Sentry configuration (XCODEBUILDMCP_SENTRY_DISABLED)
  • Error Handling: Comprehensive error boundaries with graceful degradation when Sentry unavailable

⚠️ Areas for Consideration

1. Potential Import Issues

In src/index.ts:17, the Sentry import is still synchronous:

import './utils/sentry.js';

While this may work due to the refactored sentry.ts being minimal, consider whether this should also be lazy-loaded to prevent any potential module linking issues.

2. Global State Management

src/index.ts:68 sets a global MCP server reference:

(globalThis as { mcpServer?: McpServer }).mcpServer = server;

This pattern can complicate testing and create implicit dependencies. Consider using a more explicit dependency injection approach or service locator pattern.

3. Bootstrap Pattern Complexity

The src/server/bootstrap.ts uses both option-based and environment-based configuration determination (lines 42-45). Consider standardizing on one approach for consistency.

4. Symbol-based Idempotency

src/server/instrumentation.ts:21 uses a Symbol for tracking wrapped servers:

const SENTRY_WRAPPED_SYMBOL = Symbol.for('xbuild.sentryWrapped');

While functional, this adds complexity. Consider whether the lazy loading alone is sufficient protection.

🔧 Minor Technical Issues

Import Optimization

Several UI testing tools have redundant imports that could be consolidated:

// In button.ts, describe_ui.ts, etc.
import { createAxeNotAvailableResponse, getAxePath, getBundledAxeEnvironment } from '../../../utils/index.js';

These could be imported from the new axe-command.ts utility directly.

Type Safety

The mock executor type definitions in src/test-utils/mock-executors.ts could be strengthened with more specific return types for better test type safety.

🚀 Performance & Security Assessment

Performance

  • ✅ Lazy Sentry loading reduces startup overhead
  • ✅ Shared axe command utility reduces memory footprint
  • ✅ Clean module boundaries improve bundle optimization

Security

  • ✅ No hardcoded credentials or sensitive data exposed
  • ✅ Proper error handling prevents information leakage
  • ✅ Environment variable validation prevents injection attacks

📋 Test Coverage Assessment

The testing approach is exemplary:

  • ✅ Doctor tool tests demonstrate proper dependency injection patterns
  • ✅ Mock dependencies provide comprehensive test coverage scenarios
  • ✅ No reliance on Vitest mocking maintains test reliability
  • ✅ Literal validation ensures interface contracts are maintained

🎯 Recommendation

APPROVE - This is a high-quality refactoring that successfully:

  1. ✅ Fixes the critical CI module linking issues
  2. ✅ Dramatically improves code organization and maintainability
  3. ✅ Reduces technical debt through DRY principle application
  4. ✅ Maintains excellent test coverage with robust patterns
  5. ✅ Follows established project architectural principles

The architectural separation is clean, the dependency injection patterns are consistently applied, and the lazy loading approach effectively prevents the original module linking issues. The consolidation of duplicated code provides immediate maintenance benefits.

Minor suggestions above can be addressed in future iterations without blocking this critical fix.


@claude
Copy link
Contributor

claude bot commented Aug 15, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Progress:

  • Analyze new directory structure and module separation
  • Review Sentry lazy loading implementation
  • Examine DRY principle implementations (axe-command.ts, system-info.ts)
  • Assess test architecture improvements
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new directory structure (src/server/, src/test-utils/, src/utils/) creates clear boundaries between production server code, testing utilities, and pure utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Lazy Loading: The Sentry integration uses dynamic imports (await import('@sentry/node')) to prevent module loading in test environments

2. DRY Principle Implementation

  • axe-command.ts: Brilliant consolidation eliminating ~580 lines of duplicated code across UI testing tools. The shared executeAxeCommand function with proper error handling is a significant improvement
  • system-info.ts: Centralizes system/environment discovery logic, reducing duplication between doctor tool and Sentry instrumentation

3. Testing Architecture Improvements

  • Mock Executors: src/test-utils/mock-executors.ts provides clean, dependency-free testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy, avoiding vi.mock() anti-patterns
  • Call Tracking: Mock executors include sophisticated call tracking for test assertions

4. Code Quality Fixes

  • ESLint Configuration: Line 9 correctly ignores all generated-*.ts files, preventing linting errors in auto-generated code
  • Environment Variables: Standardized Sentry configuration (XCODEBUILDMCP_SENTRY_DISABLED)
  • Error Handling: Comprehensive error boundaries with graceful degradation when Sentry unavailable

⚠️ Areas for Consideration

1. Potential Import Issues

In src/index.ts:17, the Sentry import is still synchronous:

import './utils/sentry.js';

While this may work due to the refactored sentry.ts being minimal, consider whether this should also be lazy-loaded to prevent any potential module linking issues.

2. Global State Management

src/index.ts:68 sets a global MCP server reference:

(globalThis as { mcpServer?: McpServer }).mcpServer = server;

This pattern can complicate testing and create implicit dependencies. Consider using a more explicit dependency injection approach or service locator pattern.

3. Bootstrap Pattern Complexity

The src/server/bootstrap.ts uses both option-based and environment-based configuration determination (lines 42-45). Consider standardizing on one approach for consistency.

4. Symbol-based Idempotency

src/server/instrumentation.ts:21 uses a Symbol for tracking wrapped servers:

const SENTRY_WRAPPED_SYMBOL = Symbol.for('xbuild.sentryWrapped');

While functional, this adds complexity. Consider whether the lazy loading alone is sufficient protection.

🔧 Minor Technical Issues

Import Optimization

Several UI testing tools have redundant imports that could be consolidated:

// In button.ts, describe_ui.ts, etc.
import { createAxeNotAvailableResponse, getAxePath, getBundledAxeEnvironment } from '../../../utils/index.js';

These could be imported from the new axe-command.ts utility directly.

Type Safety

The mock executor type definitions in src/test-utils/mock-executors.ts could be strengthened with more specific return types for better test type safety.

🚀 Performance & Security Assessment

Performance

  • ✅ Lazy Sentry loading reduces startup overhead
  • ✅ Shared axe command utility reduces memory footprint
  • ✅ Clean module boundaries improve bundle optimization

Security

  • ✅ No hardcoded credentials or sensitive data exposed
  • ✅ Proper error handling prevents information leakage
  • ✅ Environment variable validation prevents injection attacks

📋 Test Coverage Assessment

The testing approach is exemplary:

  • ✅ Doctor tool tests demonstrate proper dependency injection patterns
  • ✅ Mock dependencies provide comprehensive test coverage scenarios
  • ✅ No reliance on Vitest mocking maintains test reliability
  • ✅ Literal validation ensures interface contracts are maintained

🎯 Recommendation

APPROVE - This is a high-quality refactoring that successfully:

  1. ✅ Fixes the critical CI module linking issues
  2. ✅ Dramatically improves code organization and maintainability
  3. ✅ Reduces technical debt through DRY principle application
  4. ✅ Maintains excellent test coverage with robust patterns
  5. ✅ Follows established project architectural principles

The architectural separation is clean, the dependency injection patterns are consistently applied, and the lazy loading approach effectively prevents the original module linking issues. The consolidation of duplicated code provides immediate maintenance benefits.

Minor suggestions above can be addressed in future iterations without blocking this critical fix.


cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🔭 Outside diff range comments (14)
README.md (3)

194-201: Fix typo in table header: “Samplng” → “Sampling”

User-facing docs should be typo-free.

Apply this diff:

-| Editor | Tools | Resources | Samplng |
+| Editor | Tools | Resources | Sampling |

130-131: Fix typo: “ammending” → “amending”

Minor documentation correction for professionalism.

Apply this diff:

-Configure your MCP client (Windsurf, Cursor, Claude Desktop, Claude Code etc.) to use the XcodeBuildMCP server by ammending your client application's MCP configuration.
+Configure your MCP client (Windsurf, Cursor, Claude Desktop, Claude Code etc.) to use the XcodeBuildMCP server by amending your client application's MCP configuration.

419-421: Fix typo: “MseeP.a” → “MseeP.ai”

Correct the link label to avoid confusion.

Apply this diff:

-### MseeP.a
+### MseeP.ai
src/utils/logger.ts (1)

46-48: Use the lazy Sentry capture to avoid hard dependency and keep logging non-blocking

This change eliminates the static Sentry import and ensures error logging doesn’t block execution.

Apply this diff:

-  if (level === 'error' && SENTRY_ENABLED) {
-    Sentry.captureMessage(logMessage);
-  }
+  if (level === 'error' && SENTRY_ENABLED) {
+    void captureErrorInSentry(logMessage);
+  }
src/server/tool-registry.ts (3)

121-128: Type the discoveryTools array to satisfy no-unsafe-argument and improve safety

Empty arrays default to any[], which violates the ESLint unsafe rules when passed to registerTools.

Apply this diff:

-  const discoveryTools = [];
+  const discoveryTools: Parameters<McpServer['registerTools']>[0] = [];

157-176: Type the selectedTools array for the same reason

Ensures type-safe bulk registration and keeps ESLint happy.

Apply this diff:

-  const selectedTools = [];
+  const selectedTools: Parameters<McpServer['registerTools']>[0] = [];

191-204: Type the allTools array to avoid implicit any

Consistent with the other arrays.

Apply this diff:

-  const allTools = [];
+  const allTools: Parameters<McpServer['registerTools']>[0] = [];
src/mcp/tools/discovery/discover_tools.ts (1)

24-31: Fix NaN handling for LLM env vars (can crash createMessage with invalid inputs)

If env values are set but non-numeric, parseInt/parseFloat produce NaN. You’ll pass NaN to the client LLM, likely causing runtime errors.

-const getLLMConfig = (): LLMConfig => ({
-  maxTokens: process.env.XCODEBUILDMCP_LLM_MAX_TOKENS
-    ? parseInt(process.env.XCODEBUILDMCP_LLM_TOKENS, 10)
-    : 200,
-  temperature: process.env.XCODEBUILDMCP_LLM_TEMPERATURE
-    ? parseFloat(process.env.XCODEBUILDMCP_LLM_TEMPERATURE)
-    : undefined,
-});
+const getLLMConfig = (): LLMConfig => {
+  const maxRaw = process.env.XCODEBUILDMCP_LLM_MAX_TOKENS;
+  const parsedMax = maxRaw !== undefined ? Number.parseInt(maxRaw, 10) : undefined;
+  const safeMax = Number.isFinite(parsedMax as number) && (parsedMax as number) > 0 ? (parsedMax as number) : 200;
+
+  const tempRaw = process.env.XCODEBUILDMCP_LLM_TEMPERATURE;
+  const parsedTemp = tempRaw !== undefined ? Number.parseFloat(tempRaw) : undefined;
+  const safeTemp = Number.isFinite(parsedTemp as number) ? (parsedTemp as number) : undefined;
+
+  return { maxTokens: safeMax, temperature: safeTemp };
+};
src/mcp/tools/simulator/test_sim.ts (1)

61-69: Missing XOR validation for simulatorId vs simulatorName (declared in docs, not enforced)

This can admit invalid states (neither or both), leading to ambiguous execution.

 const testSimulatorSchema = baseSchema
   .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
     message: 'Either projectPath or workspacePath is required.',
   })
   .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
     message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
-  });
+  })
+  .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
+    message: 'Either simulatorId or simulatorName is required.',
+  })
+  .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
+    message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
+  });
src/mcp/tools/ui-testing/long_press.ts (1)

127-149: Bug: describe_ui warning state is never updated, causing perpetual warnings

The map describeUITimestamps is never written to in this module (and it’s not exported), so getCoordinateWarning will always return the “describe_ui has not been called yet” warning. This is likely a regression from moving logic around.

Consider one of these fixes:

A) Minimal fix (export state and a setter from this module):

-const describeUITimestamps = new Map<string, DescribeUISession>();
+export const describeUITimestamps = new Map<string, DescribeUISession>();
 const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds

-function getCoordinateWarning(simulatorUuid: string): string | null {
+export function recordDescribeUiCall(simulatorUuid: string): void {
+  describeUITimestamps.set(simulatorUuid, { timestamp: Date.now(), simulatorUuid });
+}
+
+export function getCoordinateWarning(simulatorUuid: string): string | null {
   const session = describeUITimestamps.get(simulatorUuid);
   if (!session) {
     return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.';
   }

Then, in describe_ui.ts (or wherever describe_ui is implemented), record the event after a successful run:

// after successful describe_ui completion
import { recordDescribeUiCall } from './long_press.js'; // or better, from a shared module
recordDescribeUiCall(simulatorUuid);

B) Preferred: move this session tracking to a shared module (e.g., src/utils/ui-testing-session.ts) so multiple tools can read/write state without cross-tool coupling. I can draft that module and the call sites on request.

Would you like me to propose a small shared session module and patch describe_ui.ts to call it?

src/index.ts (1)

17-17: Remove static Sentry import in src/index.ts

To preserve the lazy-loading guarantees in src/server/instrumentation.ts, drop the top-level Sentry init in your entrypoint:

• File: src/index.ts
– Line 17: import './utils/sentry.js';

Proposed diff:

- // Import Sentry instrumentation
- import './utils/sentry.js';

Rely on maybeWrapWithSentry in src/server/bootstrap.ts to initialize Sentry at runtime. If you still need global logging hooks, wire them through the lazy loader or guard them by NODE_ENV/custom flags.

src/test-utils/test-execution.ts (1)

165-246: Always clean up the temporary directory (leak risk on early failures)

If executeXcodeBuildCommand throws before the xcresult parse block, tempDir isn’t removed. Move cleanup to a finally block and drop the duplicate rm calls in the inner branches.

Minimal refactor:

 export async function handleTestLogic(
   params: {
@@
-  try {
-    // Create temporary directory for xcresult bundle
-    const tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-'));
-    const resultBundlePath = join(tempDir, 'TestResults.xcresult');
+  let tempDir: string | undefined;
+  try {
+    // Create temporary directory for xcresult bundle
+    tempDir = await mkdtemp(join(tmpdir(), 'xcodebuild-test-'));
+    const resultBundlePath = join(tempDir, 'TestResults.xcresult');
@@
-      // Clean up temporary directory
-      await rm(tempDir, { recursive: true, force: true });
-
       // Return combined result - preserve isError from testResult (test failures should be marked as errors)
       const combinedResponse: ToolResponse = {
@@
-      // Clean up temporary directory even if parsing fails
-      try {
-        await rm(tempDir, { recursive: true, force: true });
-      } catch (cleanupError) {
-        log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
-      }
-
       return consolidateContentForClaudeCode(testResult);
     }
   } catch (error) {
@@
-  }
+  } finally {
+    if (tempDir) {
+      try {
+        await rm(tempDir, { recursive: true, force: true });
+      } catch (cleanupError) {
+        log('warn', `Failed to clean up temporary directory: ${cleanupError}`);
+      }
+    }
+  }
src/mcp/tools/ui-testing/touch.ts (1)

121-137: Unshared describe_ui session map causes constant warnings in touch.ts

The describeUITimestamps Map defined in src/mcp/tools/ui-testing/touch.ts (lines 121–137) is never updated, so getCoordinateWarning will always return a warning even after describe_ui runs. To resolve this:

• In src/mcp/tools/ui-testing/describe_ui.ts
– Export the shared session API:
ts export { recordDescribeUICall, describeUITimestamps };

• In each UI tool (touch.ts, tap.ts, swipe.ts, long_press.ts)
– Remove the local describeUITimestamps and getCoordinateWarning definitions.
– Import and use the shared describeUITimestamps Map and call recordDescribeUICall(simulatorUuid) in your handlers.

This ensures all coordinate‐based tools reference the same, updated timestamp map rather than their own dead code copies.

src/mcp/tools/ui-testing/type_text.ts (1)

35-39: Type mismatch: optional AxeHelpers passed to a required parameter

axeHelpers is optional here, but executeAxeCommand expects a non-optional AxeHelpers (with a default). Passing AxeHelpers | undefined can cause a TypeScript error under strict mode.

Apply this diff to provide a default AxeHelpers consistent with other tools (e.g., describe_ui):

 export async function type_textLogic(
   params: TypeTextParams,
   executor: CommandExecutor,
-  axeHelpers?: AxeHelpers,
+  axeHelpers: AxeHelpers = {
+    getAxePath,
+    getBundledAxeEnvironment,
+    createAxeNotAvailableResponse,
+  },
 ): Promise<ToolResponse> {
🧹 Nitpick comments (42)
README.md (1)

364-379: Env var rename looks good; consider clarifying accepted values

The rename to XCODEBUILDMCP_SENTRY_DISABLED and examples are consistent with the codebase. As a minor doc improvement, consider explicitly noting that only the literal string "true" disables Sentry (case-sensitive), and any other value or unset enables it. This prevents confusion for users who may try values like 1/yes/TRUE.

src/utils/logger.ts (1)

22-22: Make env parsing more robust (case-insensitive; support 1/yes)

If you want to align expectations with common patterns, accept multiple truthy values for disabling. Not required, but reduces surprises.

Example:

-const SENTRY_ENABLED = process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
+const disabled = String(process.env.XCODEBUILDMCP_SENTRY_DISABLED ?? '').toLowerCase();
+const SENTRY_ENABLED = !(disabled === 'true' || disabled === '1' || disabled === 'yes');
src/server/tool-registry.ts (1)

59-66: Include error stack when removal fails

Helps diagnosis without exposing PII beyond what’s already in the Error.

Apply this diff:

-  } catch (error) {
-    log('error', `❌ Failed to remove tool ${name}: ${error}`);
+  } catch (error) {
+    const details = error instanceof Error ? error.stack ?? error.message : String(error);
+    log('error', `❌ Failed to remove tool ${name}: ${details}`);
src/utils/system-info.ts (2)

20-24: Consider adding timeouts to execSync calls to avoid potential hangs

xcodebuild/xcrun calls should be near-instant, but misconfigured environments can hang. A small timeout improves robustness without changing behavior.

Apply this diff:

-    const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim();
+    const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8', timeout: 2000 }).trim();
-    const path = execSync('xcode-select -p', { encoding: 'utf8' }).trim();
-    const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8' }).trim();
+    const path = execSync('xcode-select -p', { encoding: 'utf8', timeout: 2000 }).trim();
+    const selectedXcode = execSync('xcrun --find xcodebuild', { encoding: 'utf8', timeout: 2000 }).trim();

68-73: Harden binary detection against shell injection and improve cross-platform support

Using execSync with a string (which spawns a shell) is fragile. Prefer execFileSync with explicit args and support Windows (“where”) to reduce false negatives and avoid injection when the input is not controlled.

Apply this diff:

-import { execSync } from 'child_process';
+import { execSync, execFileSync } from 'child_process';
@@
 export function checkBinaryAvailability(binary: string): { available: boolean; version?: string } {
   try {
-    execSync(`which ${binary}`, { stdio: 'ignore' });
+    if (process.platform === 'win32') {
+      execFileSync('where', [binary], { stdio: 'ignore' });
+    } else {
+      execFileSync('which', [binary], { stdio: 'ignore' });
+    }
   } catch {
     return { available: false };
   }
 
   let version: string | undefined;
-  const versionCommands: Record<string, string> = {
-    axe: 'axe --version',
-    mise: 'mise --version',
-  };
+  const versionArgsMap: Record<string, string[]> = {
+    axe: ['--version'],
+    mise: ['--version'],
+  };
 
-  if (binary in versionCommands) {
+  if (binary in versionArgsMap) {
     try {
-      version = execSync(versionCommands[binary], {
-        encoding: 'utf8',
-        stdio: ['ignore', 'pipe', 'ignore'],
-      }).trim();
+      version = execFileSync(binary, versionArgsMap[binary], {
+        encoding: 'utf8',
+        stdio: ['ignore', 'pipe', 'ignore'],
+      }).trim();
     } catch {
       // Version command failed, but binary exists
     }
   }

Also applies to: 75-87

src/utils/sentry.ts (2)

12-30: Guard Sentry initialization via the standardized env var

Given the new XCODEBUILDMCP_SENTRY_DISABLED flag and the intent to initialize Sentry only in production paths, add a local guard to prevent accidental init if this file is imported elsewhere.

Apply this diff:

-Sentry.init({
+const SENTRY_ENABLED = process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
+
+if (SENTRY_ENABLED) Sentry.init({
   dsn:
     process.env.SENTRY_DSN ??
     'https://798607831167c7b9fe2f2912f5d3c665@o4509258288332800.ingest.de.sentry.io/4509258293837904',
@@
-  tracesSampleRate: 1.0,
+  tracesSampleRate: 1.0,
 });

And wrap the tag/context setup at the bottom similarly:

-const axeAvailable = checkBinaryAvailability('axe');
+if (SENTRY_ENABLED) {
+  const axeAvailable = checkBinaryAvailability('axe');
  // ...rest of setup...
-}
+  Sentry.setTags(tags);
+}

Please verify the server/instrumentation layer doesn’t already provide an equivalent guard to avoid double checks.


37-49: Move large environment data from tags to context to avoid tag truncation and PII leaks

Sentry tags are truncated and intended for faceting. PATH, HOME, USER, etc., are better in contexts/extras. Keep a minimal set of tags; move the full map to a context.

Apply this diff:

-const tags: Record<string, string> = {
+const tags: Record<string, string> = {
   nodeVersion: process.version,
   platform: process.platform,
   arch: process.arch,
   axeAvailable: axeAvailable.available ? 'true' : 'false',
   axeVersion: axeAvailable.version ?? 'Unknown',
   miseAvailable: miseAvailable.available ? 'true' : 'false',
   miseVersion: miseAvailable.version ?? 'Unknown',
-  ...Object.fromEntries(Object.entries(envVars).map(([k, v]) => [`env_${k}`, v ?? ''])),
   xcodeVersion: xcodeInfo.version ?? 'Unknown',
   xcodePath: xcodeInfo.path ?? 'Unknown',
 };
 
 Sentry.setTags(tags);
+Sentry.setContext('environment', envVars);

Also applies to: 51-51

src/utils/axe-command.ts (2)

60-61: Bundled AXe detection is brittle; rely on an explicit signal instead of string comparison

Using axeBinary !== 'axe' to infer “bundled” will misclassify absolute paths to a system-installed axe (e.g., /usr/local/bin/axe). Prefer returning an isBundled boolean from the helper.

Proposed API change:

  • Update getAxePath() to return { path: string, isBundled: boolean }.
  • Consume it here to select the environment only when isBundled is true.

Example diff (assuming helper returns an object):

-  const axeBinary = axeHelpers.getAxePath();
+  const axeInfo = axeHelpers.getAxePath();
+  const axeBinary = typeof axeInfo === 'string' ? axeInfo : axeInfo?.path;
   if (!axeBinary) {
     throw new DependencyError('AXe binary not found');
   }
@@
-    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
+    const isBundled = typeof axeInfo === 'object' ? axeInfo.isBundled : axeBinary !== 'axe';
+    const axeEnv = isBundled ? axeHelpers.getBundledAxeEnvironment() : undefined;

I can propagate the type change and usages if you want.


43-44: Nit: unify the “AXe” label casing

Minor consistency tweak: use a single casing (e.g., “[AXE]”) across logs and docs.

-  logPrefix: string = '[AXe]',
+  logPrefix: string = '[AXE]',
docs/TOOLS.md (2)

41-43: Fix grammar for stop_app_sim entry

Same stray “or” pattern as above.

-- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
+- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. Example: stop_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })

124-127: Bullet list formatting is off; break items onto separate lines

LanguageTool flags likely stem from collapsed list items. Split them to one-per-line.

-- **Total Tools**: 59 canonical tools + 22 re-exports = 81 total
-- **Workflow Groups**: 12
-- **Analysis Method**: Static AST parsing with TypeScript compiler API
+- **Total Tools**: 59 canonical tools + 22 re-exports = 81 total
+- **Workflow Groups**: 12
+- **Analysis Method**: Static AST parsing with TypeScript compiler API

Also applies to: 130-130

src/mcp/tools/discovery/discover_tools.ts (2)

6-11: Prefer importing from the server barrel for stability and simpler pathing

This keeps import paths resilient to internal file moves and matches the PR’s new server barrel pattern.

-import {
-  enableWorkflows,
-  getAvailableWorkflows,
-  generateWorkflowDescriptions,
-} from '../../../server/dynamic-tools.js';
+import {
+  enableWorkflows,
+  getAvailableWorkflows,
+  generateWorkflowDescriptions,
+} from '../../../server/index.js';

270-283: Enforce “EXACTLY ONE” workflow selection to align with the prompt

Currently multiple selections are allowed if the LLM returns >1. Either constrain to one or reject with a helpful message.

       selectedWorkflows = parsedResponse;
 
       // Validate that all selected workflows are valid
       const validWorkflows = selectedWorkflows.filter((workflow) =>
         workflowNames.includes(workflow),
       );
       if (validWorkflows.length !== selectedWorkflows.length) {
         const invalidWorkflows = selectedWorkflows.filter(
           (workflow) => !workflowNames.includes(workflow),
         );
         log('warn', `LLM selected invalid workflows: ${invalidWorkflows.join(', ')}`);
         selectedWorkflows = validWorkflows;
       }
+
+      // Enforce the UX contract: exactly one workflow
+      if (selectedWorkflows.length > 1) {
+        log('warn', `LLM returned multiple workflows; selecting the first: ${selectedWorkflows[0]}`);
+        selectedWorkflows = [selectedWorkflows[0]];
+      }
src/mcp/tools/doctor/doctor.ts (1)

221-223: Sentry env var rename LGTM; make boolean parsing more robust

Support common truthy forms ('true'/'1'/'yes') and different casings to reduce surprises.

-    `- Sentry enabled: ${doctorInfo.environmentVariables.XCODEBUILDMCP_SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
+    `- Sentry enabled: ${
+      /^(true|1|yes)$/i.test(String(doctorInfo.environmentVariables.XCODEBUILDMCP_SENTRY_DISABLED))
+        ? '❌ No'
+        : '✅ Yes'
+    }`,
src/mcp/tools/simulator/test_sim.ts (1)

79-83: Use consistent log level key: 'warn' instead of 'warning'

Logger elsewhere uses 'warn'. Using 'warning' may drop severity handling/filtering.

-    log(
-      'warning',
-      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
-    );
+    log('warn', 'useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)');
src/server/dynamic-tools.ts (1)

153-163: Minor typing improvement opportunity (optional)

Avoid broad casts by tightening the ToolRegistration config type to accept ZodTypeAny (or the plugin meta’s exact schema type), removing the need for as unknown.

If the registration API can be typed to accept inputSchema: ZodTypeAny | undefined, you can drop:

inputSchema: tool.config.inputSchema as unknown

and keep the actual schema type for downstream tooling/validation.

src/mcp/tools/ui-testing/tap.ts (1)

41-57: “Shared across UI tools” map isn’t actually shared across modules

This local Map won’t reflect calls made from describe_ui.ts. Consider moving session state to a shared module or a well-namespaced global.

Two lightweight options:

  • Shared module: create utils/ui-session.ts that exports a singleton Map and import it from all UI tools.
  • Global: use a namespaced global to share across modules:
// in a shared module, then import from all UI tools
export const describeUITimestamps =
  ((globalThis as any).__xcodebuildmcpDescribeUITimestamps as Map<string, { timestamp: number }>) ??
  ((globalThis as any).__xcodebuildmcpDescribeUITimestamps = new Map<string, { timestamp: number }>());

Happy to draft the shared module and update describe_ui.ts and others.

src/mcp/tools/ui-testing/key_press.ts (1)

30-34: Deduplicate AxeHelpers by importing the shared type

This interface is re-declared here; import the shared type to keep things DRY and consistent with other files (e.g., type_text.ts).

Apply this minimal diff to remove the local interface:

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

And add this import near the top (no diff here since it’s outside the selected range):

import type { AxeHelpers } from '../../../utils/axe-command.js';
src/mcp/tools/ui-testing/long_press.ts (2)

38-42: Deduplicate AxeHelpers by importing the shared type

Same as other tools: import the AxeHelpers type from utils instead of redefining it.

Apply this diff to remove the local interface:

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

Add this import at the top:

import type { AxeHelpers } from '../../../utils/axe-command.js';

58-59: Minor: unnecessary Number() coercion

duration is already validated as a number by zod; Number(duration) is redundant.

-const delayInSeconds = Number(duration) / 1000; // Convert ms to seconds
+const delayInSeconds = duration / 1000; // Convert ms to seconds
src/mcp/tools/ui-testing/button.ts (1)

36-40: Import AxeHelpers type rather than redefining it

Keep AxeHelpers canonical by importing the type from utils to avoid drift across files.

Remove the local interface:

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

Add this import at the top:

import type { AxeHelpers } from '../../../utils/axe-command.js';
src/mcp/tools/ui-testing/key_sequence.ts (1)

36-41: Unify AxeHelpers type by importing it from utils (avoid local duplication).

Defining AxeHelpers locally is structurally fine, but it duplicates the contract that already exists in the utils layer. Importing the type keeps the contract single-sourced and prevents drift.

Apply this diff to remove the local interface:

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

Add this type-only import (outside the selected range):

// near other imports
import type { AxeHelpers } from '../../../utils/axe-command.js';
src/mcp/tools/ui-testing/swipe.ts (1)

89-93: Include all timing options in the user-facing message (nit).

Only duration is reflected in optionsText; delta, preDelay, and postDelay are ignored in the message even when provided. Consider reflecting them for clarity.

-  const optionsText = duration ? ` duration=${duration}s` : '';
+  const parts: string[] = [];
+  if (duration !== undefined) parts.push(`duration=${duration}s`);
+  if (delta !== undefined) parts.push(`delta=${delta}`);
+  if (preDelay !== undefined) parts.push(`preDelay=${preDelay}ms`);
+  if (postDelay !== undefined) parts.push(`postDelay=${postDelay}ms`);
+  const optionsText = parts.length ? ` ${parts.join(' ')}` : '';
src/mcp/tools/ui-testing/gesture.ts (3)

12-19: Consistent typing: import shared AxeHelpers type instead of redefining locally

To avoid type drift across tools and centralize the contract, prefer importing the shared AxeHelpers type from the axe-command module rather than redefining it here.

Apply this addition to your imports:

+import type { AxeHelpers } from '../../../utils/axe-command.js';

85-90: Remove locally duplicated AxeHelpers interface

This local interface duplicates the source of truth from utils/axe-command.ts. Import the type instead (as suggested above) to keep things consistent across files.

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

139-143: Improve error log message for unknown/error types

Interpolating the raw error can result in “[object Object]”. This aligns with touch.ts and improves observability.

-  log('error', `${LOG_PREFIX}/${toolName}: Failed - ${error}`);
+  log(
+    'error',
+    `${LOG_PREFIX}/${toolName}: Failed - ${
+      error instanceof Error ? error.message : String(error)
+    }`,
+  );
src/test-utils/test-execution.ts (2)

57-72: Prefer execFile/spawn over shell-quoted exec for robustness

exec with a composed string risks quoting issues. Using execFile (or spawn) with arguments is safer and avoids shell interpretation.

Example replacement:

import { execFile } from 'child_process';
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync('xcrun', [
  'xcresulttool',
  'get',
  'test-results',
  'summary',
  '--path',
  resultBundlePath,
]);

12-15: Prefer static import for stat (consistency and minor perf win)

You already import from fs/promises. Pull stat in statically and drop the dynamic import.

-import { mkdtemp, rm } from 'fs/promises';
+import { mkdtemp, rm, stat } from 'fs/promises';
-      try {
-        const { stat } = await import('fs/promises');
-        await stat(resultBundlePath);
-        log('info', `xcresult bundle exists at: ${resultBundlePath}`);
-      } catch {
+      try {
+        await stat(resultBundlePath);
+        log('info', `xcresult bundle exists at: ${resultBundlePath}`);
+      } catch {
         log('warn', `xcresult bundle does not exist at: ${resultBundlePath}`);
         throw new Error(`xcresult bundle not found at ${resultBundlePath}`);
       }

Also applies to: 198-201

src/utils/index.ts (1)

26-28: Consider re-exporting AxeHelpers type for consumers

Many tools/types now depend on AxeHelpers. Re-exporting it from index helps avoid deep import paths and improves consistency.

 export type { ToolResponse } from '../types/common.js';
+export type { AxeHelpers } from './axe-command.js';
src/mcp/tools/ui-testing/touch.ts (1)

88-90: Honor injected AxeHelpers for not-available response

Use the injected helpers when present to keep behavior overridable in tests and consistent with gesture.ts.

-      return createAxeNotAvailableResponse();
+      return axeHelpers?.createAxeNotAvailableResponse() ?? createAxeNotAvailableResponse();
src/server/bootstrap.ts (4)

43-46: Coerce env to boolean for clarity and correctness

process.env.SENTRY_DSN is a string; relying on its truthiness works but is brittle. Explicitly coerce to boolean for readability and to avoid accidental non-empty string issues.

Apply this diff:

-  const shouldInstrument =
-    options.instrumentation === 'sentry' ||
-    (options.instrumentation !== 'none' && process.env.SENTRY_DSN);
+  const shouldInstrument =
+    options.instrumentation === 'sentry' ||
+    (options.instrumentation !== 'none' && Boolean(process.env.SENTRY_DSN));

39-41: Log the effective version used by the server (respect deps.version)

When a custom version is passed via deps.version, the log still uses the imported version, which can be misleading.

Apply this diff:

   // Create base server
   const baseServer = createBaseServer(deps);
+  const effectiveVersion = deps.version ?? version;

51-51: Use the effective version in the log

This keeps the log consistent with the version actually applied to the server instance.

Apply this diff:

-    log('info', `Base server created without instrumentation (version ${version})`);
+    log('info', `Base server created without instrumentation (version ${effectiveVersion})`);

48-50: Remove unnecessary await in return

No try/catch around the awaited call; returning the promise directly is sufficient.

Apply this diff:

-    // Apply Sentry instrumentation
-    return await maybeWrapWithSentry(baseServer, options.sentry);
+    // Apply Sentry instrumentation
+    return maybeWrapWithSentry(baseServer, options.sentry);
src/mcp/tools/ui-testing/describe_ui.ts (2)

36-37: Type the session-tracking map for better safety

Provide explicit generics to avoid implicit any and make downstream usage safer.

Apply this diff:

-const describeUITimestamps = new Map();
+const describeUITimestamps = new Map<
+  string,
+  { timestamp: number; simulatorUuid: string }
+>();

63-72: Avoid the cast by tightening executeAxeCommand’s types

Casting the result to string works, but we can eliminate it by adding overloaded signatures to executeAxeCommand so TypeScript knows that returnOutput = true yields a Promise.

If you’d like, I can open a follow-up PR to adjust the shared utility. Example overloads (in src/utils/axe-command.ts):

// Overloads
export function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor?: CommandExecutor,
  axeHelpers?: AxeHelpers,
  logPrefix?: string,
  returnOutput?: false,
): Promise<void>;
export function executeAxeCommand(
  commandArgs: string[],
  simulatorUuid: string,
  commandName: string,
  executor: CommandExecutor | undefined,
  axeHelpers: AxeHelpers | undefined,
  logPrefix: string | undefined,
  returnOutput: true,
): Promise<string>;

// Implementation (existing) remains unchanged

This change would allow removing the “as string” here while keeping the same call-site shape.

src/test-utils/mock-executors.ts (3)

23-28: Expose call tracking in the return type for better DX in tests

Tests will likely want to access .calls and .lastCall, but the current return type (CommandExecutor) doesn’t surface those accessors. Add a dedicated MockCommandExecutor type that extends the function type with these properties.

Apply this diff:

 export type CommandExecutor = (
   command: string[],
   description?: string,
   allowFailure?: boolean,
 ) => Promise<MockCommandResult>;
+
+// Function type with call-tracking accessors for tests
+export type MockCommandExecutor = CommandExecutor & {
+  readonly calls: Array<{ command: string[]; description?: string; allowFailure?: boolean }>;
+  readonly lastCall?: { command: string[]; description?: string; allowFailure?: boolean };
+};

55-56: Return the enriched executor type from createMockExecutor

This exposes the accessor properties with proper typing.

Apply this diff:

-): CommandExecutor {
+): MockCommandExecutor {

98-99: Cast the returned function to the enriched type

Preserves the function signature while surfacing the accessors to TypeScript.

Apply this diff:

-  return executor;
+  return executor as MockCommandExecutor;
src/server/generated-plugins.ts (3)

6-47: Load tool modules in parallel to reduce workflow bootstrap latency

Each loader currently awaits imports sequentially, incurring N round-trips per workflow. Switching to Promise.all parallelizes the I/O and noticeably speeds up registry hydration. Example refactor for the device loader (apply this pattern in the generator so all workflows benefit):

-  device: async () => {
-    const { workflow } = await import('../mcp/tools/device/index.js');
-    const tool_0 = await import('../mcp/tools/device/build_device.js').then((m) => m.default);
-    const tool_1 = await import('../mcp/tools/device/clean.js').then((m) => m.default);
-    const tool_2 = await import('../mcp/tools/device/discover_projs.js').then((m) => m.default);
-    const tool_3 = await import('../mcp/tools/device/get_app_bundle_id.js').then((m) => m.default);
-    const tool_4 = await import('../mcp/tools/device/get_device_app_path.js').then(
-      (m) => m.default,
-    );
-    const tool_5 = await import('../mcp/tools/device/install_app_device.js').then((m) => m.default);
-    const tool_6 = await import('../mcp/tools/device/launch_app_device.js').then((m) => m.default);
-    const tool_7 = await import('../mcp/tools/device/list_devices.js').then((m) => m.default);
-    const tool_8 = await import('../mcp/tools/device/list_schemes.js').then((m) => m.default);
-    const tool_9 = await import('../mcp/tools/device/show_build_settings.js').then(
-      (m) => m.default,
-    );
-    const tool_10 = await import('../mcp/tools/device/start_device_log_cap.js').then(
-      (m) => m.default,
-    );
-    const tool_11 = await import('../mcp/tools/device/stop_app_device.js').then((m) => m.default);
-    const tool_12 = await import('../mcp/tools/device/stop_device_log_cap.js').then(
-      (m) => m.default,
-    );
-    const tool_13 = await import('../mcp/tools/device/test_device.js').then((m) => m.default);
-
-    return {
-      workflow,
-      build_device: tool_0,
-      clean: tool_1,
-      discover_projs: tool_2,
-      get_app_bundle_id: tool_3,
-      get_device_app_path: tool_4,
-      install_app_device: tool_5,
-      launch_app_device: tool_6,
-      list_devices: tool_7,
-      list_schemes: tool_8,
-      show_build_settings: tool_9,
-      start_device_log_cap: tool_10,
-      stop_app_device: tool_11,
-      stop_device_log_cap: tool_12,
-      test_device: tool_13,
-    };
-  },
+  device: async () => {
+    const [
+      workflow,
+      tool_0,
+      tool_1,
+      tool_2,
+      tool_3,
+      tool_4,
+      tool_5,
+      tool_6,
+      tool_7,
+      tool_8,
+      tool_9,
+      tool_10,
+      tool_11,
+      tool_12,
+      tool_13,
+    ] = await Promise.all([
+      import('../mcp/tools/device/index.js').then((m) => m.workflow),
+      import('../mcp/tools/device/build_device.js').then((m) => m.default),
+      import('../mcp/tools/device/clean.js').then((m) => m.default),
+      import('../mcp/tools/device/discover_projs.js').then((m) => m.default),
+      import('../mcp/tools/device/get_app_bundle_id.js').then((m) => m.default),
+      import('../mcp/tools/device/get_device_app_path.js').then((m) => m.default),
+      import('../mcp/tools/device/install_app_device.js').then((m) => m.default),
+      import('../mcp/tools/device/launch_app_device.js').then((m) => m.default),
+      import('../mcp/tools/device/list_devices.js').then((m) => m.default),
+      import('../mcp/tools/device/list_schemes.js').then((m) => m.default),
+      import('../mcp/tools/device/show_build_settings.js').then((m) => m.default),
+      import('../mcp/tools/device/start_device_log_cap.js').then((m) => m.default),
+      import('../mcp/tools/device/stop_app_device.js').then((m) => m.default),
+      import('../mcp/tools/device/stop_device_log_cap.js').then((m) => m.default),
+      import('../mcp/tools/device/test_device.js').then((m) => m.default),
+    ]);
+
+    return {
+      workflow,
+      build_device: tool_0,
+      clean: tool_1,
+      discover_projs: tool_2,
+      get_app_bundle_id: tool_3,
+      get_device_app_path: tool_4,
+      install_app_device: tool_5,
+      launch_app_device: tool_6,
+      list_devices: tool_7,
+      list_schemes: tool_8,
+      show_build_settings: tool_9,
+      start_device_log_cap: tool_10,
+      stop_app_device: tool_11,
+      stop_device_log_cap: tool_12,
+      test_device: tool_13,
+    };
+  },

312-312: Make WORKFLOW_LOADERS readonly to prevent accidental mutation and stabilize keyof inference

Small hardening that also preserves literal keys.

-export const WORKFLOW_LOADERS = {
+export const WORKFLOW_LOADERS = {-};
+} as const;

432-432: Optionally freeze WORKFLOW_METADATA with a const assertion

Prevents accidental mutation and preserves literal types for downstream consumers.

 export const WORKFLOW_METADATA = {
   …
-};
+} as const;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 29fb327 and b759710.

📒 Files selected for processing (36)
  • README.md (2 hunks)
  • docs/TOOLS.md (5 hunks)
  • eslint.config.js (1 hunks)
  • src/core/plugin-registry.ts (1 hunks)
  • src/index.ts (2 hunks)
  • src/mcp/tools/discovery/discover_tools.ts (1 hunks)
  • src/mcp/tools/doctor/__tests__/doctor.test.ts (2 hunks)
  • src/mcp/tools/doctor/doctor.ts (1 hunks)
  • src/mcp/tools/doctor/lib/doctor.deps.ts (4 hunks)
  • src/mcp/tools/simulator/test_sim.ts (1 hunks)
  • src/mcp/tools/ui-testing/button.ts (2 hunks)
  • src/mcp/tools/ui-testing/describe_ui.ts (2 hunks)
  • src/mcp/tools/ui-testing/gesture.ts (2 hunks)
  • src/mcp/tools/ui-testing/key_press.ts (2 hunks)
  • src/mcp/tools/ui-testing/key_sequence.ts (2 hunks)
  • src/mcp/tools/ui-testing/long_press.ts (2 hunks)
  • src/mcp/tools/ui-testing/swipe.ts (2 hunks)
  • src/mcp/tools/ui-testing/tap.ts (1 hunks)
  • src/mcp/tools/ui-testing/touch.ts (2 hunks)
  • src/mcp/tools/ui-testing/type_text.ts (2 hunks)
  • src/server/base-server.ts (1 hunks)
  • src/server/bootstrap.ts (1 hunks)
  • src/server/dynamic-tools.ts (1 hunks)
  • src/server/generated-plugins.ts (1 hunks)
  • src/server/index.ts (1 hunks)
  • src/server/instrumentation.ts (1 hunks)
  • src/server/server.ts (1 hunks)
  • src/server/tool-registry.ts (1 hunks)
  • src/test-utils/index.ts (1 hunks)
  • src/test-utils/mock-executors.ts (1 hunks)
  • src/test-utils/test-execution.ts (1 hunks)
  • src/utils/axe-command.ts (1 hunks)
  • src/utils/index.ts (1 hunks)
  • src/utils/logger.ts (1 hunks)
  • src/utils/sentry.ts (1 hunks)
  • src/utils/system-info.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (16)
src/server/base-server.ts (1)
src/server/server.ts (1)
  • createBaseServer (14-14)
src/utils/system-info.ts (2)
src/mcp/tools/doctor/lib/doctor.deps.ts (3)
  • getXcodeInfo (113-135)
  • getEnvironmentVariables (139-150)
  • checkBinaryAvailability (101-109)
src/mcp/tools/doctor/__tests__/doctor.test.ts (3)
  • getXcodeInfo (20-27)
  • getEnvironmentVariables (30-43)
  • checkBinaryAvailability (14-17)
src/server/bootstrap.ts (3)
src/server/instrumentation.ts (2)
  • SentryOptions (15-18)
  • maybeWrapWithSentry (34-83)
src/server/base-server.ts (2)
  • ServerDeps (15-18)
  • createBaseServer (29-48)
src/utils/logger.ts (1)
  • log (33-53)
src/server/instrumentation.ts (1)
src/utils/logger.ts (1)
  • log (33-53)
src/utils/axe-command.ts (6)
src/mcp/tools/ui-testing/button.ts (1)
  • AxeHelpers (36-40)
src/mcp/tools/ui-testing/describe_ui.ts (1)
  • AxeHelpers (27-31)
src/test-utils/mock-executors.ts (1)
  • CommandExecutor (23-27)
src/utils/command.ts (1)
  • getDefaultCommandExecutor (232-243)
src/utils/errors.ts (3)
  • DependencyError (127-136)
  • AxeError (97-108)
  • SystemError (57-66)
src/utils/logger.ts (1)
  • log (33-53)
src/mcp/tools/ui-testing/key_press.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/gesture.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/key_sequence.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/button.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/long_press.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/swipe.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/index.ts (1)
src/server/bootstrap.ts (1)
  • createServer (35-54)
src/mcp/tools/doctor/lib/doctor.deps.ts (2)
src/mcp/tools/doctor/__tests__/doctor.test.ts (3)
  • checkBinaryAvailability (14-17)
  • getXcodeInfo (20-27)
  • getEnvironmentVariables (30-43)
src/utils/system-info.ts (3)
  • checkBinaryAvailability (68-93)
  • getXcodeInfo (13-34)
  • getEnvironmentVariables (39-63)
src/mcp/tools/ui-testing/describe_ui.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/touch.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
src/mcp/tools/ui-testing/type_text.ts (1)
src/utils/axe-command.ts (1)
  • executeAxeCommand (37-98)
🪛 LanguageTool
docs/TOOLS.md

[grammar] ~38-~38: There might be a mistake here.
Context: ...pp in an iOS simulator by UUID or name. If simulator window isn't visible, use ope...

(QB_NEW_EN)


[grammar] ~124-~124: There might be a mistake here.
Context: ...nonical tools + 22 re-exports = 81 total - Workflow Groups: 12 - **Analysis Metho...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...rts = 81 total - Workflow Groups: 12 - Analysis Method: Static AST parsing wi...

(QB_NEW_EN)

🔇 Additional comments (44)
src/server/tool-registry.ts (2)

4-4: Import path update aligns with utils relocation

Good move consolidating logging under utils; keeps server modules lean.


176-178: Intentional non-tracking?

registerSelectedWorkflows uses server.registerTools without tracking, unlike discovery. If workflows may be reloaded/unloaded dynamically later, consider using registerAndTrackTools here as well. If static by design, please confirm and ignore.

src/core/plugin-registry.ts (1)

1-1: Type import relocation to server namespace makes sense

Centralizing plugin types under server/plugin-types.js matches the new architecture; no runtime changes. LGTM.

eslint.config.js (1)

9-10: Broader ignore pattern for generated files is appropriate

This will cover new generated artifacts like src/server/generated-plugins.ts and future generated files across src/. Good balance between safety and convenience.

src/utils/system-info.ts (1)

39-63: LGTM: environment capture is focused and consistent

Good balance between explicit keys and prefixed XCODEBUILDMCP_* enumeration. Returning empty strings avoids undefined surprises for downstream consumers.

src/utils/sentry.ts (1)

10-10: Nice: system-info helpers are centralized

Importing shared helpers reduces duplication and keeps Sentry wiring lean.

src/utils/axe-command.ts (1)

46-56: Overall: solid centralization and error mapping

Good dependency injection points, standardized udid handling, and clear error taxonomy (DependencyError/AxeError/SystemError). This will reduce duplicated logic across tools.

Also applies to: 58-85, 86-97

src/mcp/tools/doctor/__tests__/doctor.test.ts (1)

40-41: Env var rename to XCODEBUILDMCP_SENTRY_DISABLED looks correct

Tests reflect the standardized Sentry opt-out variable. Aligns with logger/system-info changes.

Also applies to: 263-264

src/mcp/tools/simulator/test_sim.ts (1)

10-12: Import split looks right and aligns with the new architecture

Moving handleTestLogic under test-utils and keeping log under utils matches the separation goals without altering behavior.

src/server/dynamic-tools.ts (1)

7-7: Import path update to server-local registry LGTM

This aligns dynamic-tools with the new server-scoped tool registry and avoids utils leakage.

src/mcp/tools/ui-testing/tap.ts (1)

6-13: Centralizing AXe command execution is a solid win

Converging on the shared executeAxeCommand removes duplication and keeps behavior consistent across UI tools.

src/test-utils/index.ts (2)

1-7: Clear, test-only surface is well-defined

Good documentation and separation from server code. This should help prevent the previous test/production coupling issues.


10-13: No re-export required for createMockToolResponse
Verified that createMockToolResponse is defined in src/test-utils/mock-executors.ts and is not imported from the test-utils barrel anywhere in the repo. Omitting it from src/test-utils/index.ts will not break existing tests.

If you do want to expose it on the public test-utils surface, you can add:

-export { createMockExecutor, createMockFileSystemExecutor } from './mock-executors.js';
+export { createMockExecutor, createMockFileSystemExecutor, createMockToolResponse } from './mock-executors.js';
src/mcp/tools/ui-testing/key_press.ts (1)

56-58: Centralized AXe execution usage looks good

Delegating to executeAxeCommand with a log prefix is consistent with the new utilities and reduces duplication.

src/mcp/tools/ui-testing/long_press.ts (1)

76-78: Good migration to shared AXe executor

Using executeAxeCommand with LOG_PREFIX aligns with the new centralized utility.

src/server/index.ts (2)

9-20: Barrel and boundary look good

This cleanly scopes server-only exports and should help avoid test-time linking issues.


4-7: No server imports detected in tests – CI linking issue addressed

I scanned all *.test.*, *.spec.*, and __tests__ files for imports from src/server/ and found none. Tests no longer reference server modules, so the “module is already linked” CI issue should be resolved.

src/mcp/tools/ui-testing/button.ts (1)

62-64: Good adoption of the centralized executeAxeCommand

This removes duplicate command execution logic and standardizes logging/error handling.

src/mcp/tools/ui-testing/key_sequence.ts (2)

16-17: Good move: centralized AXe execution via shared utility.

Switching to the shared executeAxeCommand removes duplicate logic and aligns this tool with the new utils layer.


65-73: Correct usage of executeAxeCommand with extended signature.

The updated call passes the log prefix and aligns with the shared util’s signature. Looks good.

src/mcp/tools/ui-testing/swipe.ts (2)

16-17: Centralizing AXe execution looks good.

Importing executeAxeCommand from the utils layer removes per-tool implementations and helps maintain DRYness.


95-97: Accurate adoption of the shared executor API.

Passing LOG_PREFIX ensures consistent log formatting across tools. No issues found.

src/index.ts (2)

41-41: Import path update to server/tool-registry is consistent with new architecture.

Matches the separation between server/test/utils modules. Looks good.


65-65: No additional createServer calls found
Only the single call in src/index.ts (the one already awaiting the Promise) and the definition in src/server/bootstrap.ts exist. No other call sites require updating.

src/server/instrumentation.ts (1)

44-54: Idempotency guard looks good.

The symbol-based guard prevents double wrapping and avoids accidental re-instrumentation. Nice touch.

src/server/base-server.ts (1)

29-48: Solid, side-effect-free server constructor.

This clean base aligns with the new architecture and is safe for tests. Capabilities configuration looks correct.

src/mcp/tools/ui-testing/gesture.ts (1)

129-136: LGTM: Centralized AXe execution call is correct

Delegating to executeAxeCommand with simulator UDID, command name, injected executor/helpers, and a log prefix matches the new shared utility signature. ReturnOutput isn’t needed here, so omitting it is appropriate.

src/server/server.ts (1)

10-14: LGTM: Backward-compatibility re-exports are clean and minimal

The wrapper clearly re-exports the new bootstrap/base APIs and documents the legacy layer. Using explicit .js extensions is consistent with ESM builds.

src/test-utils/test-execution.ts (2)

2-10: LGTM: Clear separation of concerns for test utilities

Header makes scope explicit (no server/MCP/Sentry). This helps avoid the prior “module already linked” contamination in CI.


17-23: LGTM: Imports now correctly source from pure utils

All imports come from ../utils/* and types/common, fitting the new boundaries.

src/utils/index.ts (3)

1-7: LGTM: Pure utilities boundary is clear

Header communicates the contract. This helps keep server/test code out of the utils surface.


16-16: LGTM: Re-exporting axe-command consolidates the new shared runner

This enables each UI tool to use the same execution path and error model.


21-22: LGTM: system-info exposure is useful for doctor/instrumentation without side effects

No server import leakage here.

src/mcp/tools/ui-testing/touch.ts (2)

16-21: LGTM: Centralizing AXe execution and importing shared AxeHelpers

This aligns with the new axe-command utility and keeps types consistent.


71-73: LGTM: executeAxeCommand invocation matches the shared signature

Passing LOG_PREFIX improves log readability and parity with other tools.

src/server/bootstrap.ts (1)

35-54: Overall bootstrap flow looks solid

Async server creation, clean separation between base server and optional instrumentation, and stdio startup are well-structured. The env-driven decision to enable Sentry aligns with the PR goals.

src/mcp/tools/ui-testing/describe_ui.ts (1)

61-94: Migration to shared executor is clean

Good use of the centralized AXe executor with LOG_PREFIX and returnOutput=true. Error handling and user-facing messaging remain consistent.

src/mcp/tools/ui-testing/type_text.ts (1)

51-55: Shared executor usage looks good

The centralized call with LOG_PREFIX is consistent with the refactor, and the error mapping remains correct.

src/mcp/tools/doctor/lib/doctor.deps.ts (2)

99-136: Solid dependency wiring and use of centralized system-info

Nice consolidation: checkBinaryAvailability, getXcodeInfo, and getEnvironmentVariables now use the shared utils, and you preserve xcrun version via executor. The binary check correctly short-circuits for bundled AXe.


187-193: Safer typing when extracting tool names

Explicitly typing the tool items and filtering falsy names is a good improvement that avoids undefined entries and keeps totals accurate.

src/test-utils/mock-executors.ts (1)

112-141: In-memory FS executor is straightforward and useful

Read/write/exists semantics are intuitive; rmdir prefix matching is sufficient for tests. No concerns.

src/server/generated-plugins.ts (3)

1-4: LGTM: Clear auto-generated banner and provenance comment

Header is clear and sets correct expectations for consumers/tools.


314-315: LGTM: WorkflowName derives directly from loader keys

This keeps the public name type in sync with the generated registry.


5-312: 🔎 Verify dynamic-imported modules export named workflow and default tool

Dynamic imports in src/server/generated-plugins.ts assume:

  • Each src/mcp/tools/**/index.ts exports a named workflow
  • Each non-index tool file under src/mcp/tools/** has a default export

Missing or mis-named exports will resolve to undefined at runtime. Please run a quick repo-wide check or manually verify:

  • For every path imported in generated-plugins.ts, confirm
    • src/mcp/tools/.../index.ts contains export const workflow, export function workflow, or export { workflow }
    • The corresponding src/mcp/tools/.../…tool… .ts file has export default

Comment on lines +38 to 31
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
- `list_sims` - Lists available iOS simulators with their UUIDs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix grammar and example formatting for launch_app_sim

There’s an extra “or” and a missing “Example:”.

Apply this diff:

-- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
+- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If the simulator window isn't visible, use open_sim() first. Example: launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
- `list_sims` - Lists available iOS simulators with their UUIDs.
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If the simulator window isn't visible, use open_sim() first. Example: launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
- `list_sims` - Lists available iOS simulators with their UUIDs.
🧰 Tools
🪛 LanguageTool

[grammar] ~38-~38: There might be a mistake here.
Context: ...pp in an iOS simulator by UUID or name. If simulator window isn't visible, use ope...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/TOOLS.md around lines 38 to 39, the description for `launch_app_sim`
contains an extra stray "or" and the example is missing the "Example:" label;
remove the extra "or" from the sentence and prepend "Example:" to the example
invocation so it reads like a labeled code example (e.g., Example:
`launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp'
})`), keeping `list_sims` unchanged.

Comment on lines 21 to 24
import { createAxeNotAvailableResponse } from '../../../utils/index.js';
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Re-introduce helper imports for the default AxeHelpers

To support the default object above, import the helpers from the shared utils barrel.

Apply this diff:

-import { createAxeNotAvailableResponse } from '../../../utils/index.js';
+import {
+  createAxeNotAvailableResponse,
+  getAxePath,
+  getBundledAxeEnvironment,
+} from '../../../utils/index.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createAxeNotAvailableResponse } from '../../../utils/index.js';
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/index.js';
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
🤖 Prompt for AI Agents
In src/mcp/tools/ui-testing/type_text.ts around lines 21 to 22, the default
AxeHelpers object used later relies on helper exports from the shared utils
barrel but those imports were removed; re-import the helper(s) from
'../../../utils/index.js' (for example add an import for AxeHelpers or the
specific helper names used by the default object) so the default AxeHelpers
reference resolves correctly.

Comment on lines 56 to 61
// Check if DSN is available
const dsn = options.dsn ?? process.env.SENTRY_DSN;
if (!dsn) {
log('info', 'No Sentry DSN configured, skipping instrumentation');
return server;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

DSN is checked but never used to initialize Sentry.

Sentry generally requires explicit initialization (Sentry.init({ dsn })) to function. Currently, dsn is only used for gating, not initialization.

You can initialize Sentry after the dynamic import:

   const dsn = options.dsn ?? process.env.SENTRY_DSN;
   if (!dsn) {
     log('info', 'No Sentry DSN configured, skipping instrumentation');
     return server;
   }

   try {
-    // Lazy import Sentry to avoid module linking in tests
-    log('info', 'Loading Sentry instrumentation...');
-    const Sentry = await import('@sentry/node');
-
-    // Wrap server with Sentry instrumentation
-    const instrumentedServer = Sentry.wrapMcpServerWithSentry(server);
+    // Lazy import Sentry to avoid module linking in tests
+    log('info', 'Loading Sentry instrumentation...');
+    const mod = await import('@sentry/node');
+    // Handle CJS/ESM interop
+    const Sentry: any = (mod as any)?.default ?? mod;
+    if (typeof Sentry.init === 'function') {
+      Sentry.init({ dsn });
+    } else {
+      log('info', 'Sentry SDK missing init(); skipping instrumentation');
+      return server;
+    }
+
+    // Wrap server with Sentry MCP instrumentation if available
+    if (typeof Sentry.wrapMcpServerWithSentry !== 'function') {
+      log('info', 'wrapMcpServerWithSentry not found; continuing without MCP instrumentation');
+      return server;
+    }
+    const instrumentedServer = Sentry.wrapMcpServerWithSentry(server);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if DSN is available
const dsn = options.dsn ?? process.env.SENTRY_DSN;
if (!dsn) {
log('info', 'No Sentry DSN configured, skipping instrumentation');
return server;
}
// Check if DSN is available
const dsn = options.dsn ?? process.env.SENTRY_DSN;
if (!dsn) {
log('info', 'No Sentry DSN configured, skipping instrumentation');
return server;
}
try {
// Lazy import Sentry to avoid module linking in tests
log('info', 'Loading Sentry instrumentation...');
const mod = await import('@sentry/node');
// Handle CJS/ESM interop
const Sentry: any = (mod as any)?.default ?? mod;
if (typeof Sentry.init === 'function') {
Sentry.init({ dsn });
} else {
log('info', 'Sentry SDK missing init(); skipping instrumentation');
return server;
}
// Wrap server with Sentry MCP instrumentation if available
if (typeof Sentry.wrapMcpServerWithSentry !== 'function') {
log('info', 'wrapMcpServerWithSentry not found; continuing without MCP instrumentation');
return server;
}
const instrumentedServer = Sentry.wrapMcpServerWithSentry(server);
return instrumentedServer;
} catch (err) {
log('error', 'Error loading Sentry instrumentation', { error: err });
return server;
}
🤖 Prompt for AI Agents
In src/server/instrumentation.ts around lines 56 to 61, you currently gate on
the DSN but never initialize Sentry; after the dynamic import of Sentry perform
a Sentry.init call using the resolved dsn (e.g. Sentry.init({ dsn,
...sentryOptions })) so Sentry is actually configured. Ensure you use the
imported Sentry object from the dynamic import, merge any provided options
(tracing/sample rate/integrations) rather than hardcoding, and then proceed to
set up middleware/handlers as before so instrumentation is active.

Comment on lines 63 to 77
try {
// Lazy import Sentry to avoid module linking in tests
log('info', 'Loading Sentry instrumentation...');
const Sentry = await import('@sentry/node');

// Wrap server with Sentry instrumentation
const instrumentedServer = Sentry.wrapMcpServerWithSentry(server);

// Mark as wrapped to prevent double-wrapping
(instrumentedServer as unknown as Record<string | symbol, unknown>)[SENTRY_WRAPPED_SYMBOL] =
true;

log('info', 'Server wrapped with Sentry MCP instrumentation');
return instrumentedServer;
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure @sentry/node interop and wrapper API actually exist.

Dynamic import of @sentry/node likely yields a CJS default export; accessing named properties directly (Sentry.wrapMcpServerWithSentry) may fail and silently skip instrumentation via the catch block. Additionally, the existence of wrapMcpServerWithSentry is not standard in @sentry/node.

  • Query the web for the presence of wrapMcpServerWithSentry and Sentry auto-initialization:

  • If the wrapper is custom or provided by another module, consider importing that module instead of @sentry/node, or implement the wrapping locally.

If you confirm there is no such function, we should either:

  • Implement the wrapping in this repo, or
  • Limit to Sentry.init + log/event capture without MCP-specific wrapping.

🌐 Web query:

Does @sentry/node export a function named wrapMcpServerWithSentry? Also, can @sentry/node work without calling Sentry.init if only SENTRY_DSN is set?

💡 Result:

@sentry/node does not export a function named wrapMcpServerWithSentry. This function is referenced as an export request for the Cloudflare SDK specifically[10], and does not appear in the documented exports or API of @sentry/node[1][6][9].

@sentry/node requires calling Sentry.init() in your code; it does not automatically initialize itself simply because a SENTRY_DSN environment variable is set—just setting the environment variable is not sufficient. You must explicitly call Sentry.init({ dsn: process.env.SENTRY_DSN }) or equivalent in your application for Sentry to operate[1][2][5][8]. Without this call, even if SENTRY_DSN is set, Sentry error tracking will not function.

Key details:

  • No wrapMcpServerWithSentry function in @sentry/node: The only reference to that function is in a request for the Cloudflare-related Sentry SDK, not @sentry/node[10]. Neither the docs nor typical Node.js setups mention or export it[1][6][9].

  • Initialization requirement:

    • You must call Sentry.init() to activate Sentry features. The recommended usage is:

      import * as Sentry from "@sentry/node";
      Sentry.init({ dsn: process.env.SENTRY_DSN });
    • The presence of SENTRY_DSN in your environment has no effect unless Sentry.init() is explicitly called in the codebase[2][5][8].

  • Explicit dsn passing: The DSN (Data Source Name) can be set via environment variable, but you still need to pass it as an option to init() or have process.env.SENTRY_DSN referenced[2][5][8].

  • Other integrations/usage: Once Sentry.init() is called, you can use the helper methods (Sentry.captureException, etc.) throughout your application, and further calls to init() reconfigure the instance, but are not strictly required in every file[1][6].

Conclusion:

  • wrapMcpServerWithSentry is not part of @sentry/node.
  • @sentry/node cannot be used without a call to Sentry.init(), regardless of SENTRY_DSN being set in the environment.

If you need functionality similar to wrapMcpServerWithSentry, you will need to look at Sentry’s Cloudflare SDK or file an export request, as discussed in the referenced issue[10].

Citations:


Missing wrapMcpServerWithSentry in @sentry/node & no explicit initialization

The function wrapMcpServerWithSentry isn’t exported by @sentry/node (it only exists in the Cloudflare SDK), and you must call Sentry.init(...)—simply having SENTRY_DSN set won’t initialize the SDK. Without these, the dynamic import will either throw (entering your catch block) or silently do nothing.

Please update src/server/instrumentation.ts around lines 63–77 to:

  • Import and call the correct wrapper (or implement your own) instead of Sentry.wrapMcpServerWithSentry.
  • Invoke Sentry.init({ dsn: process.env.SENTRY_DSN }) before wrapping the server.

For example:

log('info', 'Loading Sentry instrumentation...');
const Sentry = await import('@sentry/node');
Sentry.init({ dsn: process.env.SENTRY_DSN });

// TODO: replace with the real wrapper or implement equivalent
import { wrapMcpServerWithSentry } from './sentryWrapper'; 
const instrumentedServer = wrapMcpServerWithSentry(server);
...
🤖 Prompt for AI Agents
In src/server/instrumentation.ts around lines 63–77, the code calls a
non-existent Sentry.wrapMcpServerWithSentry and never initializes the Sentry
SDK; replace the call with a real wrapper (or implement one locally) and call
Sentry.init before wrapping. Specifically: keep the lazy import of
'@sentry/node', then call Sentry.init({ dsn: process.env.SENTRY_DSN }) (and any
env/options needed), then either import/use a local wrapMcpServerWithSentry
function (or implement equivalent request/error handlers) instead of
Sentry.wrapMcpServerWithSentry, apply the wrapper to the server, set the
SENTRY_WRAPPED_SYMBOL on the instrumented server, and return it.

Comment on lines 41 to 44
executor: CommandExecutor = getDefaultCommandExecutor(),
axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
logPrefix: string = '[AXe]',
returnOutput: boolean = false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm CommandExecutor accepts the optional env parameter

executeAxeCommand passes a 4th env argument to executor. Ensure all CommandExecutor implementations (including test mocks) accept this parameter to avoid type/runtime mismatches.

Run this scan to validate the CommandExecutor type signatures and usages:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching CommandExecutor type declarations and usages..."
rg -nP -C2 '\b(type|interface)\s+CommandExecutor\b'
echo
rg -nP -C2 '\bcreateMockExecutor\b'
echo
# Look for function signatures accepting 3 vs 4 params
rg -nP -C2 'CommandExecutor\s*=\s*\('
rg -nP -C2 'function\s+\w*\(\s*command:\s*string\[\],\s*description\?:\s*string,\s*allowFailure\?:\s*boolean(,|\))'

Length of output: 183481


Add optional env parameter to CommandExecutor signatures

The executeAxeCommand helper now passes a fourth env argument to the executor, but our CommandExecutor types only declare up to three parameters. We need to update both definitions and all mock/real implementations to accept an optional env parameter:

• src/utils/command.ts (around line 30):

export type CommandExecutor = (
  command: string[],
  logPrefix?: string,
  allowFailure?: boolean,
  /** Optional environment variables for the spawned process */
  env?: NodeJS.ProcessEnv
) => Promise<CommandResult>;

• src/test-utils/mock-executors.ts (around line 23):

export type CommandExecutor = (
  command: string[],
  description?: string,
  allowFailure?: boolean,
  /** Optional env for testing overrides */
  env?: Record<string, unknown>
) => Promise<MockResult>;

• Ensure getDefaultCommandExecutor, createMockExecutor and any other executor implementations include env? in their parameter lists and pass it through.

This alignment will prevent type errors and ensure runtime consistency when an env object is provided.

🤖 Prompt for AI Agents
In src/utils/axe-command.ts around lines 41 to 44, the executor and related
types/implementations don't accept the optional fourth env parameter that
executeAxeCommand now passes; update the CommandExecutor type in
src/utils/command.ts (around line 30) to add an optional env?: NodeJS.ProcessEnv
parameter, update the test mock type in src/test-utils/mock-executors.ts (around
line 23) to add env?: Record<string, unknown>, and modify
getDefaultCommandExecutor, createMockExecutor and any other executor
implementations to include env? in their parameter lists and pass that env
through to the spawned process or mock result so signatures and runtime behavior
match.

Comment on lines 65 to 71
throw new AxeError(
`axe command '${commandName}' failed.`,
commandName,
result.error ?? result.output,
simulatorUuid,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Include the actual executed command in AxeError for better diagnostics

Passing commandName as the “command” field loses detail. Include the full command string.

Apply this diff:

-      throw new AxeError(
-        `axe command '${commandName}' failed.`,
-        commandName,
-        result.error ?? result.output,
-        simulatorUuid,
-      );
+      throw new AxeError(
+        `axe command '${commandName}' failed.`,
+        fullCommand.join(' '),
+        result.error ?? result.output,
+        simulatorUuid,
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new AxeError(
`axe command '${commandName}' failed.`,
commandName,
result.error ?? result.output,
simulatorUuid,
);
}
throw new AxeError(
`axe command '${commandName}' failed.`,
fullCommand.join(' '),
result.error ?? result.output,
simulatorUuid,
);
🤖 Prompt for AI Agents
In src/utils/axe-command.ts around lines 65 to 71, the AxeError is being
constructed with commandName (a short identifier) which loses the full executed
command; replace the commandName argument with the actual executed command
string (the variable that contains the full command used to spawn the process,
e.g., fullCommand or command) so the AxeError carries the full command text,
keep the rest of the arguments the same.

Comment on lines 20 to 26
import * as Sentry from '@sentry/node';

const SENTRY_ENABLED = process.env.SENTRY_DISABLED !== 'true';
const SENTRY_ENABLED = process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';

if (!SENTRY_ENABLED) {
log('info', 'Sentry disabled due to SENTRY_DISABLED environment variable');
log('info', 'Sentry disabled due to XCODEBUILDMCP_SENTRY_DISABLED environment variable');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid static Sentry import and top-level side effects in a “pure utils” module; lazy-load in error path

This utils module statically imports @sentry/node and logs at import time, which conflicts with the PR’s goal of keeping utils side-effect-free and only loading Sentry in production. Recommend removing the static import and top-level log, and lazily importing Sentry only when logging errors and when enabled.

Apply this minimal diff in this hunk:

-import * as Sentry from '@sentry/node';
-
-const SENTRY_ENABLED = process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
-
-if (!SENTRY_ENABLED) {
-  log('info', 'Sentry disabled due to XCODEBUILDMCP_SENTRY_DISABLED environment variable');
-}
+// Sentry is loaded lazily to keep utils side‑effect‑free and avoid Node-only deps in test/runtime paths
+const SENTRY_ENABLED = process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';

Add this helper near the top of the file (outside the function), to support lazy error reporting:

let sentryModulePromise: Promise<typeof import('@sentry/node')> | undefined;

async function captureErrorInSentry(message: string): Promise<void> {
  if (!SENTRY_ENABLED) return;
  // Optionally also gate by NODE_ENV === 'production' to avoid loading Sentry outside prod
  try {
    const Sentry = await (sentryModulePromise ??= import('@sentry/node'));
    Sentry.captureMessage(message);
  } catch {
    // Swallow Sentry import/report failures; logging must not impact control flow
  }
}

…oduction dependencies

## Root Cause Analysis
CI was failing with 78 "module is already linked" errors due to import chain:
test files → utils/command.js → utils/logger.js → @sentry/node → native modules

This chain caused Vitest's vmThreads pool to load native modules multiple times
across VM contexts, triggering Linux glibc's module linking restrictions.

## Solution: Surgical Mock Function Migration
- Created isolated test-utils/mock-executors.ts with exact copies of mock functions
- Removed all mock functions from utils/command.ts (production dependency chain)
- Updated 54+ test files to import mocks from test-utils/mock-executors.js
- Extracted CommandExecutor and FileSystemExecutor interfaces to separate files
- Maintained zero behavioral changes to preserve all 1046 passing tests

## Key Changes
- NEW: src/test-utils/mock-executors.ts - Isolated test mocks (no production deps)
- NEW: src/utils/CommandExecutor.ts - Command executor interface
- NEW: src/utils/FileSystemExecutor.ts - File system executor interface
- MODIFIED: src/utils/command.ts - Removed mock functions, kept production executors
- MODIFIED: 54+ test files - Updated imports to use isolated test-utils

## Import Chain Resolution
BEFORE: test files → utils/command.js → utils/logger.js → @sentry/node (BROKEN)
AFTER:  test files → test-utils/mock-executors.js (ISOLATED ✅)

## Verification
- All 1046 tests pass locally with surgical migration
- Linting and TypeScript compilation clean
- Production command execution unchanged
- Test mock behavior preserved exactly

Resolves Ubuntu CI "module is already linked" errors while maintaining
test reliability and production functionality.
@cameroncooke cameroncooke force-pushed the fix/module-import-bug branch from b759710 to 8c5ce80 Compare August 16, 2025 19:31
@claude
Copy link
Contributor

claude bot commented Aug 16, 2025

Claude finished @cameroncooke's task —— View job


PR Review: CI Module Linking Fix

This is an excellent targeted fix that successfully addresses the CI module linking issues while maintaining clean architecture. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Progress:

  • Analyze new directory structure and module separation
  • Review Sentry lazy loading implementation
  • Examine DRY principle implementations (axe-command.ts, system-info.ts)
  • Assess test architecture improvements
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new src/test-utils/ directory creates clear boundaries between production command execution and testing utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Interface Extraction: Clean extraction of CommandExecutor and FileSystemExecutor interfaces into separate files

2. CI Issue Resolution

  • Root Cause Analysis: Excellent identification of the import chain causing module linking issues (test files → utils/command.js → utils/logger.js → @sentry/node)
  • Surgical Approach: Moved only the problematic mock functions while preserving all production functionality
  • Zero Behavioral Changes: Maintained exact mock function behavior to preserve all 1046 passing tests

3. Testing Architecture Improvements

  • Mock Executors: src/test-utils/mock-executors.ts provides clean, dependency-free testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy, avoiding vi.mock() anti-patterns
  • Comprehensive Mocking: Includes sophisticated mock types like createCommandMatchingMockExecutor for complex scenarios

4. Code Quality Enhancements

  • Type Safety: Proper TypeScript interfaces for executors improve type checking
  • Error Prevention: Guard functions in getDefaultCommandExecutor() and getDefaultFileSystemExecutor() prevent accidental production calls in tests
  • Documentation: Clear commit message and inline documentation explaining the changes

Technical Implementation Quality

1. Interface Design

// Clean, well-designed interfaces
export type CommandExecutor = (
  command: string[],
  logPrefix?: string,
  useShell?: boolean,
  env?: Record<string, string>,
  detached?: boolean,
) => Promise<CommandResponse>;

2. Test Safety Mechanisms

// Excellent guardrails to prevent production calls in tests
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
  throw new Error('🚨 REAL SYSTEM EXECUTOR DETECTED IN TEST! 🚨\n...');
}

3. Mock Function Completeness

  • createMockExecutor() - Basic command mocking
  • createCommandMatchingMockExecutor() - Pattern-based command matching
  • createNoopExecutor() - Error-throwing guard for unexpected calls
  • createMockFileSystemExecutor() - Complete filesystem mocking
  • createMockEnvironmentDetector() - Environment detection mocking

⚠️ Minor Considerations

1. Import Path Maintenance

With 54+ test files updated, future maintenance should ensure new tests use the correct import paths from test-utils/mock-executors.js rather than the old utils locations.

2. Documentation Updates

Consider updating docs/TESTING.md to reflect the new mock import patterns and emphasize the importance of using isolated test utilities.

3. Backward Compatibility

The re-exports in src/utils/command.ts (lines 19-21) maintain backward compatibility, which is excellent for gradual migration.

🚀 Performance & Security Assessment

Performance

  • ✅ Eliminates native module loading conflicts in test environments
  • ✅ Reduces test execution overhead by removing unnecessary production dependencies
  • ✅ Clean module boundaries improve build optimization

Security

  • ✅ No security implications - purely architectural improvement
  • ✅ Test isolation prevents accidental production system interactions during testing
  • ✅ Guard functions provide additional safety barriers

📋 Test Coverage & Reliability

The testing approach is exemplary:

  • ✅ All 1046 tests continue to pass with zero behavioral changes
  • ✅ Mock dependencies provide comprehensive test coverage scenarios
  • ✅ No reliance on Vitest mocking maintains test reliability
  • ✅ Interface-based design enables easy testing of different scenarios

🎯 Recommendation

STRONGLY APPROVE - This is a surgical, well-executed fix that:

  1. Resolves the critical CI module linking issues through proper dependency isolation
  2. Maintains perfect backward compatibility with all existing functionality
  3. Preserves all test behavior while improving test architecture
  4. Follows excellent engineering practices with clear interfaces and guard functions
  5. Demonstrates strong problem-solving with precise root cause analysis

The solution is focused, elegant, and addresses the exact problem without introducing unnecessary complexity. The interface extraction and test utility isolation set up a solid foundation for future development.

This fix should resolve the Ubuntu CI "module is already linked" errors while maintaining all existing functionality.


Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🔭 Outside diff range comments (5)
src/mcp/tools/project-discovery/get_app_bundle_id.ts (1)

34-40: Shell injection risk: avoid '/bin/sh -c' with user-controlled appPath

executeSyncCommand invokes '/bin/sh -c ""' with appPath interpolated. A malicious or malformed path could break quoting or inject arguments. Execute commands with argv arrays instead.

Proposed refactor:

-/**
- * Sync wrapper for CommandExecutor to handle synchronous commands
- */
-async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
-  const result = await executor(['/bin/sh', '-c', command], 'Bundle ID Extraction');
-  if (!result.success) {
-    throw new Error(result.error ?? 'Command failed');
-  }
-  return result.output || '';
-}
+/**
+ * Safer command executor: no shell; pass argv directly
+ */
+async function executeCommand(
+  cmd: string[],
+  executor: CommandExecutor,
+  logPrefix = 'Bundle ID Extraction',
+): Promise<string> {
+  const result = await executor(cmd, logPrefix);
+  if (!result.success) {
+    throw new Error(result.error ?? 'Command failed');
+  }
+  return result.output || '';
+}
@@
-      bundleId = await executeSyncCommand(
-        `defaults read "${appPath}/Info" CFBundleIdentifier`,
-        executor,
-      );
+      bundleId = await executeCommand(
+        ['defaults', 'read', `${appPath}/Info`, 'CFBundleIdentifier'],
+        executor,
+      );
@@
-        bundleId = await executeSyncCommand(
-          `/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Info.plist"`,
-          executor,
-        );
+        bundleId = await executeCommand(
+          ['/usr/libexec/PlistBuddy', '-c', 'Print :CFBundleIdentifier', `${appPath}/Info.plist`],
+          executor,
+        );

Optional guardrail: if you want extra safety, reject paths containing quotes or newlines before execution.

If you prefer to keep a shell, you should at least validate/sanitize appPath to disallow quotes and control characters and use spawn with argv where possible.

Also applies to: 71-86

src/utils/template-manager.ts (1)

117-125: Bug: returning the extracted root directory instead of the 'template' subdirectory

Comment says “return the template subdirectory,” but the code returns the extracted root. For local overrides you return the 'template' subdir; for downloaded templates you should mirror the same contract. This mismatch is likely to break downstream code that expects the template folder.

Apply this diff:

-      // Find the extracted directory and return the template subdirectory
-      const extractedDir = join(tempDir, `${repo}-${version.substring(1)}`);
-      if (!fileSystemExecutor.existsSync(extractedDir)) {
-        throw new Error(`Expected template directory not found: ${extractedDir}`);
-      }
-
-      log('info', `Successfully downloaded ${platform} template ${version}`);
-      return extractedDir;
+      // Find the extracted directory and return the template subdirectory
+      const extractedDir = join(tempDir, `${repo}-${version.substring(1)}`);
+      if (!fileSystemExecutor.existsSync(extractedDir)) {
+        throw new Error(`Expected extracted directory not found: ${extractedDir}`);
+      }
+      const templateSubdir = join(extractedDir, 'template');
+      if (!fileSystemExecutor.existsSync(templateSubdir)) {
+        throw new Error(`Expected template subdirectory not found: ${templateSubdir}`);
+      }
+
+      log('info', `Successfully downloaded ${platform} template ${version}`);
+      return templateSubdir;
src/mcp/tools/logging/stop_device_log_cap.ts (1)

1-293: Enforce type-only, .js imports for FileSystemExecutor
To prevent runtime errors from importing source files, all occurrences of FileSystemExecutor must be imported as a type from the compiled .js module. Please update each value import from .ts to a type-only import from .js.

Affected files:

  • src/test-utils/mock-executors.ts
  • src/utils/validation.ts
  • src/utils/template-manager.ts
  • src/utils/command.ts
  • src/mcp/tools/project-discovery/get_mac_bundle_id.ts
  • src/mcp/tools/project-discovery/get_app_bundle_id.ts
  • src/mcp/tools/project-discovery/discover_projs.ts
  • src/mcp/tools/macos/launch_mac_app.ts
  • src/mcp/tools/macos/test_macos.ts
  • src/mcp/tools/project-scaffolding/scaffold_macos_project.ts
  • src/mcp/tools/device/test_device.ts
  • src/mcp/tools/logging/stop_device_log_cap.ts

Example diff for each import (adjust paths as needed):

- import { FileSystemExecutor } from './FileSystemExecutor.ts';
+ import type { FileSystemExecutor } from './FileSystemExecutor.js';
src/utils/command.ts (2)

68-76: Align spawn options with detached semantics

When detached is true, set spawnOpts.detached and use stdio: 'ignore' to prevent the parent from holding references. Otherwise, keep piping.

Apply this diff:

-    const spawnOpts: Parameters<typeof spawn>[2] = {
-      stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr
-    };
+    const spawnOpts: Parameters<typeof spawn>[2] = {};
+    if (detached) {
+      spawnOpts.detached = true;
+      spawnOpts.stdio = 'ignore';
+    } else {
+      // ignore stdin, pipe stdout/stderr
+      spawnOpts.stdio = ['ignore', 'pipe', 'pipe'];
+    }

81-87: Avoid attaching stream listeners for detached processes

Piping stdout/stderr for detached processes pins the child. Only attach listeners when not detached.

Apply this diff:

-    let stdout = '';
-    let stderr = '';
-
-    childProcess.stdout?.on('data', (data: Buffer) => {
-      stdout += data.toString();
-    });
-
-    childProcess.stderr?.on('data', (data: Buffer) => {
-      stderr += data.toString();
-    });
+    let stdout = '';
+    let stderr = '';
+    if (!detached) {
+      childProcess.stdout?.on('data', (data: Buffer) => {
+        stdout += data.toString();
+      });
+      childProcess.stderr?.on('data', (data: Buffer) => {
+        stderr += data.toString();
+      });
+    }
♻️ Duplicate comments (1)
docs/TOOLS.md (1)

30-31: Fix grammar and add “Example:” label for launch_app_sim.

This mirrors a prior suggestion and improves clarity.

-- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
+- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If the simulator window isn't visible, use open_sim() first. Example: launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
🧹 Nitpick comments (34)
src/mcp/tools/macos/launch_mac_app.ts (1)

12-13: Prefer type-only imports to avoid unnecessary runtime deps (and reflect new module split).

FileSystemExecutor is used only as a type here. Import it with import type so it’s erased at emit time. Also consider importing the CommandExecutor type from the new dedicated module while keeping the runtime import for getDefaultCommandExecutor from utils/command.js.

Apply this diff:

-import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import { getDefaultCommandExecutor } from '../../../utils/command.js';
+import type { CommandExecutor } from '../../../utils/CommandExecutor.ts';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
scripts/update-tools-docs.ts (2)

81-83: Polish: correct singular/plural for tool count (1 tool vs 2 tools).

Small readability win in generated docs.

-  content += `**Purpose**: ${workflow.description} (${toolCount} tools)\n\n`;
+  const toolsLabel = toolCount === 1 ? 'tool' : 'tools';
+  content += `**Purpose**: ${workflow.description} (${toolCount} ${toolsLabel})\n\n`;

105-106: Avoid mutating input arrays during sort.

Sorting in place modifies analysis.workflows. Defensive copy keeps analysis immutable to callers.

-  const sortedWorkflows = workflows.sort((a, b) => a.displayName.localeCompare(b.displayName));
+  const sortedWorkflows = [...workflows].sort((a, b) => a.displayName.localeCompare(b.displayName));
docs/TOOLS.md (1)

65-66: Minor: pluralization nit (“1 tools”).

Several sections with a single tool read “(1 tools)”. This comes from the generator; see suggested fix in scripts/update-tools-docs.ts to render “1 tool”.

src/mcp/tools/device/__tests__/launch_app_device.test.ts (1)

205-246: Reduce global side effects when patching fs in tests.

Overwriting fs.promises directly can introduce flaky behavior under parallel execution. Prefer scoped spies/mocks.

Example using Vitest:

import { vi } from 'vitest';
const readFileSpy = vi.spyOn(fs.promises, 'readFile').mockImplementation(async (p: any) => {
  if (String(p).includes('launch-')) return JSON.stringify({ result: { process: { processIdentifier: 12345 } } });
  return originalReadFile(p as any);
});
const unlinkSpy = vi.spyOn(fs.promises, 'unlink').mockResolvedValue();

try {
  // run test body
} finally {
  readFileSpy.mockRestore();
  unlinkSpy.mockRestore();
}
src/utils/validation.ts (1)

25-25: Use type-only re-exports/imports to avoid runtime artifacts

These are types only; exporting/importing them as values can cause issues under isolatedModules and increases risk of runtime import churn.

Apply:

-import { ToolResponse, ValidationResult } from '../types/common.js';
+import type { ToolResponse, ValidationResult } from '../types/common.js';

And at the bottom:

-export { ToolResponse, ValidationResult };
+export type { ToolResponse, ValidationResult };

Also applies to: 262-262

src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts (1)

3-3: Standardize import specifiers: prefer .js (or enable allowImportingTsExtensions)

This test imports from '../reset_sim_location.ts' while other imports use .js specifiers. Unless the repo explicitly enables allowImportingTsExtensions and tooling is configured for it, prefer .js to avoid resolution issues.

Apply:

-import resetSimLocationPlugin, { reset_sim_locationLogic } from '../reset_sim_location.ts';
+import resetSimLocationPlugin, { reset_sim_locationLogic } from '../reset_sim_location.js';
src/mcp/tools/project-discovery/get_app_bundle_id.ts (1)

51-65: Optional: validate .app directory shape earlier

You could proactively validate that appPath ends with .app and that Info.plist exists to produce a clearer error before running commands.

Example:

  • Check: appPath.endsWith('.app')
  • Check: fileSystemExecutor.existsSync(${appPath}/Info.plist)
src/mcp/tools/device/__tests__/install_app_device.test.ts (1)

41-45: Tighten types in test harness (unknown → string/typed env)

Using concrete types here increases signal and catches regressions earlier without impacting the test behavior.

-      let capturedCommand: unknown[] = [];
+      let capturedCommand: string[] = [];
       let capturedDescription: string = '';
       let capturedUseShell: boolean = false;
-      let capturedEnv: unknown = undefined;
+      let capturedEnv: Record<string, string> | undefined = undefined;

-      const trackingExecutor = async (
-        command: unknown[],
+      const trackingExecutor = async (
+        command: string[],
         description: string,
         useShell: boolean,
-        env: unknown,
+        env: Record<string, string> | undefined,
       ) => {

Also applies to: 52-57

src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts (1)

11-14: Remove unused test variables

mockCalls is declared and reassigned but never used. Trimming this keeps the test concise.

-  let mockCalls: any[] = [];
-
-  mockCalls = [];
+  // (removed unused mockCalls)
src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts (1)

60-61: Remove unused variable

originalExecutor is assigned but never used in this test.

-      const originalExecutor = mockExecutor;
-
+      // (removed unused originalExecutor)
src/mcp/tools/ui-testing/__tests__/gesture.test.ts (1)

11-11: Optional: introduce a path alias for test-utils to avoid brittle deep relatives

Deep relative imports are harder to maintain across moves. Consider a short alias for test-utils:

Add to tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@test-utils/*": ["test-utils/*"]
    },
    "moduleResolution": "Bundler"
  }
}

And in vitest.config.ts:

import { defineConfig } from 'vitest/config';
import path from 'node:path';

export default defineConfig({
  resolve: {
    alias: {
      '@test-utils': path.resolve(__dirname, 'src/test-utils'),
    },
  },
});

Then update imports:

-} from '../../../../test-utils/mock-executors.js';
+} from '@test-utils/mock-executors.js';
src/mcp/tools/macos/__tests__/test_macos.test.ts (1)

60-71: Remove unused mocks in the first two XOR validation tests

Both tests call the handler directly and don’t use the locally-declared mockExecutor or mockFileSystemExecutor. Dropping them reduces noise and avoids potential no-unused-vars lint warnings.

-      const mockExecutor = createMockExecutor({
-        success: true,
-        output: 'Test Suite All Tests passed',
-      });
-
-      const mockFileSystemExecutor = {
-        mkdtemp: async () => '/tmp/test-123',
-        rm: async () => {},
-        tmpdir: () => '/tmp',
-        stat: async () => ({ isDirectory: () => true }),
-      };
-      const mockExecutor = createMockExecutor({
-        success: true,
-        output: 'Test Suite All Tests passed',
-      });
-
-      const mockFileSystemExecutor = {
-        mkdtemp: async () => '/tmp/test-123',
-        rm: async () => {},
-        tmpdir: () => '/tmp',
-        stat: async () => ({ isDirectory: () => true }),
-      };

Also applies to: 82-93

src/mcp/tools/simulator/__tests__/boot_sim.test.ts (1)

10-13: Trim unused imports from test-utils

createMockFileSystemExecutor and createNoopExecutor aren’t used in this file. Remove them from the import to keep things tidy and avoid lint warnings.

-import {
-  createMockExecutor,
-  createMockFileSystemExecutor,
-  createNoopExecutor,
-} from '../../../../test-utils/mock-executors.js';
+import { createMockExecutor } from '../../../../test-utils/mock-executors.js';
src/mcp/tools/ui-testing/__tests__/type_text.test.ts (1)

8-11: Narrow the test-utils import to used symbols

createMockFileSystemExecutor isn’t referenced in this test. Removing it prevents future no-unused-vars issues and clarifies dependencies.

-import {
-  createMockExecutor,
-  createMockFileSystemExecutor,
-  createNoopExecutor,
-} from '../../../../test-utils/mock-executors.js';
+import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.js';
src/utils/index.ts (1)

4-5: 🔧 Convert executor exports to type-only

To keep the utils barrel side-effect free and avoid unnecessary runtime resolution for pure interface modules, please update src/utils/index.ts:

--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ lines 4-5
-export * from './CommandExecutor.js';
-export * from './FileSystemExecutor.js';
+export type { CommandExecutor, CommandResponse } from './CommandExecutor.js';
+export type { FileSystemExecutor } from './FileSystemExecutor.js';

This change ensures only types are re-exported at runtime, aligning with our “pure utils” boundary. [optional_refactors_recommended]

src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts (1)

7-7: Consider a path alias for test utilities to reduce fragile relative paths.

This will improve maintainability across the repo when files move.

Apply this diff once aliases are configured:

-} from '../../../../test-utils/mock-executors.js';
+} from '@test-utils/mock-executors.js';

Add alias support:

  • tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@test-utils/*": ["src/test-utils/*"]
    }
  }
}
  • vitest.config.ts (or vite.config.ts)
import { defineConfig } from 'vitest/config';
import { fileURLToPath } from 'node:url';

export default defineConfig({
  resolve: {
    alias: {
      '@test-utils': fileURLToPath(new URL('./src/test-utils', import.meta.url)),
    },
  },
});
src/mcp/tools/logging/stop_device_log_cap.ts (4)

13-14: Prefer type-only import and .js extension for ESM stability

To prevent a runtime import of a .ts file (and ensure elision under TS), switch to a type-only import and reference the emitted .js extension. This avoids subtle bundler/Node resolution issues if the import is preserved.

-import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';

94-96: Guard against undefined exitCode when stopping the process

exitCode is typed as number | null | undefined. If it’s undefined, the current condition won’t attempt to terminate an active process. Include undefined in the check.

-    if (!session.process.killed && session.process.exitCode === null) {
+    if (!session.process.killed && (session.process.exitCode === null || session.process.exitCode === undefined)) {
       session.process.kill?.('SIGTERM');
     }

246-248: Use os.tmpdir() instead of hard-coded '/tmp'

Hard-coding '/tmp' reduces portability. Use Node’s os.tmpdir() for cross-platform correctness.

-    tmpdir(): string {
-      return '/tmp';
-    },
+    tmpdir(): string {
+      return os.tmpdir();
+    },

Add this import at the top of the file:

+import os from 'os';

152-249: Consider extracting the legacy adapter to a shared util

The inlined adapter for backward compatibility is lengthy and duplicates fs logic. Extracting it (e.g., to utils/fs-adapter.ts) would improve reuse and readability across plugins that may need the same shim.

I can sketch a small toFileSystemExecutor(fileSystem?: unknown): FileSystemExecutor helper if helpful.

src/mcp/tools/macos/test_macos.ts (1)

19-19: Make FileSystemExecutor a type-only import (use .js extension)

This keeps the import purely for typing and avoids runtime resolution of the .ts source.

-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';
src/mcp/tools/device/test_device.ts (2)

19-19: Make FileSystemExecutor a type-only import (use .js extension)

Same rationale as other files: avoid runtime import of a .ts module.

-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';

206-208: Consistency: default preferXcodebuild to false

test_macos.ts passes params.preferXcodebuild ?? false. Here it forwards possibly undefined. Aligning makes behavior consistent and explicit.

-      params.preferXcodebuild,
+      params.preferXcodebuild ?? false,
src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts (1)

20-50: Optional: avoid hardcoding '/tmp' and clean up temp files

The helper writes directly to /tmp and doesn’t remove files, which can leak artifacts and isn’t cross-platform. Prefer os.tmpdir() and add afterEach cleanup.

@@
-import { describe, it, expect, beforeEach } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import os from 'os';
+import { join } from 'path';
@@
-  let mockFileSystem: any;
+  let mockFileSystem: any;
+  const createdFiles: string[] = [];
@@
   beforeEach(() => {
     mockFileSystem = createMockFileSystemExecutor();
     // Clear any active sessions before each test
     activeLogSessions.clear();
   });
+
+  afterEach(async () => {
+    const fs = await import('fs/promises');
+    for (const p of createdFiles.splice(0)) {
+      try { await fs.unlink(p); } catch {}
+    }
+  });
@@
-  async function createTestLogSession(sessionId: string, logContent: string = '') {
+  async function createTestLogSession(sessionId: string, logContent: string = '') {
@@
-    const logFilePath = `/tmp/xcodemcp_sim_log_test_${sessionId}.log`;
+    const logFilePath = join(os.tmpdir(), `xcodemcp_sim_log_test_${sessionId}.log`);
@@
-    await fs.writeFile(logFilePath, logContent, 'utf-8');
+    await fs.writeFile(logFilePath, logContent, 'utf-8');
+    createdFiles.push(logFilePath);
src/utils/CommandExecutor.ts (3)

1-1: Use type-only import to avoid unintended runtime dependency on child_process

This file defines types only; importing ChildProcess as a value will generate a runtime import. Switch to a type-only import.

Apply this diff:

-import { ChildProcess } from 'child_process';
+import type { ChildProcess } from 'child_process';

6-12: Broaden env type to align with Node’s spawn env semantics

Node’s spawn expects env values as string | undefined (NodeJS.ProcessEnv). Using Record<string, string> is stricter and can force casts downstream. Prefer NodeJS.ProcessEnv (or a compatible partial) in the API surface.

Apply this diff:

 export type CommandExecutor = (
   command: string[],
   logPrefix?: string,
   useShell?: boolean,
-  env?: Record<string, string>,
+  env?: NodeJS.ProcessEnv,
   detached?: boolean,
 ) => Promise<CommandResponse>;

If you prefer to avoid NodeJS namespace, an alternative is:

-  env?: Record<string, string>,
+  env?: Record<string, string | undefined>,

14-22: Consider documenting error semantics (reject vs resolve with error) in the type contract

The default executor rejects on spawn errors, while CommandResponse also has an error field. Clarify in the JSDoc whether callers should expect rejections for transport-level failures (spawn errors) vs resolution with success=false for process exit failures.

src/utils/FileSystemExecutor.ts (1)

5-16: Tighten readdir/stat typings with overloads and Node types (type-only) for better DX

unknown[] for readdir loses useful type info. You can provide overloads mirroring fs.readdir and keep zero runtime cost via type-only imports. Same for stat returning a minimal shape; consider Node’s Stats.

Apply this diff:

+import type { Dirent, Stats } from 'fs';
+
 export interface FileSystemExecutor {
   mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
   readFile(path: string, encoding?: BufferEncoding): Promise<string>;
   writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>;
   cp(source: string, destination: string, options?: { recursive?: boolean }): Promise<void>;
-  readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]>;
+  // Overloads to mirror Node's fs.readdir behavior
+  readdir(path: string, options: { withFileTypes: true }): Promise<Dirent[]>;
+  readdir(path: string, options?: { withFileTypes?: false }): Promise<string[]>;
+  readdir(path: string, options?: { withFileTypes?: boolean }): Promise<string[] | Dirent[]>;
   rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
   existsSync(path: string): boolean;
-  stat(path: string): Promise<{ isDirectory(): boolean }>;
+  stat(path: string): Promise<Stats>;
   mkdtemp(prefix: string): Promise<string>;
   tmpdir(): string;
 }

If you want to keep the minimal surface intentionally, feel free to ignore—this is a DX improvement.

src/utils/command.ts (2)

90-121: Unref truly detached processes

Call unref() so the parent can exit independently of the child. Keep your existing short readiness delay, but unref immediately after spawn.

Apply this diff:

-    // For detached processes, handle differently to avoid race conditions
+    // For detached processes, handle differently to avoid race conditions
     if (detached) {
+      // allow parent to exit independently of the child
+      if (typeof childProcess.unref === 'function') {
+        childProcess.unref();
+      }
       // For detached processes, only wait for spawn success/failure
       let resolved = false;

41-57: Shell quoting is fragile; consider spawn's shell: true instead

Manual quoting with sh -c is error-prone for special characters and expansions. Using spawn(commandString, { shell: true }) delegates parsing to the platform shell and avoids custom quoting logic. Not mandatory for this PR, but worth tracking.

src/test-utils/mock-executors.ts (3)

172-175: Doc nit: incorrect return type mentioned

Comment says “returns CommandExecutor” but this factory returns a FileSystemExecutor. Update the doc to avoid confusion.

Apply this diff:

- * @returns CommandExecutor that throws on invocation
+ * @returns FileSystemExecutor that throws on invocation

176-252: Intentional inconsistency: tmpdir does not throw in Noop FS

All FS methods throw except tmpdir(), which returns '/tmp'. If this is deliberate (allowing path computations without I/O), add a short note in the JSDoc to prevent confusion during test authoring.


99-148: Command pattern matching is order-dependent

First matching key wins (Object.keys order). If overlapping patterns exist, behavior may be surprising. Consider documenting precedence or supporting priority via an array of [pattern, result] to make ordering explicit.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b759710 and 8c5ce80.

📒 Files selected for processing (77)
  • TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md (0 hunks)
  • TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md.bak (0 hunks)
  • docs/TOOLS.md (4 hunks)
  • scripts/update-tools-docs.ts (1 hunks)
  • src/mcp/resources/__tests__/devices.test.ts (1 hunks)
  • src/mcp/resources/__tests__/doctor.test.ts (1 hunks)
  • src/mcp/resources/__tests__/simulators.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/build_device.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/get_device_app_path.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/install_app_device.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/launch_app_device.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/list_devices.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/stop_app_device.test.ts (1 hunks)
  • src/mcp/tools/device/__tests__/test_device.test.ts (1 hunks)
  • src/mcp/tools/device/test_device.ts (1 hunks)
  • src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts (1 hunks)
  • src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts (1 hunks)
  • src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts (1 hunks)
  • src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts (1 hunks)
  • src/mcp/tools/logging/stop_device_log_cap.ts (1 hunks)
  • src/mcp/tools/macos/__tests__/build_macos.test.ts (1 hunks)
  • src/mcp/tools/macos/__tests__/build_run_macos.test.ts (1 hunks)
  • src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts (1 hunks)
  • src/mcp/tools/macos/__tests__/launch_mac_app.test.ts (1 hunks)
  • src/mcp/tools/macos/__tests__/test_macos.test.ts (1 hunks)
  • src/mcp/tools/macos/launch_mac_app.ts (1 hunks)
  • src/mcp/tools/macos/test_macos.ts (1 hunks)
  • src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts (1 hunks)
  • src/mcp/tools/project-discovery/__tests__/get_app_bundle_id.test.ts (1 hunks)
  • src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts (1 hunks)
  • src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts (1 hunks)
  • src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts (1 hunks)
  • src/mcp/tools/project-discovery/discover_projs.ts (1 hunks)
  • src/mcp/tools/project-discovery/get_app_bundle_id.ts (1 hunks)
  • src/mcp/tools/project-discovery/get_mac_bundle_id.ts (1 hunks)
  • src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts (1 hunks)
  • src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts (1 hunks)
  • src/mcp/tools/project-scaffolding/scaffold_macos_project.ts (1 hunks)
  • src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts (1 hunks)
  • src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts (1 hunks)
  • src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts (1 hunks)
  • src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/boot_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/build_run_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/build_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/install_app_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/list_sims.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/open_sim.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/screenshot.test.ts (1 hunks)
  • src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts (1 hunks)
  • src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts (1 hunks)
  • src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts (1 hunks)
  • src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts (1 hunks)
  • src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/button.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/gesture.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/key_press.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/long_press.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/screenshot.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/swipe.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/tap.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/touch.test.ts (1 hunks)
  • src/mcp/tools/ui-testing/__tests__/type_text.test.ts (1 hunks)
  • src/mcp/tools/utilities/__tests__/clean.test.ts (1 hunks)
  • src/test-utils/mock-executors.ts (1 hunks)
  • src/utils/CommandExecutor.ts (1 hunks)
  • src/utils/FileSystemExecutor.ts (1 hunks)
  • src/utils/__tests__/simulator-utils.test.ts (1 hunks)
  • src/utils/__tests__/typed-tool-factory.test.ts (1 hunks)
  • src/utils/command.ts (1 hunks)
  • src/utils/index.ts (1 hunks)
  • src/utils/template-manager.ts (1 hunks)
  • src/utils/validation.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md
  • TOOL_NAMING_VERIFICATION_2025-08-14_22-13.md.bak
✅ Files skipped from review due to trivial changes (21)
  • src/mcp/tools/swift-package/tests/swift_package_clean.test.ts
  • src/mcp/tools/simulator-management/tests/set_sim_appearance.test.ts
  • src/mcp/tools/device/tests/list_devices.test.ts
  • src/mcp/tools/simulator/tests/build_sim.test.ts
  • src/mcp/tools/logging/tests/start_sim_log_cap.test.ts
  • src/mcp/tools/ui-testing/tests/touch.test.ts
  • src/mcp/tools/ui-testing/tests/swipe.test.ts
  • src/mcp/tools/simulator/tests/install_app_sim.test.ts
  • src/mcp/tools/project-scaffolding/tests/scaffold_ios_project.test.ts
  • src/mcp/tools/simulator-management/tests/set_sim_location.test.ts
  • src/mcp/tools/logging/tests/start_device_log_cap.test.ts
  • src/mcp/tools/ui-testing/tests/long_press.test.ts
  • src/mcp/resources/tests/devices.test.ts
  • src/mcp/tools/logging/tests/stop_device_log_cap.test.ts
  • src/mcp/tools/project-discovery/tests/get_mac_bundle_id.test.ts
  • src/mcp/tools/macos/tests/build_run_macos.test.ts
  • src/mcp/tools/project-discovery/tests/list_schemes.test.ts
  • src/mcp/tools/project-discovery/tests/get_app_bundle_id.test.ts
  • src/mcp/resources/tests/doctor.test.ts
  • src/mcp/tools/device/tests/get_device_app_path.test.ts
  • src/mcp/tools/ui-testing/tests/screenshot.test.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/utils/CommandExecutor.ts (1)
src/utils/command.ts (2)
  • CommandExecutor (20-20)
  • CommandResponse (20-20)
src/utils/FileSystemExecutor.ts (1)
src/utils/command.ts (1)
  • FileSystemExecutor (21-21)
src/test-utils/mock-executors.ts (2)
src/utils/CommandExecutor.ts (1)
  • CommandExecutor (6-12)
src/utils/FileSystemExecutor.ts (1)
  • FileSystemExecutor (5-16)
🪛 LanguageTool
docs/TOOLS.md

[grammar] ~30-~30: There might be a mistake here.
Context: ...pp in an iOS simulator by UUID or name. If simulator window isn't visible, use ope...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ...e session and returns the captured logs. ### macOS Development (macos) Purpose:...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...nonical tools + 22 re-exports = 81 total - Workflow Groups: 12 --- *This docume...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: claude-review
🔇 Additional comments (37)
docs/TOOLS.md (2)

21-22: Verify section separation – headings may be rendered inside the preceding list.

There’s no blank line between the last bullet of the previous section and this ### iOS Simulator Development heading. If you regenerate after fixing the updater script to add a trailing newline per workflow, this should resolve globally.

Would you like me to open a follow-up to regenerate docs after merging the script fix?


47-47: Rename acknowledgment looks good.

The doc rename to get_mac_app_path aligns with the code refactor and improves consistency with iOS counterpart naming.

src/mcp/tools/device/__tests__/launch_app_device.test.ts (1)

12-12: Good migration to test-only mocks.

Switching to test-utils/mock-executors removes test dependency on production executors and helps avoid “module already linked” issues in CI.

src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts (1)

9-9: Nice: tests now use centralized mock executors.

This aligns tests with the new architecture and keeps production code out of the test environment.

src/utils/validation.ts (1)

26-26: Use a type-only import with the .js specifier for FileSystemExecutor

Replace the runtime .ts import with a type‐only import from the .js module:

- import { FileSystemExecutor } from './FileSystemExecutor.ts';
+ import type { FileSystemExecutor } from './FileSystemExecutor.js';

Since FileSystemExecutor is an interface (no runtime value), this prevents importing a .ts file at runtime and keeps our ESM bundles clean. For consistency and to avoid similar runtime‐resolution issues, consider applying the same change wherever you import executor types (e.g. in src/utils/command.ts, src/utils/template-manager.ts, and your test‐utils). Ensure your TypeScript setup (>=4.5) supports type‐only imports from .js specifiers. [optional_refactors_recommended]

src/utils/__tests__/simulator-utils.test.ts (1)

3-3: LGTM: tests now depend on centralized test-utils mocks

Switching to ../../test-utils/mock-executors.js decouples tests from production utils and aligns with the new architecture. Nice.

src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts (1)

4-4: LGTM: import migrated to shared test-utils

This aligns tests to the new mock executors module and reduces production coupling.

src/mcp/resources/__tests__/simulators.test.ts (1)

5-5: LGTM: migrate to test-utils mocks

This keeps the resource tests independent from production command infrastructure as intended in this PR.

src/mcp/tools/device/__tests__/install_app_device.test.ts (1)

8-8: Import moved to test-utils – decoupled tests from production code

Verified that test-utils imports don’t appear outside of test directories. This change centralizes test-only mocks and prevents accidental production imports, which should help avoid “module already linked” CI failures. LGTM.

src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts (1)

13-13: Centralized mock import – good boundary hygiene

Switching to test-utils keeps the utils barrel clean and avoids cross-linking test code into production modules. No change needed.

src/mcp/tools/macos/__tests__/launch_mac_app.test.ts (1)

12-12: Use of createMockFileSystemExecutor from test-utils is appropriate

This keeps filesystem mocking clearly scoped to tests and removes implicit coupling with production executors. Looks good.

src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts (1)

7-7: Import migrated to test-utils – aligns with refactor goals

The tests now depend on dedicated mock factories instead of production exports, which should reduce CI linking noise. ✅

src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts (1)

3-3: Import updated to test-utils – good separation

Nice cleanup. This reduces the risk of re-linking production utils during test runs.

src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts (1)

8-8: Good decoupling: switch to test-utils mocks avoids production coupling in tests

Importing mocks from test-utils aligns with the new boundaries and eliminates accidental server/utils linkage in test runs. The type-only import also keeps runtime clean.

src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts (1)

12-12: LGTM: centralized mock import

Consistent with the new test-utils module; helps keep test dependencies hermetic.

src/mcp/tools/device/__tests__/build_device.test.ts (1)

8-8: LGTM: import now correctly targets test-only mocks

This aligns with the new architecture and should help prevent the CI “module already linked” issues.

src/mcp/tools/simulator/__tests__/screenshot.test.ts (1)

10-13: Mocks Centralized and Verified
All test executors are now consistently imported from src/test-utils/mock-executors.ts, and production code remains unaffected:

  • src/test-utils/mock-executors.ts exports all four factories:
    createMockExecutor
    createNoopExecutor
    createCommandMatchingMockExecutor
    createMockFileSystemExecutor
  • No tests still import from utils/command.
  • Every test under __tests__ references test-utils/mock-executors.js.
  • No non-test code imports from test-utils/mock-executors.

This cleanly decouples test mocks from production utilities and prevents the CI “module already linked” errors. Approving these changes.

src/mcp/tools/ui-testing/__tests__/button.test.ts (1)

7-7: LGTM: switched to centralized test mocks.

Importing mocks from test-utils is consistent with the new architecture and reduces coupling with production utilities.

src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts (1)

9-12: LGTM: tests now depend on test-utils/mock-executors.

This keeps tests hermetic and avoids pulling in prod command modules.

src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts (1)

3-3: LGTM: import path migration to test-utils.

Clean separation of test helpers from production code.

src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts (1)

7-7: LGTM: mocks sourced from test-utils.

Consistent with the new module boundaries and should help prevent cross-environment linking issues in CI.

src/mcp/tools/device/__tests__/stop_app_device.test.ts (1)

8-8: Import moved to test-utils looks correct and aligns with the new boundaries

Path depth is correct and the switch to test-only mocks decouples the test from production utilities. API usage remains unchanged.

src/mcp/tools/macos/__tests__/test_macos.test.ts (1)

8-8: Import updated to test-utils: good move

This keeps the test harness isolated from production modules and matches the new architecture.

src/mcp/tools/ui-testing/__tests__/tap.test.ts (1)

7-7: Import source switch to test-utils confirmed

Correct relative path and consistent use with the new test scaffolding.

src/mcp/tools/simulator/__tests__/build_run_sim.test.ts (1)

8-11: Import path migration to test-utils looks good

Switching to centralized test mocks decouples tests from production executors and aligns with the new module boundaries.

src/mcp/tools/macos/__tests__/build_macos.test.ts (1)

10-10: LGTM on moving mocks to test-utils

This keeps tests isolated from production modules and supports the architectural refactor.

src/mcp/tools/ui-testing/__tests__/key_press.test.ts (1)

11-11: LGTM on switching to centralized mocks

Importing from test-utils/mock-executors.js keeps tests pure and avoids linking production modules during CI, which matches the PR goals.

src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts (1)

7-7: LGTM: migrated mocks to test-utils to prevent prod/test coupling.

Nice move—this directly targets the “module already linked” CI flake by removing test imports from prod barrels.

src/mcp/tools/utilities/__tests__/clean.test.ts (1)

3-3: LGTM: updated to centralized test mocks.

Importing from test-utils/mock-executors keeps tests isolated from production code and aligns with the new boundaries.

src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts (1)

17-17: LGTM: test mocks sourced from test-utils.

This matches the architecture refactor and reduces the chance of cross-environment side effects during CI.

src/mcp/tools/project-discovery/__tests__/discover_projs.test.ts (1)

13-13: LGTM: imports switched to test-utils.

Consistent with the repo-wide migration; keeps the test surface clean and deterministic.

src/mcp/tools/device/__tests__/test_device.test.ts (1)

9-12: Good move: decouple tests from production command module

Switching to test-utils/mock-executors keeps tests pure and prevents accidental coupling to server/runtime code. No issues spotted.

src/utils/__tests__/typed-tool-factory.test.ts (1)

8-8: LGTM: migrate to centralized test mocks

Importing createMockExecutor from test-utils/mock-executors is consistent with the new boundaries. Keeps this test side-effect-free.

src/mcp/tools/simulator/__tests__/list_sims.test.ts (1)

3-6: LGTM: centralized mocks import

Using test-utils/mock-executors across tests aligns with the new architecture and avoids production module linkage during CI. Looks good.

src/mcp/tools/simulator/__tests__/open_sim.test.ts (1)

9-9: LGTM: type-only import aligns with the new test-utils boundary

Using type CommandExecutor from the centralized test-utils/mock-executors.js is exactly what we want post refactor.

src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts (1)

17-17: LGTM: tests now consume mocks from test-utils

Importing createMockFileSystemExecutor from the centralized test-utils module matches the new architecture and keeps tests decoupled from production code.

src/utils/command.ts (1)

201-229: Great safety checks for tests

Guarding default executors in test environments prevents accidental real I/O during tests. Messaging is clear and actionable.

Comment on lines +33 to +34
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix stray “or” and add “Example:” label for stop_app_sim.

Keeps descriptions consistent across tools.

-- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
+- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. Example: stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. Example: stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
🤖 Prompt for AI Agents
In docs/TOOLS.md around lines 33 to 34, the stop_app_sim entry contains a stray
"or" and its example lacks an "Example:" label; update the stop_app_sim bullet
to remove the extra "or" so it reads as a single sentence, and add an "Example:"
label before the usage (keeping the example format consistent with other tool
entries), ensuring punctuation and casing match the test_sim line style.

}

return content + '\n';
return content;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix Markdown section breaks: ensure a blank line between tool lists and the next header.

Returning content without a trailing newline can cause the following ### header to be swallowed by the preceding list item on some renderers (including GitHub). Restore the trailing newline to keep headings rendering correctly.

-  return content;
+  return content + '\n';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return content;
return content + '\n';
🤖 Prompt for AI Agents
In scripts/update-tools-docs.ts around line 95, the function returns content
without a trailing newline which can cause the next "###" header to be rendered
as part of the previous list; fix this by ensuring the content string ends with
a single newline before returning (e.g., append "\n" if missing) so there is a
blank line between the tool list and the following header.


import { describe, it, expect } from 'vitest';
import { createMockExecutor } from '../../../../utils/command.js';
import { createMockExecutor } from '../../../../test-utils/mock-executors.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Sanity-check for leftover legacy imports and TS ESM config

To ensure the refactor is complete and stable across environments, verify there are no remaining imports from the old utils/command.js and that TS ESM settings support mixed .ts/.js extension imports used in tests.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Searching for legacy imports of utils/command.js (expect: no matches)"
rg -nP --type=ts -C0 "from\s+['\"][./]+.*utils/command\.js['\"]" || true

echo
echo "2) Count of current imports of test-utils/mock-executors.(ts|js) (informational)"
rg -nP --type=ts -C0 "from\s+['\"][./]+.*test-utils/mock-executors\.(ts|js)['\"]" | wc -l

echo
echo "3) Checking tsconfig settings related to ESM and extension imports (NodeNext + allowImportingTsExtensions recommended)"
fd -HI --glob "tsconfig*.json" | while read -r f; do
  echo "== $f =="
  jq '{module: .compilerOptions.module, moduleResolution: .compilerOptions.moduleResolution, allowImportingTsExtensions: .compilerOptions.allowImportingTsExtensions, verbatimModuleSyntax: .compilerOptions.verbatimModuleSyntax, esModuleInterop: .compilerOptions.esModuleInterop}' "$f" || true
done

echo
echo "Expected:"
echo "- No results in step 1."
echo "- module: \"NodeNext\" and allowImportingTsExtensions: true (or an equivalent setup that supports .ts and .js extension imports in tests)."

Length of output: 5173


Remove leftover legacy utils/command.js imports & align TS ESM configuration

  • Legacy imports detected in 22 files (run rg "utils/command\\.js" for full list), for example:
    • src/mcp/tools/project-discovery/get_mac_bundle_id.ts:14
    • src/mcp/tools/project-discovery/get_app_bundle_id.ts:15
    • src/mcp/tools/project-discovery/discover_projs.ts:12
    • src/core/dynamic-tools.ts:2
    • src/mcp/tools/logging/stop_device_log_cap.ts:13
    …and 17 more.
    Please replace all of these with the new executor import (e.g. from '../../../../test-utils/mock-executors.js' or its .ts variant).

  • TSConfig updates required to support mixed-extension imports under ESM:
    In both tsconfig.json and tsconfig.test.json, under compilerOptions add or update:

    {
      "compilerOptions": {
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "allowImportingTsExtensions": true
        // preserve other flags (e.g. esModuleInterop) as needed
      }
    }
🤖 Prompt for AI Agents
In src/mcp/tools/device/__tests__/stop_app_device.test.ts around line 8, the
test is importing the legacy utils/command.js; replace that legacy import with
the new mock executor import (e.g. from
'../../../../test-utils/mock-executors.js' or the .ts variant) to match the
other updated files, and then update tsconfig.json and tsconfig.test.json under
compilerOptions to set "module": "NodeNext", "moduleResolution": "NodeNext", and
"allowImportingTsExtensions": true so TypeScript ESM mixed-extension imports
work correctly; preserve all other existing compiler flags.

Comment on lines 12 to 13
import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Fix ESM import: use .js extension and type-only import for FileSystemExecutor

Importing from a .ts path will break at runtime under Node ESM once transpiled. Also, FileSystemExecutor is a type; use import type to avoid emitting a runtime import (especially if verbatimModuleSyntax is enabled).

Apply this diff:

-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';

Run this to find any remaining .ts path imports for FileSystemExecutor:


🏁 Script executed:

#!/bin/bash
# Find imports that reference FileSystemExecutor.ts (should be .js)
rg -nP -C2 "from ['\"][^'\"]*FileSystemExecutor\.ts['\"]"

Length of output: 5588


Fix ESM import paths and use import type for FileSystemExecutor

Node ESM requires .js extensions at runtime, and since FileSystemExecutor is a type, it should be imported with import type to avoid emitting a runtime import (especially when verbatimModuleSyntax is enabled). Please update all imports of FileSystemExecutor.ts in the following files:

  • src/utils/command.ts (line 16)
  • src/utils/validation.ts (line 26)
  • src/utils/template-manager.ts (line 7)
  • src/test-utils/mock-executors.ts (line 20)
  • src/mcp/tools/project-discovery/discover_projs.ts (line 13)
  • src/mcp/tools/project-discovery/get_app_bundle_id.ts (line 16)
  • src/mcp/tools/project-discovery/get_mac_bundle_id.ts (line 15)
  • src/mcp/tools/device/test_device.ts (line 19)
  • src/mcp/tools/macos/test_macos.ts (line 19)
  • src/mcp/tools/macos/launch_mac_app.ts (line 13)
  • src/mcp/tools/project-scaffolding/scaffold_macos_project.ts (line 18)
  • src/mcp/tools/logging/stop_device_log_cap.ts (line 14)

For each, replace:

-import { FileSystemExecutor } from '…/FileSystemExecutor.ts';
+import type { FileSystemExecutor } from '…/FileSystemExecutor.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';
🤖 Prompt for AI Agents
In src/mcp/tools/project-discovery/discover_projs.ts around lines 12 to 13, the
import of FileSystemExecutor is using a .ts path and a runtime import; update
the imports to use Node ESM-safe .js module specifiers and import the
FileSystemExecutor only as a type: change any direct import of
'../../../utils/FileSystemExecutor.ts' to use "import type { FileSystemExecutor
} from '../../../utils/FileSystemExecutor.js';" and ensure other runtime imports
keep their .js extensions (e.g.,
getDefaultFileSystemExecutor/getDefaultCommandExecutor) so no .ts paths or
non-ESM-safe imports remain.

getDefaultFileSystemExecutor,
getDefaultCommandExecutor,
} from '../../../utils/command.js';
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid importing .ts files at runtime; use .js specifier and type-only import

Align with your ESM import style and prevent accidental runtime imports of types.

-import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
+import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts';
import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.js';
🤖 Prompt for AI Agents
In src/mcp/tools/project-discovery/get_app_bundle_id.ts around line 16, the
current import pulls a .ts module which can cause runtime ESM resolution issues;
change the import to use the .js specifier and make it a type-only import if you
only need types (e.g., import type { FileSystemExecutor } from
'../../../utils/FileSystemExecutor.js'), or if you require runtime values import
from the .js file without the type modifier so the runtime loader resolves
correctly; update the import path and modifier accordingly and ensure any
build/tsconfig settings support preserving .js extensions.

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { createMockExecutor, createNoopExecutor } from '../../../../utils/command.js';
import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm there are no lingering imports from utils/command.js and no production imports of test-utils

The import move looks correct. Run the following to verify the migration is complete and type re-exports are in place:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "1) Finding any test files still importing utils/command.js..."
rg -nP -C1 '(from|require\()\s*[\'"]\.{1,4}/(?:\.{1,2}/)*utils/command\.js[\'"]' --type=ts --type=tsx --type=js --type=jsx src || true

echo
echo "2) Ensuring production code does not import test-utils..."
rg -nP 'test-utils/mock-executors' -g '!**/__tests__/**' -g '!**/*.test.*' -g '!**/test-utils/**' src || true

echo
echo "3) Checking that test-utils/mock-executors exports CommandExecutor type..."
rg -nP -C2 'export\s+(?:type\s+)?\{?\s*CommandExecutor\b' src/test-utils/mock-executors.ts || true

echo
echo "4) Showing the CommandExecutor type declaration in utils (for reference)..."
rg -nP -C2 '^\s*export\s+type\s+CommandExecutor\b|^\s*export\s+interface\s+CommandExecutor\b' src/utils --type=ts || true

Length of output: 263


The previous script failed due to quoting issues around the regex. Please rerun with the corrected patterns:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "1) Finding any test files still importing utils/command.js..."
rg -nP -C1 "(from|require\\()\\s*['\"]\\.{1,4}/(?:\\.{1,2}/)*utils/command\\.js['\"]" --type=ts --type=tsx --type=js --type=jsx src || true

echo
echo "2) Ensuring production code does not import test-utils..."
rg -nP "test-utils/mock-executors" -g '!**/__tests__/**' -g '!**/*.test.*' -g '!**/test-utils/**' src || true

echo
echo "3) Checking that test-utils/mock-executors exports CommandExecutor type..."
rg -nP -C2 "export\s+(?:type\s+)?\{?\s*CommandExecutor\b" src/test-utils/mock-executors.ts || true

echo
echo "4) Showing the CommandExecutor type declaration in utils (for reference)..."
rg -nP -C2 "^\s*export\s+type\s+CommandExecutor\b|^\s*export\s+interface\s+CommandExecutor\b" src/utils --type=ts || true

Length of output: 1407


To verify that the migration is fully complete, let’s inspect the test-utils file and rerun the import checks without relying on unsupported file-type flags:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "1) Searching for any tests still importing utils/command.js..."
rg -nP "utils/command\\.js" -g "__tests__/**" -g "*.test.*" src || echo "✅ No utils/command.js imports found in tests"

echo
echo "2) Ensuring no production code imports test-utils/mock-executors..."
rg -nP "test-utils/mock-executors" \
  -g '!**/__tests__/**' \
  -g '!**/*.test.*' \
  -g '!**/test-utils/**' src \
  || echo "✅ No production imports of test-utils/mock-executors"

echo
echo "3) Listing all exports in src/test-utils/mock-executors.ts…"
rg -nP "^export " -n src/test-utils/mock-executors.ts || echo "⚠️ No exports found"

echo
echo "4) Looking for CommandExecutor references in mock-executors.ts…"
rg -nP "CommandExecutor" -n src/test-utils/mock-executors.ts || echo "⚠️ No CommandExecutor reference found"

Length of output: 1878


Add CommandExecutor type re-export to mock-executors

All migration checks passed—no leftover utils/command.js imports in tests and no production code importing test-utils. The only missing piece is re-exporting the CommandExecutor type from your mock file so consumers can import it directly:

File: src/test-utils/mock-executors.ts
Add at the top:

export type { CommandExecutor } from '../utils/CommandExecutor.ts';
🤖 Prompt for AI Agents
In src/test-utils/mock-executors.ts (add at file top), re-export the
CommandExecutor type so tests can import it directly; add an export type line
that re-exports CommandExecutor from ../utils/CommandExecutor.ts (i.e. export
type { CommandExecutor } from '../utils/CommandExecutor.ts';) and save the file
so consumers can import the type from the mock-executors module.

Comment on lines +18 to +21
import { ChildProcess } from 'child_process';
import { CommandExecutor } from '../utils/CommandExecutor.ts';
import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ESM specifiers and use type-only imports for types

Like in utils/command.ts, importing from .ts paths will break at runtime. Also mark imports as type-only to avoid runtime loads.

Apply this diff:

-import { ChildProcess } from 'child_process';
-import { CommandExecutor } from '../utils/CommandExecutor.ts';
-import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
+import type { ChildProcess } from 'child_process';
+import type { CommandExecutor } from '../utils/CommandExecutor.js';
+import type { FileSystemExecutor } from '../utils/FileSystemExecutor.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { ChildProcess } from 'child_process';
import { CommandExecutor } from '../utils/CommandExecutor.ts';
import { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
import type { ChildProcess } from 'child_process';
import type { CommandExecutor } from '../utils/CommandExecutor.js';
import type { FileSystemExecutor } from '../utils/FileSystemExecutor.js';
🤖 Prompt for AI Agents
In src/test-utils/mock-executors.ts around lines 18 to 21, the imports use .ts
specifiers and are not marked type-only which can break ESM runtime loading and
cause unnecessary runtime imports; change them to ESM-safe specifiers and
type-only imports (e.g., use "import type { ChildProcess } from
'child_process';" and "import type { CommandExecutor } from
'../utils/CommandExecutor';" and "import type { FileSystemExecutor } from
'../utils/FileSystemExecutor';") so the compiler keeps them as types only and
the runtime does not attempt to load .ts modules.

Comment on lines +39 to +43
if (result instanceof Error || typeof result === 'string') {
return async () => {
throw result;
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Throw Error instances, not strings

Throwing a string loses stack and type information. Wrap string results in an Error.

Apply this diff:

-  if (result instanceof Error || typeof result === 'string') {
+  if (result instanceof Error || typeof result === 'string') {
     return async () => {
-      throw result;
+      if (result instanceof Error) throw result;
+      throw new Error(result);
     };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result instanceof Error || typeof result === 'string') {
return async () => {
throw result;
};
}
if (result instanceof Error || typeof result === 'string') {
return async () => {
if (result instanceof Error) throw result;
throw new Error(result);
};
}
🤖 Prompt for AI Agents
In src/test-utils/mock-executors.ts around lines 39 to 43, the code may throw a
string which loses stack and type information; change the returned async
function to ensure it always throws an Error instance by wrapping string results
in new Error(result) while leaving existing Error objects untouched (i.e., if
result is an Error re-throw it, otherwise construct and throw new
Error(result.toString())).

Comment on lines 16 to 21
import { FileSystemExecutor } from './FileSystemExecutor.ts';
import { CommandExecutor, CommandResponse } from './CommandExecutor.ts';

/**
* Command execution response interface
*/
export interface CommandResponse {
success: boolean;
output: string;
error?: string;
process: ChildProcess;
}

/**
* Command executor function type for dependency injection
*/
export type CommandExecutor = (
command: string[],
logPrefix?: string,
useShell?: boolean,
env?: Record<string, string>,
detached?: boolean,
) => Promise<CommandResponse>;

/**
* File system executor interface for dependency injection
*/
export interface FileSystemExecutor {
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
readFile(path: string, encoding?: BufferEncoding): Promise<string>;
writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>;
cp(source: string, destination: string, options?: { recursive?: boolean }): Promise<void>;
readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]>;
rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
existsSync(path: string): boolean;
stat(path: string): Promise<{ isDirectory(): boolean }>;
mkdtemp(prefix: string): Promise<string>;
tmpdir(): string;
}
// Re-export types for backward compatibility
export { CommandExecutor, CommandResponse } from './CommandExecutor.js';
export { FileSystemExecutor } from './FileSystemExecutor.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ESM import specifiers and use type-only imports for types

Importing from .ts paths will break at runtime in Node ESM builds. Also, use type-only imports to avoid runtime loading. The re-exports are correctly pointing at .js—mirror that for local type imports.

Apply this diff:

-import { FileSystemExecutor } from './FileSystemExecutor.ts';
-import { CommandExecutor, CommandResponse } from './CommandExecutor.ts';
+import type { FileSystemExecutor } from './FileSystemExecutor.js';
+import type { CommandExecutor, CommandResponse } from './CommandExecutor.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { FileSystemExecutor } from './FileSystemExecutor.ts';
import { CommandExecutor, CommandResponse } from './CommandExecutor.ts';
/**
* Command execution response interface
*/
export interface CommandResponse {
success: boolean;
output: string;
error?: string;
process: ChildProcess;
}
/**
* Command executor function type for dependency injection
*/
export type CommandExecutor = (
command: string[],
logPrefix?: string,
useShell?: boolean,
env?: Record<string, string>,
detached?: boolean,
) => Promise<CommandResponse>;
/**
* File system executor interface for dependency injection
*/
export interface FileSystemExecutor {
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
readFile(path: string, encoding?: BufferEncoding): Promise<string>;
writeFile(path: string, content: string, encoding?: BufferEncoding): Promise<void>;
cp(source: string, destination: string, options?: { recursive?: boolean }): Promise<void>;
readdir(path: string, options?: { withFileTypes?: boolean }): Promise<unknown[]>;
rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
existsSync(path: string): boolean;
stat(path: string): Promise<{ isDirectory(): boolean }>;
mkdtemp(prefix: string): Promise<string>;
tmpdir(): string;
}
// Re-export types for backward compatibility
export { CommandExecutor, CommandResponse } from './CommandExecutor.js';
export { FileSystemExecutor } from './FileSystemExecutor.js';
import type { FileSystemExecutor } from './FileSystemExecutor.js';
import type { CommandExecutor, CommandResponse } from './CommandExecutor.js';
// Re-export types for backward compatibility
export { CommandExecutor, CommandResponse } from './CommandExecutor.js';
export { FileSystemExecutor } from './FileSystemExecutor.js';
🤖 Prompt for AI Agents
In src/utils/command.ts around lines 16 to 21, the file currently imports from
.ts specifiers which will break under Node ESM and performs normal imports for
types; change those imports to type-only imports that reference the .js
specifiers (e.g. import type { FileSystemExecutor } from
'./FileSystemExecutor.js' and import type { CommandExecutor, CommandResponse }
from './CommandExecutor.js') so no runtime modules are loaded and the import
paths match the re-exports.

Comment on lines 6 to 7
import { CommandExecutor } from './command.js';
import { FileSystemExecutor } from './FileSystemExecutor.ts';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Use .js extension (or barrel) for internal imports to avoid ESM resolution pitfalls

Similar to other imports in this repo, prefer importing with .js. Importing .ts can break under NodeNext unless you explicitly enable TS extension imports.

Apply this diff:

-import { CommandExecutor } from './command.js';
-import { FileSystemExecutor } from './FileSystemExecutor.ts';
+import { CommandExecutor } from './command.js';
+import { FileSystemExecutor } from './FileSystemExecutor.js';

Verification script (optional):


🏁 Script executed:

#!/bin/bash
# Show imports using .ts extensions in utils
rg -nP "from\s+['\"][./].*\.ts['\"]" src/utils --type=ts -S

Length of output: 431


Use .js extension for internal ESM imports in src/utils
To avoid NodeNext resolution issues, all imports of local helpers should reference the .js output files, not .ts.

Files needing updates:

  • src/utils/template-manager.ts (line 7): change ./FileSystemExecutor.ts./FileSystemExecutor.js
  • src/utils/validation.ts (line 26): change ./FileSystemExecutor.ts./FileSystemExecutor.js
  • src/utils/command.ts (lines 16–17): change
    • ./FileSystemExecutor.ts./FileSystemExecutor.js
    • ./CommandExecutor.ts./CommandExecutor.js

Example diff for template-manager.ts:

--- a/src/utils/template-manager.ts
+++ b/src/utils/template-manager.ts
@@ -6,7 +6,7 @@
 import { CommandExecutor } from './command.js';
-import { FileSystemExecutor } from './FileSystemExecutor.ts';
+import { FileSystemExecutor } from './FileSystemExecutor.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { CommandExecutor } from './command.js';
import { FileSystemExecutor } from './FileSystemExecutor.ts';
import { CommandExecutor } from './command.js';
import { FileSystemExecutor } from './FileSystemExecutor.js';
🤖 Prompt for AI Agents
In src/utils/template-manager.ts around lines 6 to 7, the local ESM import uses
a .ts extension which can break NodeNext resolution; change the import path from
./FileSystemExecutor.ts to ./FileSystemExecutor.js so internal imports reference
the emitted .js files; similarly update the other listed files
(src/utils/validation.ts line 26 and src/utils/command.ts lines 16–17) to use
.js extensions for their local imports.

This commit implements a comprehensive architectural improvement that replaces
the previous barrel import pattern (importing everything from utils/index.ts)
with focused facades that expose specific functionality through dedicated
index.ts files in subdirectories.

Key Changes:
- Created 11 focused facade modules in src/utils/ (execution, logging, responses, validation, axe, plugin-registry, xcodemake, template, version, test, log-capture)
- Migrated 65+ tool files from barrel imports to focused facade imports
- Added ESLint rule to prevent regression to barrel imports
- Updated ARCHITECTURE.md to document the new module organization pattern

Performance Impact:
- Eliminates loading of unused modules, reducing startup time
- Improves module resolution speed for Node.js runtime
- Prevents circular dependency issues that were risks with large barrel files
- Enables better tree-shaking for bundlers

Architecture Benefits:
- Clear dependency graph - imports explicitly show what functionality is needed
- Reduced coupling between unrelated utility components
- Better maintainability and code organization
- Type-safe imports with focused interfaces
@claude
Copy link
Contributor

claude bot commented Aug 16, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Progress

  • Analyze new directory structure and module separation
  • Review focused facades pattern implementation
  • Examine test utilities separation
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage and architecture
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new src/test-utils/ directory creates clear boundaries between production command execution and testing utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Interface Extraction: Clean extraction of CommandExecutor and FileSystemExecutor interfaces into separate files

2. CI Issue Resolution

  • Root Cause Analysis: Excellent identification of the import chain causing module linking issues (test files → utils/command.js → utils/logger.js → @sentry/node)
  • Surgical Approach: Moved only the problematic mock functions while preserving all production functionality
  • Zero Behavioral Changes: Maintained exact mock function behavior to preserve all test compatibility

3. Testing Architecture Improvements

  • Mock Executors: src/test-utils/mock-executors.ts provides clean, dependency-free testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy, avoiding vi.mock() anti-patterns
  • Comprehensive Mocking: Includes sophisticated mock types like createCommandMatchingMockExecutor for complex scenarios

4. Code Quality Enhancements

  • Type Safety: Proper TypeScript interfaces for executors improve type checking
  • Error Prevention: Guard functions in getDefaultCommandExecutor() and getDefaultFileSystemExecutor() prevent accidental production calls in tests
  • Documentation: Clear commit message and inline documentation explaining the changes

Technical Implementation Quality

1. Interface Design

// Clean, well-designed interfaces
export type CommandExecutor = (
  command: string[],
  logPrefix?: string,
  useShell?: boolean,
  env?: Record<string, string>,
  detached?: boolean,
) => Promise<CommandResponse>;

2. Test Safety Mechanisms

// Excellent guardrails to prevent production calls in tests
if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') {
  throw new Error('🚨 REAL SYSTEM EXECUTOR DETECTED IN TEST! 🚨\n...');
}

3. Mock Function Completeness

  • createMockExecutor() - Basic command mocking
  • createCommandMatchingMockExecutor() - Pattern-based command matching
  • createNoopExecutor() - Error-throwing guard for unexpected calls
  • createMockFileSystemExecutor() - Complete filesystem mocking
  • createMockEnvironmentDetector() - Environment detection mocking

⚠️ Minor Considerations

1. Import Path Maintenance

With 59 test files updated, future maintenance should ensure new tests use the correct import paths from test-utils/mock-executors.js rather than the old utils locations.

2. Documentation Updates

Consider updating docs/TESTING.md to reflect the new mock import patterns and emphasize the importance of using isolated test utilities.

3. Backward Compatibility

The re-exports in src/utils/command.ts (lines 24-25) maintain backward compatibility, which is excellent for gradual migration.

🚀 Performance & Security Assessment

Performance

  • ✅ Eliminates native module loading conflicts in test environments
  • ✅ Reduces test execution overhead by removing unnecessary production dependencies
  • ✅ Clean module boundaries improve build optimization

Security

  • ✅ No security implications - purely architectural improvement
  • ✅ Test isolation prevents accidental production system interactions during testing
  • ✅ Guard functions provide additional safety barriers

📋 Test Coverage & Reliability

The testing approach is exemplary:

  • ✅ All test files successfully migrated to use isolated mock executors
  • ✅ Mock dependencies provide comprehensive test coverage scenarios
  • ✅ No reliance on Vitest mocking maintains test reliability
  • ✅ Interface-based design enables easy testing of different scenarios

🎯 Recommendation

STRONGLY APPROVE - This is a surgical, well-executed fix that:

  1. Resolves the critical CI module linking issues through proper dependency isolation
  2. Maintains perfect backward compatibility with all existing functionality
  3. Preserves all test behavior while improving test architecture
  4. Follows excellent engineering practices with clear interfaces and guard functions
  5. Demonstrates strong problem-solving with precise root cause analysis

The solution is focused, elegant, and addresses the exact problem without introducing unnecessary complexity. The interface extraction and test utility isolation set up a solid foundation for future development.

This fix should resolve the Ubuntu CI "module is already linked" errors while maintaining all existing functionality.


Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🔭 Outside diff range comments (24)
src/mcp/tools/discovery/discover_tools.ts (1)

24-31: Harden env var parsing to prevent NaN/invalid LLM config.

If the env vars are set to non-numeric values, parseInt/parseFloat will yield NaN and propagate to createMessage, likely causing runtime errors. Add guards and reasonable bounds.

Apply this diff:

-const getLLMConfig = (): LLMConfig => ({
-  maxTokens: process.env.XCODEBUILDMCP_LLM_MAX_TOKENS
-    ? parseInt(process.env.XCODEBUILDMCP_LLM_MAX_TOKENS, 10)
-    : 200,
-  temperature: process.env.XCODEBUILDMCP_LLM_TEMPERATURE
-    ? parseFloat(process.env.XCODEBUILDMCP_LLM_TEMPERATURE)
-    : undefined,
-});
+const getLLMConfig = (): LLMConfig => {
+  const rawMax = process.env.XCODEBUILDMCP_LLM_MAX_TOKENS;
+  const rawTemp = process.env.XCODEBUILDMCP_LLM_TEMPERATURE;
+
+  let maxTokens = 200;
+  if (rawMax) {
+    const parsed = Number.parseInt(rawMax, 10);
+    if (Number.isFinite(parsed) && parsed > 0) {
+      // Clamp to a sane upper bound to avoid client errors
+      maxTokens = Math.min(parsed, 4000);
+    } else {
+      log('warn', `Invalid XCODEBUILDMCP_LLM_MAX_TOKENS="${rawMax}", using default ${maxTokens}`);
+    }
+  }
+
+  let temperature: number | undefined;
+  if (rawTemp !== undefined) {
+    const parsed = Number.parseFloat(rawTemp);
+    if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 2) {
+      temperature = parsed;
+    } else {
+      log('warn', `Invalid XCODEBUILDMCP_LLM_TEMPERATURE="${rawTemp}", ignoring`);
+    }
+  }
+
+  return { maxTokens, temperature };
+};
src/mcp/tools/simulator/list_sims.ts (1)

74-83: Ensure all failure returns include isError: true in list_sims.ts

The list_sims tool currently omits isError: true in its inner failure paths, unlike other tools. Please update the following sections to return error flags consistently:

• src/mcp/tools/simulator/list_sims.ts
– Lines 74–83: on !result.success
– Lines 88–97: when !isSimulatorData(parsedData)
– Lines 135–143: in the inner catch block

Apply this patch:

--- a/src/mcp/tools/simulator/list_sims.ts
+++ b/src/mcp/tools/simulator/list_sims.ts
@@ 74,8 @@
-    if (!result.success) {
-      return {
+    if (!result.success) {
+      return {
         content: [
           {
             type: 'text',
             text: `Failed to list simulators: ${result.error}`,
           },
+        isError: true,
       };
@@ 88,8 @@
-      if (!isSimulatorData(parsedData)) {
-        return {
+      if (!isSimulatorData(parsedData)) {
+        return {
           content: [
             {
               type: 'text',
               text: 'Failed to parse simulator data: Invalid format',
             },
+          isError: true,
         };
@@ 135,8 @@
-    } catch {
-      return {
+    } catch {
+      return {
         content: [
           {
             type: 'text',
             text: result.output,
           },
+        isError: true,
       };
     }
src/mcp/tools/doctor/doctor.ts (2)

14-15: Fix ESM import: avoid importing a .ts file at runtime

Importing ./lib/doctor.deps.ts will emit a runtime import to a .ts path, which Node cannot resolve in ESM builds. Use the .js extension (TypeScript will type-check against the .d.ts) and keep the type-only binding.

-import { type DoctorDependencies, createDoctorDependencies } from './lib/doctor.deps.ts';
+import { type DoctorDependencies, createDoctorDependencies } from './lib/doctor.deps.js';

223-224: Standardize Sentry opt-out env var across the codebase

The PR objective specifies XCODEBUILDMCP_SENTRY_DISABLED as the canonical opt-out flag. We’re still referencing the old SENTRY_DISABLED in multiple places, which will lead to incorrect reporting and behavior. Please update all occurrences:

• README.md (opt-out docs & examples)
• src/utils/sentry.ts (env var list)
• src/utils/logger.ts (SENTRY_ENABLED flag)
• src/mcp/tools/doctor/lib/doctor.deps.ts (dependency declarations)
• src/mcp/tools/doctor/tests/doctor.test.ts (test fixtures)
• src/mcp/tools/doctor/doctor.ts (doctor output)

Apply this change in doctor.ts as an example:

-    `\n## Sentry`,
-    `- Sentry enabled: ${doctorInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
+    `\n## Sentry`,
+    `- Sentry enabled: ${doctorInfo.environmentVariables.XCODEBUILDMCP_SENTRY_DISABLED === 'true' ? '❌ No' : '✅ Yes'}`,

After updating, run a global grep for SENTRY_DISABLED to confirm no legacy references remain.

src/mcp/tools/logging/start_device_log_cap.ts (5)

51-62: Bug: tmpdir mismatch when FileSystemExecutor is injected

You compute the log path with os.tmpdir() but create the directory via fileSystemExecutor.tmpdir() when a fileSystemExecutor is provided. If the two differ (very likely in tests/mocks), the created path may not exist for the subsequent fs operations, causing ENOENT. Use a single tmpDir source consistently.

Apply this diff to normalize tmpDir usage:

-  const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`;
-  const logFilePath = path.join(os.tmpdir(), logFileName);
+  const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`;
+  const tmpDir = fileSystemExecutor ? fileSystemExecutor.tmpdir() : os.tmpdir();
+  const logFilePath = path.join(tmpDir, logFileName);
@@
-    if (fileSystemExecutor) {
-      await fileSystemExecutor.mkdir(fileSystemExecutor.tmpdir(), { recursive: true });
-      await fileSystemExecutor.writeFile(logFilePath, '');
-    } else {
-      await fs.promises.mkdir(os.tmpdir(), { recursive: true });
-      await fs.promises.writeFile(logFilePath, '');
-    }
+    if (fileSystemExecutor) {
+      await fileSystemExecutor.mkdir(tmpDir, { recursive: true });
+      await fileSystemExecutor.writeFile(logFilePath, '');
+    } else {
+      await fs.promises.mkdir(tmpDir, { recursive: true });
+      await fs.promises.writeFile(logFilePath, '');
+    }

64-69: Resource leak: write stream is never tracked or closed

logStream is created but not managed. It’s not stored for later closure, nor closed on errors. Keep a handle in the session so the stop operation can flush/close it; add an error handler to avoid silent failures.

-  const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
+  const logStream = fs.createWriteStream(logFilePath, { flags: 'a' });
+  logStream.on('error', (e) => {
+    log('warn', `Device log stream error for ${logFilePath}: ${e instanceof Error ? e.message : String(e)}`);
+  });

31-31: Add strong typing for session registry

activeDeviceLogSessions is untyped, which weakens guarantees for downstream stop/cleanup logic. Add a typed Map including the process and stream handles.

+import type { ChildProcess } from 'node:child_process';
@@
-export const activeDeviceLogSessions = new Map();
+export const activeDeviceLogSessions: Map<
+  string,
+  {
+    process: ChildProcess | undefined;
+    logFilePath: string;
+    deviceUuid: string;
+    bundleId: string;
+    logStream: fs.WriteStream;
+  }
+> = new Map();

91-97: Persist the log stream in the session for proper lifecycle management

Store the stream so your stop operation can flush/close and you can later handle cleanup safely.

-  activeDeviceLogSessions.set(logSessionId, {
-    process: result.process,
-    logFilePath,
-    deviceUuid,
-    bundleId,
-  });
+  activeDeviceLogSessions.set(logSessionId, {
+    process: result.process,
+    logFilePath,
+    deviceUuid,
+    bundleId,
+    logStream,
+  });

70-88: Adjust Device Log Capture to use detached mode
The CommandExecutor supports a detached flag (its 5th argument). By default (detached = false) it waits for the process to exit—which, with xcrun … --console, never happens and blocks forever.

To fix, pass true for detached and stream the child process yourself:

// Spawn the console session in background and get the process handle
const { process: childProc } = await executor(
  [
    'xcrun',
    'devicectl',
    'device',
    'process',
    'launch',
    '--console',
    '--terminate-existing',
    '--device',
    deviceUuid,
    bundleId,
  ],
  'Device Log Capture',
  true,       // useShell
  undefined,  // env
  true,       // detached!
);

// Pipe logs to your file/stream
childProc.stdout?.pipe(logFileStream);
childProc.stderr?.pipe(logFileStream);

// Store `childProc` in your session map so you can call childProc.kill() later
sessionMap.set(sessionId, childProc);

This way the promise resolves immediately with the ChildProcess handle, you capture its output as it arrives, and you avoid hanging on a never-ending console session.

src/mcp/tools/macos/build_run_macos.ts (1)

95-103: Arch parameter is ignored when resolving app path from build settings

You accept an arch parameter in the schema but don’t pass it to xcodebuild for the -showBuildSettings call, unlike get_mac_app_path which supports macOS arch. This can produce mismatched or ambiguous build settings on dual-arch setups.

Apply this patch to include -destination for macOS arch when present:

     // Add derived data path if provided
     if (params.derivedDataPath) {
       command.push('-derivedDataPath', params.derivedDataPath);
     }
 
+    // Handle destination for macOS when arch is specified (parity with get_mac_app_path)
+    if (params.arch) {
+      const destinationString = `platform=macOS,arch=${params.arch}`;
+      command.push('-destination', destinationString);
+    }
+
     // Add extra args if provided
     if (params.extraArgs && params.extraArgs.length > 0) {
       command.push(...params.extraArgs);
     }
src/mcp/tools/project-scaffolding/scaffold_ios_project.ts (4)

437-444: Avoid optional commandExecutor by using a default parameter.

You’re already importing getDefaultCommandExecutor statically. Use it as a default parameter and remove the dynamic import branch below. This prevents reintroducing the utils/index.js barrel (which the PR is trying to eliminate) and simplifies the control flow.

Apply this diff:

-async function scaffoldProject(
-  params: Record<string, unknown>,
-  commandExecutor?: CommandExecutor,
-  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
-): Promise<string> {
+async function scaffoldProject(
+  params: Record<string, unknown>,
+  commandExecutor: CommandExecutor = getDefaultCommandExecutor(),
+  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
+): Promise<string> {

459-463: Remove dynamic import from utils/index.js to prevent barrel reintroduction.

This dynamic import directly references the old barrel and undermines the PR’s separation. With the default parameter in place, this block is no longer needed.

Apply this diff:

-    // Import the default command executor if not provided
-    if (!commandExecutor) {
-      const { getDefaultCommandExecutor } = await import('../../../utils/index.js');
-      commandExecutor = getDefaultCommandExecutor();
-    }

72-85: Fix orientation mapping to match schema values (case/hyphen mismatch).

Current orientationToIOSConstant expects "Portrait"/"LandscapeLeft" etc., but schema provides 'portrait', 'landscape-left', 'landscape-right', 'portrait-upside-down'. This produces incorrect XCConfig values.

Apply this diff:

-function orientationToIOSConstant(orientation: string): string {
-  switch (orientation) {
-    case 'Portrait':
-      return 'UIInterfaceOrientationPortrait';
-    case 'PortraitUpsideDown':
-      return 'UIInterfaceOrientationPortraitUpsideDown';
-    case 'LandscapeLeft':
-      return 'UIInterfaceOrientationLandscapeLeft';
-    case 'LandscapeRight':
-      return 'UIInterfaceOrientationLandscapeRight';
-    default:
-      return orientation;
-  }
-}
+function orientationToIOSConstant(orientation: string): string {
+  // Normalize to lower-case for matching
+  const key = orientation.toLowerCase();
+  switch (key) {
+    case 'portrait':
+      return 'UIInterfaceOrientationPortrait';
+    case 'portrait-upside-down':
+      return 'UIInterfaceOrientationPortraitUpsideDown';
+    case 'landscape-left':
+      return 'UIInterfaceOrientationLandscapeLeft';
+    case 'landscape-right':
+      return 'UIInterfaceOrientationLandscapeRight';
+    default:
+      // Fallback: return original in case templates expect raw string
+      return orientation;
+  }
+}

90-101: Correct targetedDeviceFamily typing and mapping.

Schema defines targetedDeviceFamily as an array: ['iphone', 'ipad', 'universal'] yet code treats it as a string and maps to 'iPhone'/'iPad'/'iPhone+iPad'. This will silently generate wrong XCConfig values.

Apply these diffs to (1) map array values to numeric family, and (2) fix type casts and usage:

-function deviceFamilyToNumeric(family: string): string {
-  switch (family) {
-    case 'iPhone':
-      return '1';
-    case 'iPad':
-      return '2';
-    case 'iPhone+iPad':
-      return '1,2';
-    default:
-      return '1,2';
-  }
-}
+function deviceFamilyToNumeric(
+  families: ('iphone' | 'ipad' | 'universal')[],
+): string {
+  // If 'universal' is present, it's both iPhone and iPad
+  if (families.includes('universal')) return '1,2';
+  const codes = new Set<string>();
+  if (families.includes('iphone')) codes.add('1');
+  if (families.includes('ipad')) codes.add('2');
+  // Default to universal if nothing recognized
+  return codes.size ? Array.from(codes).sort().join(',') : '1,2';
+}
-  const targetedDeviceFamily = params.targetedDeviceFamily as string | undefined;
+  const targetedDeviceFamily = params.targetedDeviceFamily as
+    | ('iphone' | 'ipad' | 'universal')[]
+    | undefined;
-    if (targetedDeviceFamily) {
-      const deviceFamilyValue = deviceFamilyToNumeric(targetedDeviceFamily);
+    if (targetedDeviceFamily && targetedDeviceFamily.length > 0) {
+      const deviceFamilyValue = deviceFamilyToNumeric(targetedDeviceFamily);
       result = result.replace(
         /TARGETED_DEVICE_FAMILY = .+/g,
         `TARGETED_DEVICE_FAMILY = ${deviceFamilyValue}`,
       );
     }

Also applies to: 147-187

src/mcp/tools/ui-testing/screenshot.ts (1)

86-96: Use log level 'warn' instead of 'warning'.

Other tools use 'warn'. If log’s level type is a union, 'warning' will either be downgraded or fail type-checking/logging filters.

Apply this diff:

-        log('warning', `${LOG_PREFIX}/screenshot: Image optimization failed, using original PNG`);
+        log('warn', `${LOG_PREFIX}/screenshot: Image optimization failed, using original PNG`);
@@
-          log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temp file: ${err}`);
+          log('warn', `${LOG_PREFIX}/screenshot: Failed to delete temp file: ${err}`);
@@
-        log('warning', `${LOG_PREFIX}/screenshot: Failed to delete temporary files: ${err}`);
+        log('warn', `${LOG_PREFIX}/screenshot: Failed to delete temporary files: ${err}`);

Also applies to: 111-116

src/mcp/tools/simulator/launch_app_sim.ts (2)

179-188: Missing isError on failure response (breaks client error detection).

The error path returns content without isError, unlike other branches. This can cause the caller to treat failures as success.

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

211-218: Missing isError on catch-all error response.

Same issue here—errors should be explicit for consistent handling downstream.

   return {
     content: [
       {
         type: 'text',
         text: `Launch app in simulator operation failed: ${errorMessage}`,
       },
     ],
+    isError: true,
   };
src/mcp/tools/simulator/build_run_sim.ts (1)

203-206: Guard against missing output before regex parsing.

result.success can be true while output is empty. Add a check to avoid .match on undefined.

-    const buildSettingsOutput = result.output;
+    const buildSettingsOutput = result.output;
+    if (!buildSettingsOutput) {
+      return createTextResponse(
+        'Build succeeded, but no build settings output was returned. Ensure the scheme is valid and produces build settings.',
+        true,
+      );
+    }
src/mcp/tools/ui-testing/swipe.ts (2)

84-89: Duration message is dropped for duration=0.

The truthy check omits “0s” (valid per schema). Use an undefined check.

-  const optionsText = duration ? ` duration=${duration}s` : '';
+  const optionsText = duration !== undefined ? ` duration=${duration}s` : '';

199-203: Standardize log level naming: use ‘warning’ instead of ‘warn’

The logger function accepts any string, but its documentation and existing usages rely on the level warning. To avoid confusion and ensure consistency across the codebase, please replace all instances of:

log('warn', )

with

log('warning', )

– In this PR, update src/mcp/tools/ui-testing/swipe.ts lines 199–203.
– Consider running a codemod or search/replace for other log('warn'…) calls to align with the documented level.

src/mcp/tools/macos/stop_mac_app.ts (1)

49-54: Command injection risk via string interpolation in sh -c.

Interpolating appName into a shell command allows injection. Avoid sh -c and pass arguments positionally.

Apply this refactor to eliminate sh -c and maintain the fallback:

-      // Stop by app name - use shell command with fallback for complex logic
-      command = [
-        'sh',
-        '-c',
-        `pkill -f "${params.appName}" || osascript -e 'tell application "${params.appName}" to quit'`,
-      ];
+      // Stop by app name without shell interpolation; try exact match, then AppleScript quit
+      try {
+        await executor(['pkill', '-x', String(params.appName)], 'Stop macOS App (pkill)');
+      } catch {
+        // Fallback to AppleScript quit if pkill didn't match/failed
+        await executor(
+          ['osascript', '-e', `tell application "${params.appName}" to quit`],
+          'Stop macOS App (osascript)',
+        );
+      }
+      return {
+        content: [
+          {
+            type: 'text',
+            text: `✅ macOS app stopped successfully: ${params.appName}`,
+          },
+        ],
+      };

Note:

  • pkill -x matches the exact process name, reducing accidental matches.
  • We return early after executing the stop flow; if you prefer keeping the existing structure, we can restructure the try block accordingly.

I can adjust this patch to preserve the original control flow if preferred.

src/mcp/tools/ui-testing/tap.ts (1)

36-51: Extract and Share describe_ui Session Store

The coordinate‐warning maps in each UI tool (tap.ts, long_press.ts, swipe.ts, touch.ts) are never updated—only the internal map in describe_ui.ts is mutated via recordDescribeUICall. This means getCoordinateWarning will always warn. We need to centralize the session store and warning logic:

  • In src/mcp/tools/ui-testing/describe_ui.ts
    • Export the shared describeUITimestamps map (or provide a getDescribeSession helper).
    • Export getCoordinateWarning (and DESCRIBE_UI_WARNING_TIMEOUT) instead of duplicating them.

  • In each UI tool file (tap.ts, long_press.ts, swipe.ts, touch.ts)
    • Remove the local describeUITimestamps, DESCRIBE_UI_WARNING_TIMEOUT, and getCoordinateWarning.
    • Import the shared getCoordinateWarning (and any needed constants) from ./describe_ui.

Example diff for tap.ts:

- // Session tracking for describe_ui warnings (shared across UI tools)
- const describeUITimestamps = new Map<string, { timestamp: number }>();
- const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds
-
- function getCoordinateWarning(simulatorUuid: string): string | null {
-   const session = describeUITimestamps.get(simulatorUuid);
-
- }
+ import { getCoordinateWarning } from './describe_ui';
src/mcp/tools/doctor/lib/doctor.deps.ts (1)

170-171: Standardize Sentry env var name to align with PR (XCODEBUILDMCP_SENTRY_DISABLED).

The PR standardizes the opt‑out env var to XCODEBUILDMCP_SENTRY_DISABLED. Reflect that here so doctor reports the correct flag. Consider keeping the legacy SENTRY_DISABLED only if you intentionally want to surface it.

-        'SENTRY_DISABLED',
+        'XCODEBUILDMCP_SENTRY_DISABLED',
docs/ARCHITECTURE.md (1)

32-36: Sentry initialization text conflicts with lazy/dynamic production-only loading in PR summary

The “Initialization” step currently says “Sentry initialized for error tracking (optional)”, but the PR states Sentry is lazily loaded via dynamic import and only in production, with opt-out via XCODEBUILDMCP_SENTRY_DISABLED. Please update this text for consistency.

Suggested wording (outside diff range):

- Sentry initialized for error tracking (optional)
+ Sentry is lazily loaded via dynamic import in production environments (respects XCODEBUILDMCP_SENTRY_DISABLED for opt-out)
♻️ Duplicate comments (2)
src/utils/logger.ts (1)

60-62: Avoid top‑level logging to keep utils side‑effect‑free.

Logging on import violates the “pure utils” constraint and can produce unexpected output in consumers.

-if (!SENTRY_ENABLED) {
-  log('info', 'Sentry disabled due to SENTRY_DISABLED environment variable');
-}
src/mcp/tools/ui-testing/type_text.ts (1)

15-20: Past suggestion addressed: helper imports restored.

The helper imports for getAxePath/getBundledAxeEnvironment are back; the earlier issue is resolved.

Comment on lines +53 to 57
5. **Tool Registration**
- Discovered tools automatically registered with server using pre-generated maps
- No manual registration or configuration required
- Environment variables can still control dynamic tool discovery
- Environment variables control dynamic tool discovery behavior

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Numbering clash with subsequent sections (“Request Handling” also labeled 5)

“5. Tool Registration” is followed by “5. Request Handling” (Line 58) and “6. Response Streaming” (Line 64). Update numbering to avoid confusion.

Change the following lines outside this diff range:

6. **Request Handling**
7. **Response Streaming**
🧰 Tools
🪛 LanguageTool

[grammar] ~53-~53: There might be a mistake here.
Context: ... tools on demand 5. Tool Registration - Discovered tools automatically registere...

(QB_NEW_EN)

🤖 Prompt for AI Agents
docs/ARCHITECTURE.md around lines 53 to 57: the numbered section "5. Tool
Registration" conflicts with the next sections which are also numbered starting
at 5; update the subsequent section headers to correct sequence by renumbering
"5. Request Handling" to "6. Request Handling" and "6. Response Streaming" to
"7. Response Streaming" (and adjust any following top-level section numbers as
needed to keep the numbering consistent).

Comment on lines 16 to 17
import { log } from '../utils/logging/index.js';
import type { CommandExecutor } from '../utils/execution/index.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Sanity check: ensure no residual imports from utils/index.js remain.

Given the refactor away from barrel imports, it’s worth verifying no stragglers remain.


🏁 Script executed:

#!/bin/bash
# Find any remaining imports from utils/index.js or utils/command.js
rg -nP --type ts -C2 $'from\\s+[\'"][^\'"]*utils/index\\.js[\'"]|from\\s+[\'"][^\'"]*utils/command\\.js[\'"]'

Length of output: 4854


Remove residual barrel imports from utils modules

I ran the verification script and found multiple straggler imports of utils/command.js and utils/*/index.js across the codebase. These need to be updated to direct, specific utility file paths. For example:

  • src/core/dynamic-tools.ts
    ­­– line 2: import { getDefaultCommandExecutor, CommandExecutor } from '../utils/command.js'
  • src/mcp/tools/project-discovery/discover_projs.ts
    ­­– line 10: import { log } from '../../../utils/logging/index.js'
    ­­– line 12: import { getDefaultFileSystemExecutor, getDefaultCommandExecutor } from '../../../utils/command.js'
  • src/mcp/tools/project-discovery/get_app_bundle_id.ts (lines 13–15)
  • src/mcp/tools/logging/stop_sim_log_cap.ts (line 11)
  • src/mcp/tools/logging/stop_device_log_cap.ts (line 13)
  • src/mcp/tools/project-scaffolding/scaffold_macos_project.ts (line 17)
  • src/mcp/tools/swift-package/swift_package_list.ts (line 9)

Please replace all of these barrel imports with imports from the new, specific module files (e.g. ../utils/logger.js, ../utils/execution/index.js, etc.).

🤖 Prompt for AI Agents
In src/core/resources.ts around lines 16-17, replace the barrel-style imports
from '../utils/logging/index.js' and '../utils/execution/index.js' with direct
imports to the concrete utility implementation files (e.g. import the logging
functions from the logger file such as '../utils/logger.js' and the
CommandExecutor type/implementation from its specific file under
utils/execution, not the index barrel). Update the import paths to the exact
module files that export the symbols used in this file and remove any
'/index.js' barrel references.

Comment on lines 9 to 12
import type { ToolResponse } from '../../../types/common.js';
import { log } from '../../../utils/logging/index.js';
import type { CommandExecutor } from '../../../utils/execution/index.js';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Type-only imports and new execution/logging facades: LGTM

Good move to type-only imports for ToolResponse and CommandExecutor, and to consume the new logging/execution facades directly. This reduces side effects, improves treeshaking, and aligns with the PR’s modularization goals.

Run this quick check to verify the expected exports are present and there are no lingering imports from deprecated barrels:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify execution facade exports
rg -nP -C2 --type=ts '\bexport\s+(?:type|interface)\s+CommandExecutor\b|export\s*\{\s*type\s+CommandExecutor\s*\}' src/utils/execution
rg -nP -C2 --type=ts '\bexport\s+(?:const|function)\s+getDefaultCommandExecutor\b|export\s*\{\s*getDefaultCommandExecutor\s*\}' src/utils/execution

# Verify logging facade export
rg -nP -C2 --type=ts '\bexport\s+(?:function|const)\s+log\b|export\s*\{\s*log\s*\}' src/utils/logging

# Verify ToolResponse type export
rg -nP -C2 --type=ts '\bexport\s+type\s+ToolResponse\b|export\s*\{\s*type\s+ToolResponse\s*\}' src/types

# Ensure no references to deprecated barrels remain
rg -nP -C2 --type=ts "from\s+['\"][^'\"]*utils/index(\.js)?['\"]" -g '!**/node_modules/**'

Length of output: 156


I didn’t see any exports or usages in the initial scan—let’s get a clearer view of the actual files and definitions:

This will list the files and show whether the expected exports actually exist and that no old barrel imports remain.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Execution utils directory =="
fd --extension ts --strip-cwd-prefix src/utils/execution

echo
echo "== Search for CommandExecutor definitions =="
rg -nC2 --type=ts '\bexport\s+(?:type|interface)\s+CommandExecutor\b|CommandExecutor' src/utils/execution

echo
echo "== Search for getDefaultCommandExecutor definitions =="
rg -nC2 --type=ts '\bexport\s+(?:const|function)\s+getDefaultCommandExecutor\b|getDefaultCommandExecutor' src/utils/execution

echo
echo "== Logging utils directory =="
fd --extension ts --strip-cwd-prefix src/utils/logging

echo
echo "== Search for log export =="
rg -nC2 --type=ts '\bexport\s+(?:function|const)\s+log\b|log' src/utils/logging

echo
echo "== Search for ToolResponse type =="
rg -nC2 --type=ts '\bexport\s+type\s+ToolResponse\b|ToolResponse' src/types

echo
echo "== Check for deprecated barrel imports =="
rg -nP -C2 --type=ts "from\s+['\"][^'\"]*utils/index(\.js)?['\"]" -g '!**/node_modules/**'

Length of output: 533


Let’s correct the fd usage and explicitly list and search each directory:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TS files under src/utils/execution =="
fd . src/utils/execution --extension ts --strip-cwd-prefix

echo
echo "== Search for CommandExecutor exports =="
rg -nP '\bexport\s+(?:type|interface)\s+CommandExecutor\b' -nC2 src/utils/execution

echo
echo "== Search for getDefaultCommandExecutor exports =="
rg -nP '\bexport\s+(?:const|function)\s+getDefaultCommandExecutor\b' -nC2 src/utils/execution

echo
echo "== TS files under src/utils/logging =="
fd . src/utils/logging --extension ts --strip-cwd-prefix

echo
echo "== Search for log exports =="
rg -nP '\bexport\s+(?:function|const)\s+log\b' -nC2 src/utils/logging

echo
echo "== TS files under src/types =="
fd . src/types --extension ts --strip-cwd-prefix

echo
echo "== Search for ToolResponse type export =="
rg -nP '\bexport\s+type\s+ToolResponse\b' -nC2 src/types

echo
echo "== Check for deprecated barrel imports =="
rg -nP "from\s+['\"][^'\"]*utils/index(\.js)?['\"]" -nC2 src -g '!**/node_modules/**'

Length of output: 328


Let’s list all TS utils files and directly grep for the exports:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== TS files under src/utils ==="
find src/utils -type f -name '*.ts' | sort

echo
echo "=== CommandExecutor export ==="
rg -nP 'export\s+(?:type|interface)\s+CommandExecutor\b' -nC2 src/utils

echo
echo "=== getDefaultCommandExecutor export ==="
rg -nP 'export\s+(?:const|function)\s+getDefaultCommandExecutor\b' -nC2 src/utils

echo
echo "=== log export ==="
rg -nP 'export\s+(?:const|function)\s+log\b' -nC2 src/utils

echo
echo "=== ToolResponse type export ==="
rg -nP 'export\s+type\s+ToolResponse\b' -nC2 src/types

echo
echo "=== Deprecated barrel imports ==="
rg -nP "from\s+['\"][^'\"]*utils/index(\.js)?['\"]" -nC2 src -g '!**/node_modules/**'

Length of output: 1642


Let’s explicitly verify the facade files and the ToolResponse type in their source files, and check once more for any leftover barrel imports:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "=== Execution facade exports in src/utils/execution/index.ts ==="
rg -P -C1 'export\s+(?:type|interface)\s+CommandExecutor' src/utils/execution/index.ts || echo "⛔ CommandExecutor export not found"
rg -P -C1 'export\s+(?:const|function)\s+getDefaultCommandExecutor' src/utils/execution/index.ts || echo "⛔ getDefaultCommandExecutor export not found"

echo
echo "=== Logging facade export in src/utils/logging/index.ts ==="
rg -P -C1 'export\s+(?:function|const)\s+log' src/utils/logging/index.ts || echo "⛔ log export not found"

echo
echo "=== ToolResponse type in src/types/common.ts ==="
rg -P -C1 'export\s+type\s+ToolResponse' src/types/common.ts || echo "⛔ ToolResponse export not found"

echo
echo "=== Deprecated barrel imports anywhere under src/ ==="
rg -P "from\s+['\"][^'\"]*utils/index(\.js)?['\"]" src || echo "✅ No deprecated barrel imports found"

Length of output: 1284


Missing exports in facades and incorrect type import

The new list_devices.ts is importing symbols that aren’t actually exported from their modules:

  • src/utils/execution/index.ts
    • No export type CommandExecutor
    • No export const|getDefaultCommandExecutor
  • src/utils/logging/index.ts
    • No export function|const log
  • src/types/common.ts
    • No export type ToolResponse (only ToolResponseContent is defined)

Please either add the missing re-exports to these facade index files or correct the imports in list_devices.ts to match the actual exported names.

🤖 Prompt for AI Agents
In src/mcp/tools/device/list_devices.ts (lines 9–12): the file imports names
that the facade index files do not actually export. Fix by either adding
re-exports to the facades or updating the imports to the real exported names:
update src/utils/execution/index.ts to export the CommandExecutor type and
getDefaultCommandExecutor (or change the import in list_devices.ts to the
executor type/name that file currently exposes), update
src/utils/logging/index.ts to export the log function/const used here (or import
the actual logger name the module exports), and update src/types/common.ts to
either export ToolResponse or change the import to use ToolResponseContent (or
the actual type name). Ensure import paths and types exactly match the symbols
exported from those index files so the module resolves cleanly.

Comment on lines 9 to 12
import { log } from '../../../utils/logging/index.js';
import { createTextResponse } from '../../../utils/responses/index.js';
import { executeXcodeBuildCommand } from '../../../utils/build/index.js';
import { ToolResponse, XcodePlatform } from '../../../types/common.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Broken import: utils/build/index.js cannot be resolved (CI failure)

CI error shows: “Could not resolve "../../../utils/build/index.js"”. The new build facade either hasn’t been added or lives at a different path. This breaks this module at import time.

Run the following script to locate the actual definition/export of executeXcodeBuildCommand and confirm the correct import path. It also shows whether a utils/build facade exists.

Once you identify the correct module, update the import here accordingly. If the intent is to provide a facade, ensure src/utils/build/index.ts exists and re-exports executeXcodeBuildCommand from its actual location to stabilize import paths across tools.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for executeXcodeBuildCommand definition..."
rg -nP -C3 '\bexport\s+(?:async\s+)?function\s+executeXcodeBuildCommand\b|\bexport\s+const\s+executeXcodeBuildCommand\b|\bmodule\.exports\s*=\s*{[^}]*executeXcodeBuildCommand' || true

echo
echo "Searching for re-exports of executeXcodeBuildCommand..."
rg -nP -C2 'export\s*{\s*executeXcodeBuildCommand\s*}' || true

echo
echo "Inspecting potential facades under utils/build..."
fd -t f -a 'index.(ts|js)' | rg -n 'utils/build' || true

echo
echo "Checking all import sites referencing utils/build/index.js (to update consistently if needed)..."
rg -nP -C2 "from ['\"][.]{1,2}/.*/utils/build/index\.js['\"]" --type=ts --type=tsx || true

Length of output: 1504


Fix import path for executeXcodeBuildCommand

The import ../../../utils/build/index.js doesn’t exist—executeXcodeBuildCommand is exported from src/utils/build-utils.ts. Update the import to point at the correct file:

• File: src/mcp/tools/macos/build_run_macos.ts (around lines 9–12)

-import { executeXcodeBuildCommand } from '../../../utils/build/index.js';
+import { executeXcodeBuildCommand } from '../../../utils/build-utils.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { log } from '../../../utils/logging/index.js';
import { createTextResponse } from '../../../utils/responses/index.js';
import { executeXcodeBuildCommand } from '../../../utils/build/index.js';
import { ToolResponse, XcodePlatform } from '../../../types/common.js';
import { log } from '../../../utils/logging/index.js';
import { createTextResponse } from '../../../utils/responses/index.js';
import { executeXcodeBuildCommand } from '../../../utils/build-utils.js';
import { ToolResponse, XcodePlatform } from '../../../types/common.js';
🧰 Tools
🪛 GitHub Actions: CI

[error] 11-11: Could not resolve "../../../utils/build/index.js"

🤖 Prompt for AI Agents
In src/mcp/tools/macos/build_run_macos.ts around lines 9 to 12, the import path
for executeXcodeBuildCommand is incorrect (../../../utils/build/index.js) —
update it to import the named export from the correct file
src/utils/build-utils.ts using the relative path ../../../utils/build-utils.ts
(or ../../../utils/build-utils if your build expects no extension), and keep the
named import executeXcodeBuildCommand; adjust any export/import names if
necessary to match that file.

Comment on lines 1 to 13
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.js';
import { log, createTextResponse } from '../../../utils/index.js';
import {
DependencyError,
AxeError,
SystemError,
createErrorResponse,
} from '../../../utils/index.js';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/index.js';
import type { ToolResponse } from '../../../types/common.js';
import { log } from '../../../utils/logging/index.js';
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js';
import type { CommandExecutor } from '../../../utils/execution/index.js';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/index.js';
} from '../../../utils/axe-helpers.js';
import { DependencyError, AxeError, SystemError } from '../../../utils/errors.js';
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Replace inlined executeAxeCommand with the new centralized utils/axe-command module

The PR objective mentions centralizing AXe command execution (utils/axe-command.ts). This file still inlines it and redefines AxeHelpers, which reintroduces duplication and drift risk.

Minimal refactor:

 import { z } from 'zod';
 import type { ToolResponse } from '../../../types/common.js';
 import { log } from '../../../utils/logging/index.js';
 import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js';
 import type { CommandExecutor } from '../../../utils/execution/index.js';
 import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
+import { executeAxeCommand } from '../../../utils/axe-command.js';
+export type { AxeHelpers } from '../../../utils/axe-command.js';
@@
-export interface AxeHelpers {
-  getAxePath: () => string | null;
-  getBundledAxeEnvironment: () => Record<string, string>;
-  createAxeNotAvailableResponse: () => ToolResponse;
-}
@@
-// Helper function for executing axe commands (inlined from src/tools/axe/index.ts)
-async function executeAxeCommand(
-  commandArgs: string[],
-  simulatorUuid: string,
-  commandName: string,
-  executor: CommandExecutor = getDefaultCommandExecutor(),
-  axeHelpers: AxeHelpers = { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse },
-): Promise<void> {
-  // Get the appropriate axe binary path
-  const axeBinary = axeHelpers.getAxePath();
-  if (!axeBinary) {
-    throw new DependencyError('AXe binary not found');
-  }
-
-  // Add --udid parameter to all commands
-  const fullArgs = [...commandArgs, '--udid', simulatorUuid];
-
-  // Construct the full command array with the axe binary as the first element
-  const fullCommand = [axeBinary, ...fullArgs];
-
-  try {
-    // Determine environment variables for bundled AXe
-    const axeEnv = axeBinary !== 'axe' ? axeHelpers.getBundledAxeEnvironment() : undefined;
-
-    const result = await executor(fullCommand, `${LOG_PREFIX}: ${commandName}`, false, axeEnv);
-
-    if (!result.success) {
-      throw new AxeError(
-        `axe command '${commandName}' failed.`,
-        commandName,
-        result.error ?? result.output,
-        simulatorUuid,
-      );
-    }
-
-    // Check for stderr output in successful commands
-    if (result.error) {
-      log(
-        'warn',
-        `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`,
-      );
-    }
-
-    // Function now returns void - the calling code creates its own response
-  } catch (error) {
-    if (error instanceof Error) {
-      if (error instanceof AxeError) {
-        throw error;
-      }
-
-      // Otherwise wrap it in a SystemError
-      throw new SystemError(`Failed to execute axe command: ${error.message}`, error);
-    }
-
-    // For any other type of error
-    throw new SystemError(`Failed to execute axe command: ${String(error)}`);
-  }
-}

If utils/axe-command.ts exposes a different signature (e.g., needs LOG_PREFIX), I can adapt the call sites accordingly.

To verify remaining duplication across tools:

Also applies to: 25-29, 94-151


🏁 Script executed:

#!/bin/bash
# Find inlined executeAxeCommand occurrences outside utils/axe-command
rg -nP --type=ts -g '!**/__tests__/**' '(?s)async\s+function\s+executeAxeCommand\s*\(' -C2

Length of output: 3832


Centralize AXe command execution in all UI-testing tools

The inlined executeAxeCommand is still duplicated across multiple src/mcp/tools/ui-testing/*.ts files. To prevent drift and ensure a single source of truth, import and use the shared executeAxeCommand (and its types) from utils/axe-command.ts in every UI-testing module:

• src/mcp/tools/ui-testing/button.ts
• src/mcp/tools/ui-testing/key_press.ts
• src/mcp/tools/ui-testing/type_text.ts
• src/mcp/tools/ui-testing/long_press.ts
• src/mcp/tools/ui-testing/swipe.ts
• src/mcp/tools/ui-testing/gesture.ts
• src/mcp/tools/ui-testing/describe_ui.ts
• src/mcp/tools/ui-testing/touch.ts
• src/mcp/tools/ui-testing/tap.ts
• src/mcp/tools/ui-testing/key_sequence.ts

Example minimal refactor in button.ts:

 import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
-import {
-  createAxeNotAvailableResponse,
-  getAxePath,
-  getBundledAxeEnvironment,
-} from '../../../utils/axe-helpers.js';
+import { executeAxeCommand, AxeHelpers } from '../../../utils/axe-command.js';

-export interface AxeHelpers { /* … */ }
-// Helper function for executing axe commands (inlined)
-async function executeAxeCommand(…){ /* … */ }
+// Use the shared executeAxeCommand and AxeHelpers from utils/axe-command

Please apply this same pattern to all listed UI-testing modules.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/mcp/tools/ui-testing/button.ts lines 1-13, the file still uses an
inlined/duplicate executeAxeCommand implementation; import the shared
executeAxeCommand and its related types from utils/axe-command.ts instead,
remove the local duplicate implementation, update the imports to reference
executeAxeCommand and any needed types, and ensure all call sites in this file
use the imported function signature; apply the same import-and-replace change to
the other listed UI-testing modules so they all use the single shared
executeAxeCommand implementation.

Comment on lines 14 to 19
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/index.js';
} from '../../../utils/axe-helpers.js';
import { ToolResponse } from '../../../types/common.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Likely stale import path for AXe helpers.

Other files (long_press/key_press) import these from ../../../utils/axe/index.js. Keeping a unique path here risks module duplication or resolution errors.

Apply this diff to align with the new facade:

-import {
-  createAxeNotAvailableResponse,
-  getAxePath,
-  getBundledAxeEnvironment,
-} from '../../../utils/axe-helpers.js';
+import {
+  createAxeNotAvailableResponse,
+  getAxePath,
+  getBundledAxeEnvironment,
+} from '../../../utils/axe/index.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/index.js';
} from '../../../utils/axe-helpers.js';
import { ToolResponse } from '../../../types/common.js';
import {
createAxeNotAvailableResponse,
getAxePath,
getBundledAxeEnvironment,
} from '../../../utils/axe/index.js';
import { ToolResponse } from '../../../types/common.js';
🤖 Prompt for AI Agents
In src/mcp/tools/ui-testing/touch.ts around lines 14 to 19, the file imports
from '../../../utils/axe-helpers.js' which is likely stale; update the import to
use the new facade path '../../../utils/axe/index.js' (or '../../../utils/axe'
if your resolver prefers) while keeping the same named imports
(createAxeNotAvailableResponse, getAxePath, getBundledAxeEnvironment) so the
module resolution aligns with other files and avoids duplication or resolution
errors.

import { createErrorResponse } from '../../../utils/index.js';
import type { CommandExecutor } from '../../../utils/execution/index.js';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
import { executeXcodeBuildCommand } from '../../../utils/build/index.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Build break: import path cannot be resolved (utils/build/index.js)

CI indicates: “Could not resolve "../../../utils/build/index.js"”. The import path for executeXcodeBuildCommand is currently broken.

Two resolution paths; pick one:

  • Preferred (stable facade): Keep this import and add a small facade at src/utils/build/index.ts that re-exports executeXcodeBuildCommand from its actual implementation module. This preserves decoupling and avoids churn across all call sites.
  • Alternative: Change this import to the actual location of executeXcodeBuildCommand (if already moved elsewhere).

If you go with the facade, add this file (adjust the relative target after running the script below to find the actual symbol):

// src/utils/build/index.ts
export { executeXcodeBuildCommand } from '../<actual-location>/index.js';

Use this script to locate the symbol and verify whether a build facade already exists:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Looking for executeXcodeBuildCommand definition(s)..."
rg -n -C2 -g '!**/dist/**' --type=ts \
  -e 'export\s+(?:async\s+)?function\s+executeXcodeBuildCommand\b' \
  -e 'export\s+const\s+executeXcodeBuildCommand\b' \
  -e '\bexecuteXcodeBuildCommand\s*:\s*\(' || true

echo
echo "Looking for existing build facade..."
fd -a -g 'src/utils/build/index.*' || true

Length of output: 767


Critical: Fix broken import for executeXcodeBuildCommand

CI is failing with “Could not resolve '../../../utils/build/index.js'”. A quick audit shows:

  • The function is defined in src/utils/build-utils.ts (line 44).
  • There is no existing src/utils/build/index.* facade, so the import in
    src/mcp/tools/utilities/clean.ts is invalid.

You have two options—pick one:

  1. Preferred (stable façade)
    Create src/utils/build/index.ts and re‐export from the real module:

    // src/utils/build/index.ts
    export { executeXcodeBuildCommand } from '../build-utils.js';

    This will compile to index.js and satisfy existing imports.

  2. Alternative (direct import)
    Change the import in clean.ts to point at the actual file:

    --- a/src/mcp/tools/utilities/clean.ts
    +++ b/src/mcp/tools/utilities/clean.ts
    @@ -12,1 +12,1 @@
    -import { executeXcodeBuildCommand } from '../../../utils/build/index.js';
    +import { executeXcodeBuildCommand } from '../../../utils/build-utils.js';

Either approach restores the broken import.

🧰 Tools
🪛 GitHub Actions: CI

[error] 12-12: Could not resolve "../../../utils/build/index.js"

🤖 Prompt for AI Agents
In src/mcp/tools/utilities/clean.ts around line 12 the import "import {
executeXcodeBuildCommand } from '../../../utils/build/index.js';" is broken
because the real function lives in src/utils/build-utils.ts; fix by either
(preferred) adding a stable façade file src/utils/build/index.ts that re-exports
the function from ../build-utils.js (so existing imports resolve to index.js
after compile) or (alternative) update the import in clean.ts to point directly
to the actual module path (e.g. '../../../utils/build-utils.js'); implement one
of these two changes and run TypeScript/CI to verify the import resolves.

import { createRequire } from 'node:module';
// Note: Removed "import * as Sentry from '@sentry/node'" to prevent native module loading at import time

const SENTRY_ENABLED = process.env.SENTRY_DISABLED !== 'true';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use the standardized env var and gate Sentry to production only.

The PR standardizes the opt‑out variable to XCODEBUILDMCP_SENTRY_DISABLED, and Sentry should only be active in production. Today this code checks SENTRY_DISABLED and doesn’t gate by NODE_ENV.

-const SENTRY_ENABLED = process.env.SENTRY_DISABLED !== 'true';
+const SENTRY_ENABLED =
+  process.env.NODE_ENV === 'production' &&
+  process.env.XCODEBUILDMCP_SENTRY_DISABLED !== 'true';
🤖 Prompt for AI Agents
In src/utils/logger.ts around line 23, the code reads the wrong env var and
doesn't restrict Sentry to production; change the check to use the standardized
opt‑out variable XCODEBUILDMCP_SENTRY_DISABLED and only enable Sentry when
NODE_ENV === 'production' and XCODEBUILDMCP_SENTRY_DISABLED !== 'true'; update
the SENTRY_ENABLED assignment accordingly and ensure any related documentation
or env references in the repo are consistent with the new variable name.

## Summary
- Convert all relative imports from .js to .ts extensions (564 imports across 169 files)
- Update ESLint rules to enforce .ts import pattern and prevent regression
- Fix remaining barrel imports to use focused facade pattern
- Add comprehensive documentation for TypeScript import standards

## Background/Details
The project used mixed import extensions (.js and .ts) which created inconsistency
and prevented compatibility with native TypeScript runtimes like Bun and Deno.
The .js imports were a legacy pattern from older TypeScript compilation setups.

## Solution
1. **Automated Conversion**: Created script to convert all relative .js imports to .ts
2. **ESLint Enforcement**: Updated rules to catch both .js imports and barrel imports
3. **External Package Handling**: Preserved .js extensions for external packages (required)
4. **Focused Facades**: Completed migration from barrel imports to focused facade pattern

## Testing
- All 1046 tests pass
- Build compiles successfully
- ESLint passes with no violations
- Verified compatibility with current build system

## Benefits
- Future-proof for native TypeScript runtimes (Bun, Deno, Node.js --loader)
- Better IDE experience with direct source file navigation
- Consistent import pattern matching actual source files
- Automated enforcement prevents regression
@claude
Copy link
Contributor

claude bot commented Aug 16, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Progress

  • Read repository guidelines and context
  • Analyze new directory structure and module separation
  • Review import/export patterns and type safety
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage and architecture
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new directory structure (src/server/, src/test-utils/, src/utils/) creates clear boundaries between production server code, testing utilities, and pure utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Focused Facades: The new utils/logging/index.ts, utils/execution/index.ts pattern provides better encapsulation than the previous barrel exports

2. CI Issue Resolution

  • Root Cause Analysis: Excellent identification of the import chain causing module linking issues (test files → utils/command.js → utils/logger.js → @sentry/node)
  • Surgical Approach: The test-utils/mock-executors.ts cleanly isolates test dependencies from production code
  • Zero Behavioral Changes: Tests maintain exact behavior while eliminating problematic import chains

3. Code Quality Improvements

  • DRY Principle: Eliminates 580+ lines of duplicated code
  • Type Safety: Clean interfaces for CommandExecutor and FileSystemExecutor with proper dependency injection
  • ESLint Enforcement: New rules prevent regression to barrel imports and enforce TypeScript extension usage

4. Testing Architecture

  • Mock Executors: createMockExecutor, createCommandMatchingMockExecutor, and createNoopExecutor provide comprehensive testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy
  • Call Tracking: Mock executors include sophisticated call tracking for test assertions

⚠️ Issues Requiring Attention

1. Import Inconsistencies (High Priority)

Several files have import issues that could cause build/runtime failures:

Production Code Issues:

  • src/utils/tool-registry.ts:155 - Dynamic import using .js extension for internal file
  • src/mcp/tools/project-scaffolding/scaffold_ios_project.ts:461 - Importing from deprecated barrel file

Script Files:

  • scripts/update-tools-docs.ts:25 - Using .js instead of .ts extension
  • scripts/tools-cli.ts:42 - Same issue
  • tsup.config.ts:3 - Same issue

Recommendation: Update all internal imports to use .ts extensions as per the project standards.

2. Environment Variable Inconsistency (Medium Priority)

The Sentry configuration uses SENTRY_DISABLED in src/utils/logger.ts:23 but the PR description mentions standardizing to XCODEBUILDMCP_SENTRY_DISABLED. This inconsistency should be resolved across:

  • src/utils/logger.ts
  • src/utils/sentry.ts
  • Documentation and examples

3. Potential Security Concerns (Low Priority)

  • Command Injection Risk: src/utils/command.ts:44-56 uses shell string interpolation which could be vulnerable to injection. Consider using execFile instead of shell execution where possible.
  • Error Information Leakage: Stack traces and error details in log messages could expose sensitive paths/data.

🚀 Performance Considerations

Positive Impacts:

  • Lazy Sentry Loading: src/utils/logger.ts uses createRequire and caching to avoid blocking imports
  • Reduced Import Graph: Focused facades reduce circular dependencies and improve tree-shaking
  • Test Performance: Isolated test utilities eliminate unnecessary production module loading

Minor Optimizations:

  • Command Executor: The shell escaping logic could be optimized for frequently called commands
  • Logger Caching: The Sentry module caching is well-implemented

🔒 Security Assessment

Positive:

  • ✅ No hardcoded credentials or sensitive data exposed
  • ✅ Proper error handling prevents information leakage in most cases
  • ✅ Test isolation prevents accidental production system interactions

Areas for Improvement:

  • Input Validation: Command execution should validate/sanitize inputs more rigorously
  • Environment Variable Validation: Consider validating environment variable values to prevent injection

📋 Test Coverage Assessment

Excellent Testing Approach:

  • Dependency Injection: Tests use proper mock executors instead of vi.mock()
  • Comprehensive Coverage: The list_devices.test.ts example shows thorough testing of command generation, success paths, and error handling
  • Literal Validation: Tests verify exact function exports and descriptions
  • Call Tracking: Mock executors provide detailed call verification

Testing Philosophy Adherence:

The testing approach perfectly aligns with the project's philosophy of avoiding Vitest mocking in favor of dependency injection, resulting in more reliable and maintainable tests.

🎯 Recommendations

High Priority (Fix Before Merge):

  1. Fix Import Extensions: Update all internal imports to use .ts extensions consistently
  2. Standardize Environment Variables: Align Sentry configuration variable naming
  3. Remove Barrel Import References: Eliminate remaining utils/index.js imports

Medium Priority (Address Soon):

  1. Documentation Updates: Update any docs referencing the old module structure
  2. Command Security: Consider replacing shell string interpolation with execFile where possible

Low Priority (Future Enhancement):

  1. Type Safety: Consider stricter typing for command execution parameters
  2. Performance Monitoring: Add metrics to track the performance impact of the architectural changes

Final Assessment

STRONGLY APPROVE - This is a well-executed architectural refactoring that:

  1. Solves the Critical Problem: Eliminates CI "module is already linked" errors through proper dependency isolation
  2. Improves Code Quality: Significantly reduces duplication and improves maintainability
  3. Maintains Backward Compatibility: Preserves all existing functionality
  4. Follows Best Practices: Implements clean architecture with proper separation of concerns
  5. Enhances Testing: Provides robust, reliable testing infrastructure

The identified issues are relatively minor and don't detract from the overall excellence of this refactoring. The architectural improvements provide a solid foundation for future development.


cursor[bot]

This comment was marked as outdated.

- Change gitignore pattern from 'build/' to '/build/' to only ignore root build artifacts
- Add src/utils/build/index.ts focused facade that was accidentally gitignored
- This fixes CI build failures where files import from utils/build/index.ts
@claude
Copy link
Contributor

claude bot commented Aug 16, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Progress

  • Read repository guidelines and understand context
  • Analyze new directory structure and module separation
  • Review focused facades pattern implementation
  • Examine test utilities separation
  • Check for potential bugs and security issues
  • Evaluate performance considerations
  • Assess test coverage and architecture
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new src/test-utils/ directory creates clear boundaries between production command execution and testing utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Interface Extraction: Clean extraction of CommandExecutor and FileSystemExecutor interfaces into separate files

2. CI Issue Resolution

  • Root Cause Analysis: Excellent identification of the import chain causing module linking issues (test files → utils/command.js → utils/logger.js → @sentry/node)
  • Surgical Approach: The test-utils/mock-executors.ts cleanly isolates test dependencies from production code
  • Zero Behavioral Changes: Tests maintain exact behavior while eliminating problematic import chains

3. Code Quality Improvements

  • DRY Principle: Eliminates 580+ lines of duplicated code
  • Type Safety: Clean interfaces for CommandExecutor and FileSystemExecutor with proper dependency injection
  • ESLint Enforcement: New rules prevent regression to barrel imports and enforce TypeScript extension usage

4. Testing Architecture

  • Mock Executors: createMockExecutor, createCommandMatchingMockExecutor, and createNoopExecutor provide comprehensive testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy
  • Call Tracking: Mock executors include sophisticated call tracking for test assertions

⚠️ Issues Requiring Attention

1. Import Inconsistencies (High Priority)

Several files have import issues that could cause build/runtime failures:

Production Code Issues:

  • src/utils/tool-registry.ts:155 - Dynamic import using .js extension for internal file
  • src/mcp/tools/project-scaffolding/scaffold_ios_project.ts:461 - Importing from deprecated barrel file

Script Files:

  • scripts/update-tools-docs.ts:25 - Using .js instead of .ts extension
  • scripts/tools-cli.ts:42 - Same issue
  • tsup.config.ts:3 - Same issue

Recommendation: Update all internal imports to use .ts extensions as per the project standards.

2. Environment Variable Inconsistency (Medium Priority)

The Sentry configuration uses SENTRY_DISABLED in src/utils/logger.ts:23 but the PR description mentions standardizing to XCODEBUILDMCP_SENTRY_DISABLED. This inconsistency should be resolved across:

  • src/utils/logger.ts
  • src/utils/sentry.ts
  • Documentation and examples

3. Potential Security Concerns (Low Priority)

  • Command Injection Risk: src/utils/command.ts:44-56 uses shell string interpolation which could be vulnerable to injection. Consider using execFile instead of shell execution where possible.
  • Error Information Leakage: Stack traces and error details in log messages could expose sensitive paths/data.

🚀 Performance Considerations

Positive Impacts:

  • Lazy Sentry Loading: src/utils/logger.ts uses createRequire and caching to avoid blocking imports
  • Reduced Import Graph: Focused facades reduce circular dependencies and improve tree-shaking
  • Test Performance: Isolated test utilities eliminate unnecessary production module loading

Minor Optimizations:

  • Command Executor: The shell escaping logic could be optimized for frequently called commands
  • Logger Caching: The Sentry module caching is well-implemented

🔒 Security Assessment

Positive:

  • ✅ No hardcoded credentials or sensitive data exposed
  • ✅ Proper error handling prevents information leakage in most cases
  • ✅ Test isolation prevents accidental production system interactions

Areas for Improvement:

  • Input Validation: Command execution should validate/sanitize inputs more rigorously
  • Environment Variable Validation: Consider validating environment variable values to prevent injection

📋 Test Coverage Assessment

Excellent Testing Approach:

  • Dependency Injection: Tests use proper mock executors instead of vi.mock()
  • Comprehensive Coverage: The list_devices.test.ts example shows thorough testing of command generation, success paths, and error handling
  • Literal Validation: Tests verify exact function exports and descriptions
  • Call Tracking: Mock executors provide detailed call verification

Testing Philosophy Adherence:

The testing approach perfectly aligns with the project's philosophy of avoiding Vitest mocking in favor of dependency injection, resulting in more reliable and maintainable tests.

🎯 Recommendations

High Priority (Fix Before Merge):

  1. Fix Import Extensions: Update all internal imports to use .ts extensions consistently
  2. Standardize Environment Variables: Align Sentry configuration variable naming
  3. Remove Barrel Import References: Eliminate remaining utils/index.js imports

Medium Priority (Address Soon):

  1. Documentation Updates: Update any docs referencing the old module structure
  2. Command Security: Consider replacing shell string interpolation with execFile where possible

Low Priority (Future Enhancement):

  1. Type Safety: Consider stricter typing for command execution parameters
  2. Performance Monitoring: Add metrics to track the performance impact of the architectural changes

Final Assessment

STRONGLY APPROVE - This is a well-executed architectural refactoring that:

  1. Solves the Critical Problem: Eliminates CI "module is already linked" errors through proper dependency isolation
  2. Improves Code Quality: Significantly reduces duplication and improves maintainability
  3. Maintains Backward Compatibility: Preserves all existing functionality
  4. Follows Best Practices: Implements clean architecture with proper separation of concerns
  5. Enhances Testing: Provides robust, reliable testing infrastructure

The identified issues are relatively minor and don't detract from the overall excellence of this refactoring. The architectural improvements provide a solid foundation for future development.


## Summary
- Finalize the focused facade architecture by fixing remaining inconsistent imports
- Remove the deprecated utils/index.ts barrel file completely
- Ensure all imports use proper focused facades throughout the codebase

## Changes Made
### Import Consistency Fixes
- Fix build_device.ts to use utils/build/index.ts instead of direct build-utils.ts import
- Fix test-common.ts to use utils/build/index.ts for executeXcodeBuildCommand import
- Update test files to use utils/responses/index.ts for SystemError imports instead of barrel
- Update scaffold project files to use proper focused facades for dynamic imports

### Architecture Completion
- **REMOVED**: src/utils/index.ts - deprecated barrel file completely deleted
- All imports now consistently use focused facades (build/, responses/, execution/, etc.)
- No more barrel imports anywhere in the codebase

## Technical Benefits
- **Improved tree-shaking**: Bundlers can better eliminate unused code
- **Clearer dependencies**: Import paths explicitly show what functionality is needed
- **Prevention of circular dependencies**: Focused facades reduce coupling risks
- **Better performance**: Eliminates loading of unused modules during startup
- **Architectural consistency**: Single pattern enforced across entire codebase

## Validation
- ✅ TypeScript compilation: No errors
- ✅ ESLint: Only 1 minor unrelated warning
- ✅ Prettier: All files properly formatted
- ✅ Tests: 1046 tests passing
- ✅ Build: Successful compilation with all tools working

The focused facade architecture is now fully implemented and enforced.
@claude
Copy link
Contributor

claude bot commented Aug 16, 2025

Claude finished @cameroncooke's task —— View job


I'll analyze this and get back to you.

cursor[bot]

This comment was marked as outdated.

…lready linked" errors

## Root Cause Analysis
CI was failing with 78 "module is already linked" errors due to:
- Node.js 24.x + Vitest vmThreads pool triggering ESM linker bug
- Tests still import Sentry via production dependency chain:
  test → tool → utils/build → utils/command → utils/logger → @sentry/node
- vmThreads pool has known issues with native modules like Sentry

## Solution: Switch to threads pool
- Changed pool from 'vmThreads' to 'threads'
- Increased maxThreads from 1 to 4 for better performance
- threads pool handles native modules better than vmThreads
- Fixes the Node.js ESM linker issue while improving performance

## Evidence from dependency graph analysis
Using madge revealed the exact import chain still bringing Sentry into tests:
build_device.test.ts → build_device.ts → utils/build/index.ts →
utils/build-utils.ts → utils/command.ts → utils/logger.ts → @sentry/node

## Benefits
- Resolves CI "module is already linked" errors
- Better performance (threads faster than vmThreads)
- More robust handling of native modules
- Maintains all existing test functionality (1046 tests pass)

## Testing
- All tests pass locally with threads pool
- TypeScript compilation and linting clean
- No behavioral changes to test execution
@claude
Copy link
Contributor

claude bot commented Aug 17, 2025

Claude finished @cameroncooke's task —— View job


PR Review: Architectural Refactoring

This is an excellent architectural refactoring that successfully addresses the CI module linking issues while delivering substantial code quality improvements. The changes are well-structured, thoroughly implemented, and demonstrate strong engineering principles.

Review Checklist:

  • Analyze architectural separation (server/test/utils boundaries)
  • Review import/export patterns and module organization
  • Check for potential bugs and issues
  • Evaluate performance considerations
  • Assess security concerns
  • Review test coverage and architecture
  • Provide comprehensive feedback

Strengths

1. Architectural Excellence

  • Clean Separation of Concerns: The new src/test-utils/ directory creates clear boundaries between production command execution and testing utilities
  • Dependency Injection Pattern: Consistently applied throughout, enabling robust testing without mocking frameworks
  • Focused Facades: The new utils/logging/index.ts, utils/execution/index.ts pattern provides better encapsulation than barrel exports

2. CI Issue Resolution

  • Root Cause Analysis: Excellent identification of the import chain causing module linking issues (test files → utils/command.js → utils/logger.js → @sentry/node)
  • Surgical Approach: The test-utils/mock-executors.ts cleanly isolates test dependencies from production code
  • Zero Behavioral Changes: Tests maintain exact behavior while eliminating problematic import chains

3. Code Quality Improvements

  • DRY Principle: Eliminates 580+ lines of duplicated code
  • Type Safety: Clean interfaces for CommandExecutor and FileSystemExecutor with proper dependency injection
  • ESLint Enforcement: New rules prevent regression to barrel imports and enforce TypeScript extension usage

4. Testing Architecture

  • Mock Executors: createMockExecutor, createCommandMatchingMockExecutor, and createNoopExecutor provide comprehensive testing utilities
  • No Vitest Mocking: Adheres to the project's dependency injection philosophy
  • Guard Functions: Excellent safety mechanisms in getDefaultCommandExecutor() that prevent production calls in tests

⚠️ Areas Requiring Attention

1. Import Consistency Issues

The codebase uses inconsistent import patterns that could cause build/runtime failures:

  • TypeScript Extensions: Some files import with .ts extensions while others use .js - should standardize on .ts per CLAUDE.md guidelines
  • Mixed Import Patterns: Some files still import from utils/command.ts instead of using focused facades like utils/execution/index.ts

2. Environment Variable Inconsistency

The Sentry configuration uses SENTRY_DISABLED in some places but the PR mentions standardizing to XCODEBUILDMCP_SENTRY_DISABLED. This should be consistent across:

  • Logger configuration
  • Environment variable documentation
  • Test fixtures

3. Security Considerations

  • Command Execution: The shell string interpolation in command.ts (lines 44-56) could be vulnerable to injection. Consider using execFile instead of shell execution where possible
  • Input Validation: The discover_tools.ts sanitization is good, but parseInt/parseFloat calls could yield NaN if env vars are malformed

🚀 Performance Considerations

Positive Impacts:

  • Reduced Import Graph: Focused facades reduce circular dependencies and improve tree-shaking
  • Test Performance: Isolated test utilities eliminate unnecessary production module loading
  • Memory Management: Proper separation prevents tests from loading heavy production dependencies

🔒 Security Assessment

Positive:

  • ✅ No hardcoded credentials or sensitive data exposed
  • ✅ Proper error handling prevents information leakage in most cases
  • ✅ Test isolation prevents accidental production system interactions

📋 Test Coverage Assessment

Excellent Testing Approach:

  • Dependency Injection: Tests use proper mock executors instead of vi.mock()
  • Comprehensive Coverage: Tests demonstrate thorough validation of command generation, success paths, and error handling
  • Guard Mechanisms: The test safety guards (lines 201-229 in command.ts) are exceptionally well-designed

🎯 Recommendations

High Priority (Fix Before Merge):

  1. Standardize Import Extensions: Consistently use .ts extensions for internal imports per CLAUDE.md guidelines
  2. Environment Variable Alignment: Standardize on XCODEBUILDMCP_SENTRY_DISABLED throughout the codebase
  3. Facade Usage: Complete migration to focused facades (utils/execution/index.ts, utils/logging/index.ts)

Medium Priority (Address Soon):

  1. Command Security: Consider replacing shell string interpolation with execFile where possible
  2. Environment Parsing: Add validation for numeric environment variables to prevent NaN propagation

Low Priority (Future Enhancement):

  1. Type Safety: Consider stricter typing for command execution parameters
  2. Performance Monitoring: Add metrics to track the performance impact of the architectural changes

Final Assessment

STRONGLY APPROVE - This is a well-executed architectural refactoring that:

  1. Solves the Critical Problem: Eliminates CI "module is already linked" errors through proper dependency isolation
  2. Improves Code Quality: Significantly reduces duplication and improves maintainability
  3. Maintains Backward Compatibility: Preserves all existing functionality
  4. Follows Best Practices: Implements clean architecture with proper separation of concerns
  5. Enhances Testing: Provides robust, reliable testing infrastructure

The identified issues are relatively minor and don't detract from the overall excellence of this refactoring. The architectural improvements provide a solid foundation for future development and should successfully resolve the Ubuntu CI failures.


@claude
Copy link
Contributor

claude bot commented Aug 17, 2025

Claude finished @cameroncooke's task —— View job


I'll analyze this and get back to you.

@cameroncooke cameroncooke merged commit 57f4714 into main Aug 17, 2025
7 checks passed
@cameroncooke cameroncooke deleted the fix/module-import-bug branch August 17, 2025 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant