diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b352b3ed..2a55b6c7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,7 @@ body: attributes: value: | Thanks for taking the time to report an issue with XcodeBuildMCP! - + - type: textarea id: description attributes: @@ -21,20 +21,23 @@ body: id: debug attributes: label: Debug Output - description: Output from the diagnostic tool, run `mise x npm:xcodebuildmcp@1.3.5 -- xcodebuildmcp-diagnostic` in your terminal (replacing the @ version with the version number you're using) and paste the output here + description: Ask your agent "Run the XcodeBuildMCP `doctor` tool and return the output as markdown verbatim" and then copy paste it here. placeholder: | ``` - Running XcodeBuildMCP Diagnostic Tool (v1.3.5)... - Collecting system information and checking dependencies... - - [2025-05-07T10:06:22.737Z] [INFO] [Diagnostic]: Running diagnostic tool - # XcodeBuildMCP Diagnostic Report - + XcodeBuildMCP Doctor + + Generated: 2025-08-11T17:42:29.812Z + Server Version: 1.11.2 + + ## System Information + - platform: darwin + - release: 25.0.0 + - arch: arm64 ... - ``` + ``` validations: - required: true - + required: true + - type: input id: editor-client attributes: @@ -43,7 +46,7 @@ body: placeholder: Cursor 0.49.1 validations: required: true - + - type: input id: mcp-server-version attributes: @@ -52,7 +55,7 @@ body: placeholder: 1.2.2 validations: required: true - + - type: input id: llm attributes: @@ -61,7 +64,7 @@ body: placeholder: Claude 3.5 Sonnet validations: required: true - + - type: textarea id: mcp-config attributes: @@ -76,7 +79,7 @@ body: } ``` render: json - + - type: textarea id: steps attributes: @@ -88,7 +91,7 @@ body: 3. What failed or didn't work as expected validations: required: true - + - type: textarea id: expected attributes: @@ -97,7 +100,7 @@ body: placeholder: The AI should have been able to... validations: required: true - + - type: textarea id: actual attributes: @@ -106,7 +109,7 @@ body: placeholder: Instead, the AI... validations: required: true - + - type: textarea id: error attributes: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 549daed1..324c8901 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -50,7 +50,7 @@ jobs: assignee_trigger: "claude-bot" # Optional: Allow Claude to run specific commands - allowed_tools: "Bash(npm install),Bash(npm run build:*),Bash(npm run test:*),Bash(npm run lint:*),Bash(npm run format:*),Bash(npm run diagnostic)" + allowed_tools: "Bash(npm install),Bash(npm run build:*),Bash(npm run test:*),Bash(npm run lint:*),Bash(npm run format:*),Bash(npm run doctor)" # Optional: Add custom instructions for Claude to customize its behavior for your project # custom_instructions: | diff --git a/.vscode/settings.json b/.vscode/settings.json index d6bc5c87..6ac0b034 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,4 +36,9 @@ "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, + "chat.mcp.serverSampling": { + "XcodeBuildMCP/.vscode/mcp.json: XcodeBuildMCP-Dev": { + "allowedDuringChat": true + } + }, } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 04c5fc04..c89555ba 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -98,7 +98,7 @@ } }, { - "label": "build: dev diagnostics", + "label": "build: dev doctor", "dependsOn": [ "lint", "typecheck (watch)" diff --git a/AGENTS.md b/AGENTS.md index 55db868c..db467ecc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ npm run format:check # Prettier code checking npm run format # Prettier code formatting npm run typecheck # TypeScript type checking npm run inspect # Run interactive MCP protocol inspector -npm run diagnostic # Diagnostic CLI +npm run doctor # Doctor CLI ``` ### Development with Reloaderoo diff --git a/README.md b/README.md index 3add22b1..92b8bb0a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ A Model Context Protocol (MCP) server that provides Xcode-related tools for inte - [Client Compatibility](#client-compatibility) - [Code Signing for Device Deployment](#code-signing-for-device-deployment) - [Troubleshooting](#troubleshooting) - - [Diagnostic Tool](#diagnostic-tool) + - [Doctor Tool](#doctor-tool) - [Privacy](#privacy) - [What is sent to Sentry?](#what-is-sent-to-sentry) - [Opting Out of Sentry](#opting-out-of-sentry) @@ -105,7 +105,7 @@ For clients that support MCP resources XcodeBuildMCP provides efficient URI-base - **Simulators Resource** (`xcodebuildmcp://simulators`): Direct access to available iOS simulators with UUIDs and states - **Devices Resource** (`xcodebuildmcp://devices`): Direct access to connected physical Apple devices with UDIDs and states -- **Environment Resource** (`xcodebuildmcp://environment`): Direct access to environment information such as Xcode version, macOS version, and Node.js version +- **Doctor Resource** (`xcodebuildmcp://doctor`): Direct access to environment information such as Xcode version, macOS version, and Node.js version ## Getting started @@ -294,18 +294,18 @@ For device deployment features to work, code signing must be properly configured ## Troubleshooting -If you encounter issues with XcodeBuildMCP, the diagnostic tool can help identify the problem by providing detailed information about your environment and dependencies. +If you encounter issues with XcodeBuildMCP, the doctor tool can help identify the problem by providing detailed information about your environment and dependencies. -### Diagnostic Tool +### Doctor Tool -The diagnostic tool is a standalone utility that checks your system configuration and reports on the status of all dependencies required by XcodeBuildMCP. It's particularly useful when reporting issues. +The doctor tool is a standalone utility that checks your system configuration and reports on the status of all dependencies required by XcodeBuildMCP. It's particularly useful when reporting issues. ```bash -# Run the diagnostic tool using npx -npx --package xcodebuildmcp@latest xcodebuildmcp-diagnostic +# Run the doctor tool using npx +npx --package xcodebuildmcp@latest xcodebuildmcp-doctor ``` -The diagnostic tool will output comprehensive information about: +The doctor tool will output comprehensive information about: - System and Node.js environment - Xcode installation and configuration @@ -313,7 +313,7 @@ The diagnostic tool will output comprehensive information about: - Environment variables affecting XcodeBuildMCP - Feature availability status -When reporting issues on GitHub, please include the full output from the diagnostic tool to help with troubleshooting. +When reporting issues on GitHub, please include the full output from the doctor tool to help with troubleshooting. ## Privacy diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9a0cbe8a..b2121ccc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -20,7 +20,7 @@ XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operat ### High-Level Objectives -- Expose Xcode-related tools (build, test, deploy, diagnostics, UI automation) through MCP +- Expose Xcode-related tools (build, test, deploy, UI automation, etc.) through MCP - Run as a long-lived stdio-based server for LLM agents, CLIs, or editors - Enable fine-grained, opt-in activation of individual tools or tool groups - Support incremental builds via experimental xcodemake with xcodebuild fallback @@ -97,8 +97,8 @@ Main server entry point responsible for: - Process lifecycle management (SIGTERM, SIGINT) - Error handling and logging -#### `src/diagnostic-cli.ts` -Standalone diagnostic tool for: +#### `src/doctor-cli.ts` +Standalone doctor tool for: - Environment validation - Dependency checking - Configuration verification @@ -370,7 +370,7 @@ describe('Tool Name', () => { - Compiles TypeScript with tsup 2. **Build Configuration** (`tsup.config.ts`) - - Entry points: `index.ts`, `diagnostic-cli.ts` + - Entry points: `index.ts`, `doctor-cli.ts` - Output format: ESM - Target: Node 18+ - Source maps enabled @@ -379,7 +379,7 @@ describe('Tool Name', () => { ``` build/ ├── index.js # Main server executable - ├── diagnostic-cli.js # Diagnostic tool + ├── doctor-cli.js # Doctor tool └── *.js.map # Source maps ``` @@ -388,7 +388,7 @@ describe('Tool Name', () => { - **Name**: `xcodebuildmcp` - **Executables**: - `xcodebuildmcp` → Main server - - `xcodebuildmcp-diagnostic` → Diagnostic tool + - `xcodebuildmcp-doctor` → Doctor tool - **Dependencies**: Minimal runtime dependencies - **Platform**: macOS only (due to Xcode requirement) @@ -439,7 +439,6 @@ The guide covers: 1. **Use Tool Groups**: Enable only needed workflows 2. **Enable Incremental Builds**: Set `INCREMENTAL_BUILDS_ENABLED=true` 3. **Limit Log Capture**: Use structured logging when possible -4. **Profile Performance**: Use diagnostic tool for bottleneck identification ## Security Considerations diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0a00238e..9c6abf42 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -195,19 +195,15 @@ XCODEBUILDMCP_DYNAMIC_TOOLS=true reloaderoo inspect mcp -- node build/index.js - **Dynamic Mode**: Only 1 tool (`discover_tools`) available initially - **Dynamic Discovery**: After calling `discover_tools`, additional workflow tools become available -#### Using the diagnostic tool +#### Using XcodeBuildMCP doctor tool -Running the XcodeBuildMCP server with the environmental variable `XCODEBUILDMCP_DEBUG=true` will expose a new diagnostic tool which you can run using MCP Inspector: +Running the XcodeBuildMCP server with the environmental variable `XCODEBUILDMCP_DEBUG=true` will expose a new doctor MCP tool called `doctor` which your agent can call to get information about the server's environment, available tools, and configuration status. -```bash -XCODEBUILDMCP_DEBUG=true npm run inspect -``` - -Alternatively, you can run the diagnostic tool directly: - -```bash -node build/diagnostic-cli.js -``` +> [!NOTE] +> You can also call the doctor tool directly using the following command but be advised that the output may vary from that of the MCP tool call due to environmental differences: +> ```bash +> npm run doctor +> ``` #### Development Workflow with Reloaderoo diff --git a/docs/MANUAL_TESTING.md b/docs/MANUAL_TESTING.md index 152b46ba..c3e3ce4a 100644 --- a/docs/MANUAL_TESTING.md +++ b/docs/MANUAL_TESTING.md @@ -83,11 +83,11 @@ Black Box Testing means testing ONLY through external interfaces without any kno // ❌ FORBIDDEN - Using tools as native functions const devices = await list_devices(); - const result = await diagnostic(); + const result = await doctor(); // ✅ CORRECT - Only through Reloaderoo inspect npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js - npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js + npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js ``` **WHY RELOADEROO INSPECT IS MANDATORY:** @@ -150,7 +150,7 @@ grep "^ • " /tmp/tools_detailed.txt | sed 's/^ • //' > /tmp/tool_names.t # Create individual todo items for each tool discovered # Use the actual tool count from step 1 # Example for first few tools: -# 1. [ ] Test tool: diagnostic +# 1. [ ] Test tool: doctor # 2. [ ] Test tool: list_devices # 3. [ ] Test tool: list_sims # ... (continue for ALL $TOTAL_TOOLS tools) @@ -238,7 +238,7 @@ fi **CRITICAL: Tools must be tested in dependency order:** 1. **Foundation Tools** (provide data for other tools): - - `diagnostic` - System info + - `doctor` - System info - `list_devices` - Device UUIDs - `list_sims` - Simulator UUIDs - `discover_projs` - Project/workspace paths @@ -467,9 +467,9 @@ done < /tmp/resource_uris.txt ```bash echo "=== FOUNDATION TOOL TESTING & DATA COLLECTION ===" -# 1. Test diagnostic (no dependencies) -echo "Testing diagnostic..." -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js 2>/dev/null +# 1. Test doctor (no dependencies) +echo "Testing doctor..." +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js 2>/dev/null # 2. Collect device data echo "Collecting device UUIDs..." @@ -543,7 +543,7 @@ done < /tmp/workspace_paths.txt ```bash # STEP 1: Test foundation tools (no parameters required) # Execute each command individually, wait for response, verify manually -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js # [Wait for response, read output, mark tool complete in task list] npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js @@ -616,8 +616,8 @@ npx reloaderoo@latest inspect call-tool "build_sim_id_proj" --params '{"projectP **VALID TESTING SEQUENCE EXAMPLE:** ```bash # ✅ CORRECT - Step-by-step manual execution via Reloaderoo -# Tool 1: Test diagnostic -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js +# Tool 1: Test doctor +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js # [Read response, verify, mark complete in TodoWrite] # Tool 2: Test list_devices diff --git a/docs/PLUGIN_DEVELOPMENT.md b/docs/PLUGIN_DEVELOPMENT.md index 59025d0c..d64080c4 100644 --- a/docs/PLUGIN_DEVELOPMENT.md +++ b/docs/PLUGIN_DEVELOPMENT.md @@ -44,7 +44,7 @@ src/mcp/tools/ ├── ui-testing/ # UI automation tools ├── project-discovery/ # Project analysis tools ├── utilities/ # General utilities -├── diagnostics/ # Diagnostic tools +├── doctor/ # System health check tools ├── logging/ # Log capture tools └── discovery/ # Dynamic tool discovery ``` diff --git a/docs/RELOADEROO.md b/docs/RELOADEROO.md index f327116d..16dbe7c5 100644 --- a/docs/RELOADEROO.md +++ b/docs/RELOADEROO.md @@ -66,8 +66,8 @@ npx reloaderoo@latest inspect ping -- node build/index.js # List connected devices npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/index.js -# Get diagnostic information -npx reloaderoo@latest inspect call-tool diagnostic --params '{}' -- node build/index.js +# Get doctor information +npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/index.js # List iOS simulators npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/index.js @@ -209,7 +209,7 @@ Options: Examples: npx reloaderoo@latest info # Show basic system information - npx reloaderoo@latest info --verbose # Show detailed diagnostics + npx reloaderoo@latest info --verbose # Show detailed system information ``` ### Response Format @@ -355,7 +355,7 @@ npx reloaderoo@latest proxy --log-level debug -- node build/index.js **JSON parsing errors:** ```bash -# Check server information for diagnostics +# Check server information for troubleshooting npx reloaderoo@latest inspect server-info -- node build/index.js # Ensure your server outputs valid JSON @@ -386,7 +386,7 @@ npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build npx reloaderoo@latest proxy --debug -- node build/index.js # For proxy mode npx reloaderoo@latest proxy --log-level debug -- node build/index.js # For detailed proxy logging -# View system diagnostics +# View system information npx reloaderoo@latest info --verbose ``` diff --git a/docs/TESTING.md b/docs/TESTING.md index 260e5208..624af2ab 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -578,11 +578,11 @@ Black Box Testing means testing ONLY through external interfaces without any kno // ❌ FORBIDDEN - Using tools as native functions const devices = await list_devices(); - const result = await diagnostic(); + const result = await doctor(); // ✅ CORRECT - Only through Reloaderoo inspect npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js - npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js + npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js ``` ### WHY RELOADEROO INSPECT IS MANDATORY @@ -643,7 +643,7 @@ jq -r '.tools[].name' /tmp/all_tools.json > /tmp/tool_names.txt ```bash # Create individual todo items for each of the 83+ tools # Example for first few tools: -# 1. [ ] Test tool: diagnostic +# 1. [ ] Test tool: doctor # 2. [ ] Test tool: list_devices # 3. [ ] Test tool: list_sims # ... (continue for ALL 83+ tools) @@ -731,7 +731,7 @@ fi **CRITICAL: Tools must be tested in dependency order:** 1. **Foundation Tools** (provide data for other tools): - - `diagnostic` - System info + - `doctor` - System info - `list_devices` - Device UUIDs - `list_sims` - Simulator UUIDs - `discover_projs` - Project/workspace paths @@ -959,9 +959,9 @@ done < /tmp/resource_uris.txt ```bash echo "=== FOUNDATION TOOL TESTING & DATA COLLECTION ===" -# 1. Test diagnostic (no dependencies) -echo "Testing diagnostic..." -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js 2>/dev/null +# 1. Test doctor (no dependencies) +echo "Testing doctor..." +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js 2>/dev/null # 2. Collect device data echo "Collecting device UUIDs..." @@ -1035,7 +1035,7 @@ done < /tmp/workspace_paths.txt ```bash # STEP 1: Test foundation tools (no parameters required) # Execute each command individually, wait for response, verify manually -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js # [Wait for response, read output, mark tool complete in task list] npx reloaderoo@latest inspect call-tool "list_devices" --params '{}' -- node build/index.js @@ -1108,8 +1108,8 @@ npx reloaderoo@latest inspect call-tool "build_sim_id_proj" --params '{"projectP ### VALID TESTING SEQUENCE EXAMPLE ```bash # ✅ CORRECT - Step-by-step manual execution via Reloaderoo -# Tool 1: Test diagnostic -npx reloaderoo@latest inspect call-tool "diagnostic" --params '{}' -- node build/index.js +# Tool 1: Test doctor +npx reloaderoo@latest inspect call-tool "doctor" --params '{}' -- node build/index.js # [Read response, verify, mark complete in TodoWrite] # Tool 2: Test list_devices diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 6b9b71db..3a17f8bd 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -188,9 +188,9 @@ XcodeBuildMCP uses a **workflow-based architecture** with tools organized into - `clean_proj` - Cleans build products for a specific project file using xcodebuild - `clean_ws` - Cleans build products for a specific workspace using xcodebuild -#### 15. System Diagnostics (`diagnostics`) -**Purpose**: System diagnostics and environment validation (1 tool) -- `diagnostic` - Provides comprehensive information about the MCP server environment, available dependencies, and configuration status +#### 15. System Doctor (`doctor`) +**Purpose**: System health checks and environment validation (1 tool) +- `doctor` - Provides comprehensive information about the MCP server environment, available dependencies, and configuration status ### Operating Modes @@ -211,4 +211,4 @@ For clients that support MCP resources, XcodeBuildMCP provides efficient URI-bas |--------------|-------------|---------------| | `xcodebuildmcp://simulators` | Available iOS simulators with UUIDs and states | `list_sims` | | `xcodebuildmcp://devices` | Available physical Apple devices with UUIDs, names, and connection status | `list_devices` | -| `xcodebuildmcp://environment` | System diagnostics and environment validation | `diagnostic` | \ No newline at end of file +| `xcodebuildmcp://doctor` | System health checks and environment validation | `doctor` | \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b6d1b881..e30903f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ }, "bin": { "xcodebuildmcp": "build/index.js", - "xcodebuildmcp-diagnostic": "build/diagnostic-cli.js" + "xcodebuildmcp-doctor": "build/doctor-cli.js" }, "devDependencies": { "@bacons/xcode": "^1.0.0-alpha.24", diff --git a/package.json b/package.json index d029a612..e2b0a6ff 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "type": "module", "bin": { "xcodebuildmcp": "build/index.js", - "xcodebuildmcp-diagnostic": "build/diagnostic-cli.js" + "xcodebuildmcp-doctor": "build/doctor-cli.js" }, "scripts": { "build": "node -e \"const fs = require('fs'); const pkg = require('./package.json'); fs.writeFileSync('src/version.ts', \\`export const version = '\\${pkg.version}';\\nexport const iOSTemplateVersion = '\\${pkg.iOSTemplateVersion}';\\nexport const macOSTemplateVersion = '\\${pkg.macOSTemplateVersion}';\\n\\`)\" && tsup", @@ -19,7 +19,7 @@ "format:check": "prettier --check 'src/**/*.{js,ts}'", "typecheck": "npx tsc --noEmit", "inspect": "npx @modelcontextprotocol/inspector node build/index.js", - "diagnostic": "node build/diagnostic-cli.js", + "doctor": "node build/doctor-cli.js", "tools": "node scripts/tools-cli.js", "tools:list": "node scripts/tools-cli.js list", "tools:static": "node scripts/tools-cli.js static", diff --git a/src/diagnostic-cli.ts b/src/diagnostic-cli.ts deleted file mode 100644 index 39a6fb7a..00000000 --- a/src/diagnostic-cli.ts +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env node - -/** - * XcodeBuildMCP Diagnostic CLI - * - * This standalone script runs the diagnostic tool and outputs the results - * to the console. It's designed to be run directly via npx or mise. - */ - -import { version } from './version.js'; -import type { ToolResponse } from './types/common.js'; - -// Set the debug environment variable -process.env.XCODEBUILDMCP_DEBUG = 'true'; - -async function runDiagnostic(): Promise { - try { - // Using console.error to avoid linting issues as it's allowed by the project's linting rules - console.error(`Running XcodeBuildMCP Diagnostic Tool (v${version})...`); - console.error('Collecting system information and checking dependencies...\n'); - - // Import the diagnostic plugin from the correct path - const diagnosticPlugin = await import('./mcp/tools/diagnostics/diagnostic.js'); - const runDiagnosticTool = diagnosticPlugin.default?.handler; - - if (!runDiagnosticTool) { - console.error('Error: Diagnostic tool handler not found'); - process.exit(1); - } - - // Run the diagnostic tool (plugin handler expects params object) - const result = (await runDiagnosticTool({})) as ToolResponse; - - // Output the diagnostic information - if (result.content && result.content.length > 0) { - const textContent = result.content.find((item) => item.type === 'text'); - if (textContent && textContent.type === 'text') { - // eslint-disable-next-line no-console - console.log(textContent.text); - } else { - console.error('Error: Unexpected diagnostic result format'); - } - } else { - console.error('Error: No diagnostic information returned'); - } - - console.error('\nDiagnostic complete. Please include this output when reporting issues.'); - } catch (error) { - console.error('Error running diagnostic:', error); - process.exit(1); - } -} - -// Run the diagnostic -runDiagnostic().catch((error) => { - console.error('Unhandled exception:', error); - process.exit(1); -}); diff --git a/src/doctor-cli.ts b/src/doctor-cli.ts new file mode 100644 index 00000000..a9e469ee --- /dev/null +++ b/src/doctor-cli.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/** + * XcodeBuildMCP Doctor CLI + * + * This standalone script runs the doctor tool and outputs the results + * to the console. It's designed to be run directly via npx or mise. + */ + +import { version } from './version.js'; +import { doctorLogic } from './mcp/tools/doctor/doctor.js'; +import { getDefaultCommandExecutor } from './utils/index.js'; + +async function runDoctor(): Promise { + try { + // Using console.error to avoid linting issues as it's allowed by the project's linting rules + console.error(`Running XcodeBuildMCP Doctor (v${version})...`); + console.error('Collecting system information and checking dependencies...\n'); + + // Run the doctor tool logic directly with CLI flag enabled + const executor = getDefaultCommandExecutor(); + const result = await doctorLogic({}, executor, true); // showAsciiLogo = true for CLI + + // Output the doctor information + if (result.content && result.content.length > 0) { + const textContent = result.content.find((item) => item.type === 'text'); + if (textContent && textContent.type === 'text') { + // eslint-disable-next-line no-console + console.log(textContent.text); + } else { + console.error('Error: Unexpected doctor result format'); + } + } else { + console.error('Error: No doctor information returned'); + } + + console.error('\nDoctor run complete. Please include this output when reporting issues.'); + } catch (error) { + console.error('Error running doctor:', error); + process.exit(1); + } +} + +// Run the doctor +runDoctor().catch((error) => { + console.error('Unhandled exception:', error); + process.exit(1); +}); diff --git a/src/mcp/resources/__tests__/environment.test.ts b/src/mcp/resources/__tests__/doctor.test.ts similarity index 52% rename from src/mcp/resources/__tests__/environment.test.ts rename to src/mcp/resources/__tests__/doctor.test.ts index 2ad8d0b0..f6c59503 100644 --- a/src/mcp/resources/__tests__/environment.test.ts +++ b/src/mcp/resources/__tests__/doctor.test.ts @@ -1,26 +1,26 @@ import { describe, it, expect } from 'vitest'; -import environmentResource, { environmentResourceLogic } from '../environment.js'; +import doctorResource, { doctorResourceLogic } from '../doctor.js'; import { createMockExecutor } from '../../../utils/command.js'; -describe('environment resource', () => { +describe('doctor resource', () => { describe('Export Field Validation', () => { it('should export correct uri', () => { - expect(environmentResource.uri).toBe('xcodebuildmcp://environment'); + expect(doctorResource.uri).toBe('xcodebuildmcp://doctor'); }); it('should export correct description', () => { - expect(environmentResource.description).toBe( + expect(doctorResource.description).toBe( 'Comprehensive development environment diagnostic information and configuration status', ); }); it('should export correct mimeType', () => { - expect(environmentResource.mimeType).toBe('text/plain'); + expect(doctorResource.mimeType).toBe('text/plain'); }); it('should export handler function', () => { - expect(typeof environmentResource.handler).toBe('function'); + expect(typeof doctorResource.handler).toBe('function'); }); }); @@ -31,10 +31,10 @@ describe('environment resource', () => { output: 'Mock command output', }); - const result = await environmentResourceLogic(mockExecutor); + const result = await doctorResourceLogic(mockExecutor); expect(result.contents).toHaveLength(1); - expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report'); + expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor'); expect(result.contents[0].text).toContain('## System Information'); expect(result.contents[0].text).toContain('## Node.js Information'); expect(result.contents[0].text).toContain('## Dependencies'); @@ -42,28 +42,41 @@ describe('environment resource', () => { expect(result.contents[0].text).toContain('## Feature Status'); }); - it('should handle spawn errors by showing diagnostic info', async () => { + it('should handle spawn errors by showing doctor info', async () => { const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT')); - const result = await environmentResourceLogic(mockExecutor); + const result = await doctorResourceLogic(mockExecutor); expect(result.contents).toHaveLength(1); - expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report'); + expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor'); expect(result.contents[0].text).toContain('Error: spawn xcrun ENOENT'); }); - it('should include required diagnostic sections', async () => { - const mockExecutor = createMockExecutor({ - success: true, - output: 'Mock output', - }); - - const result = await environmentResourceLogic(mockExecutor); - - expect(result.contents[0].text).toContain('## Troubleshooting Tips'); - expect(result.contents[0].text).toContain('brew tap cameroncooke/axe'); - expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1'); - expect(result.contents[0].text).toContain('discover_tools'); + it('should include required doctor sections', async () => { + // Set dynamic tools environment variable to include discover_tools text + const originalValue = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS; + process.env.XCODEBUILDMCP_DYNAMIC_TOOLS = 'true'; + + try { + const mockExecutor = createMockExecutor({ + success: true, + output: 'Mock output', + }); + + const result = await doctorResourceLogic(mockExecutor); + + expect(result.contents[0].text).toContain('## Troubleshooting Tips'); + expect(result.contents[0].text).toContain('brew tap cameroncooke/axe'); + expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1'); + expect(result.contents[0].text).toContain('discover_tools'); + } finally { + // Restore original environment variable + if (originalValue === undefined) { + delete process.env.XCODEBUILDMCP_DYNAMIC_TOOLS; + } else { + process.env.XCODEBUILDMCP_DYNAMIC_TOOLS = originalValue; + } + } }); it('should provide feature status information', async () => { @@ -72,7 +85,7 @@ describe('environment resource', () => { output: 'Mock output', }); - const result = await environmentResourceLogic(mockExecutor); + const result = await doctorResourceLogic(mockExecutor); expect(result.contents[0].text).toContain('### UI Automation (axe)'); expect(result.contents[0].text).toContain('### Incremental Builds'); @@ -87,10 +100,10 @@ describe('environment resource', () => { error: 'Command failed', }); - const result = await environmentResourceLogic(mockExecutor); + const result = await doctorResourceLogic(mockExecutor); expect(result.contents).toHaveLength(1); - expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report'); + expect(result.contents[0].text).toContain('XcodeBuildMCP Doctor'); }); }); }); diff --git a/src/mcp/resources/doctor.ts b/src/mcp/resources/doctor.ts new file mode 100644 index 00000000..0fb18c14 --- /dev/null +++ b/src/mcp/resources/doctor.ts @@ -0,0 +1,69 @@ +/** + * Doctor Resource Plugin + * + * Provides access to development environment doctor information through MCP resource system. + * This resource reuses the existing doctor tool logic to maintain consistency. + */ + +import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.ts'; +import { doctorLogic } from '../tools/doctor/doctor.ts'; + +// Testable resource logic separated from MCP handler +export async function doctorResourceLogic( + executor: CommandExecutor = getDefaultCommandExecutor(), +): Promise<{ contents: Array<{ text: string }> }> { + try { + log('info', 'Processing doctor resource request'); + const result = await doctorLogic({}, executor); + + if (result.isError) { + const textItem = result.content.find((i) => i.type === 'text') as + | { type: 'text'; text: string } + | undefined; + const errorText = textItem?.text; + const errorMessage = + typeof errorText === 'string' ? errorText : 'Failed to retrieve doctor data'; + log('error', `Error in doctor resource handler: ${errorMessage}`); + return { + contents: [ + { + text: `Error retrieving doctor data: ${errorMessage}`, + }, + ], + }; + } + + const okTextItem = result.content.find((i) => i.type === 'text') as + | { type: 'text'; text: string } + | undefined; + return { + contents: [ + { + text: okTextItem?.text ?? 'No doctor data available', + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log('error', `Error in doctor resource handler: ${errorMessage}`); + + return { + contents: [ + { + text: `Error retrieving doctor data: ${errorMessage}`, + }, + ], + }; + } +} + +export default { + uri: 'xcodebuildmcp://doctor', + name: 'doctor', + description: + 'Comprehensive development environment diagnostic information and configuration status', + mimeType: 'text/plain', + async handler(): Promise<{ contents: Array<{ text: string }> }> { + return doctorResourceLogic(); + }, +}; diff --git a/src/mcp/resources/environment.ts b/src/mcp/resources/environment.ts deleted file mode 100644 index 55d36fee..00000000 --- a/src/mcp/resources/environment.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Environment Resource Plugin - * - * Provides access to development environment diagnostic information through MCP resource system. - * This resource reuses the existing diagnostic tool logic to maintain consistency. - */ - -import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js'; -import { diagnosticLogic } from '../tools/diagnostics/diagnostic.js'; - -// Testable resource logic separated from MCP handler -export async function environmentResourceLogic( - executor: CommandExecutor = getDefaultCommandExecutor(), -): Promise<{ contents: Array<{ text: string }> }> { - try { - log('info', 'Processing environment resource request'); - const result = await diagnosticLogic({}, executor); - - if (result.isError) { - const errorText = result.content[0]?.text; - const errorMessage = - typeof errorText === 'string' ? errorText : 'Failed to retrieve environment data'; - log('error', `Error in environment resource handler: ${errorMessage}`); - return { - contents: [ - { - text: `Error retrieving environment data: ${errorMessage}`, - }, - ], - }; - } - - return { - contents: [ - { - text: - typeof result.content[0]?.text === 'string' - ? result.content[0].text - : 'No environment data available', - }, - ], - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - log('error', `Error in environment resource handler: ${errorMessage}`); - - return { - contents: [ - { - text: `Error retrieving environment data: ${errorMessage}`, - }, - ], - }; - } -} - -export default { - uri: 'xcodebuildmcp://environment', - name: 'environment', - description: - 'Comprehensive development environment diagnostic information and configuration status', - mimeType: 'text/plain', - async handler(): Promise<{ contents: Array<{ text: string }> }> { - return environmentResourceLogic(); - }, -}; diff --git a/src/mcp/tools/diagnostics/__tests__/diagnostic.test.ts b/src/mcp/tools/diagnostics/__tests__/diagnostic.test.ts deleted file mode 100644 index 41208df2..00000000 --- a/src/mcp/tools/diagnostics/__tests__/diagnostic.test.ts +++ /dev/null @@ -1,436 +0,0 @@ -/** - * Tests for diagnostic plugin - * Following CLAUDE.md testing standards with literal validation - * Using dependency injection for deterministic testing - */ - -import { describe, it, expect, beforeEach } from 'vitest'; -import { z } from 'zod'; -import { createMockExecutor } from '../../../../utils/command.js'; -import diagnostic, { diagnosticLogic } from '../diagnostic.ts'; - -// Mock utilities interface for dependency injection -interface MockUtilities { - areAxeToolsAvailable: () => boolean; - isXcodemakeEnabled: () => boolean; - isXcodemakeAvailable: () => Promise; - doesMakefileExist: (path: string) => boolean; - loadPlugins: () => Promise>; -} - -function createMockUtilities(overrides?: Partial): MockUtilities { - return { - areAxeToolsAvailable: () => true, - isXcodemakeEnabled: () => true, - isXcodemakeAvailable: async () => true, - doesMakefileExist: () => true, - loadPlugins: async () => { - const plugins = new Map(); - plugins.set('test-plugin', { name: 'test-plugin', pluginPath: 'test/path/test-plugin.ts' }); - return plugins; - }, - ...overrides, - }; -} - -describe('diagnostic tool', () => { - // Reset any state if needed - - describe('Export Field Validation (Literal)', () => { - it('should have correct name', () => { - expect(diagnostic.name).toBe('diagnostic'); - }); - - it('should have correct description', () => { - expect(diagnostic.description).toBe( - 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', - ); - }); - - it('should have handler function', () => { - expect(typeof diagnostic.handler).toBe('function'); - }); - - it('should have correct schema with enabled boolean field', () => { - const schema = z.object(diagnostic.schema); - - // Valid inputs - expect(schema.safeParse({ enabled: true }).success).toBe(true); - expect(schema.safeParse({ enabled: false }).success).toBe(true); - expect(schema.safeParse({}).success).toBe(true); // enabled is optional - - // Invalid inputs - expect(schema.safeParse({ enabled: 'true' }).success).toBe(false); - expect(schema.safeParse({ enabled: 1 }).success).toBe(false); - expect(schema.safeParse({ enabled: null }).success).toBe(false); - }); - }); - - describe('Handler Behavior (Complete Literal Returns)', () => { - it('should handle successful diagnostic execution', async () => { - const mockExecutor = async (command: string[]) => { - const cmdString = command.join(' '); - - if (cmdString === 'which axe') - return { success: true, output: '/usr/local/bin/axe', process: { pid: 123 } }; - if (cmdString === 'which xcodemake') - return { success: true, output: '/usr/local/bin/xcodemake', process: { pid: 123 } }; - if (cmdString === 'which mise') - return { success: true, output: '/usr/local/bin/mise', process: { pid: 123 } }; - if (cmdString === 'axe --version') - return { success: true, output: 'axe version 1.0.0', process: { pid: 123 } }; - if (cmdString === 'xcodebuild -version') - return { - success: true, - output: 'Xcode 15.0\nBuild version 15A240d', - process: { pid: 123 }, - }; - if (cmdString === 'xcode-select -p') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --find xcodebuild') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --version') - return { success: true, output: 'xcrun version 65', process: { pid: 123 } }; - - return { - success: false, - output: '', - error: `Command not found: ${cmdString}`, - process: { pid: 123 }, - }; - }; - - const mockUtilities = createMockUtilities(); - - // Mock process.env for clean test - const originalEnv = process.env; - process.env = { - ...originalEnv, - XCODEBUILDMCP_DEBUG: 'true', - INCREMENTAL_BUILDS_ENABLED: '1', - PATH: '/usr/local/bin:/usr/bin:/bin', - DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', - HOME: '/Users/testuser', - USER: 'testuser', - TMPDIR: '/tmp', - NODE_ENV: 'test', - SENTRY_DISABLED: 'false', - }; - - const result = await diagnosticLogic({ enabled: true }, mockExecutor, mockUtilities); - - // Restore process.env - process.env = originalEnv; - - expect(result.content).toEqual([ - { - type: 'text', - text: result.content[0].text, - }, - ]); - expect(typeof result.content[0].text).toBe('string'); - }); - - it('should handle plugin loading failure', async () => { - const mockExecutor = async (command: string[]) => { - const cmdString = command.join(' '); - - if (cmdString === 'which axe') - return { success: true, output: '/usr/local/bin/axe', process: { pid: 123 } }; - if (cmdString === 'which xcodemake') - return { success: true, output: '/usr/local/bin/xcodemake', process: { pid: 123 } }; - if (cmdString === 'which mise') - return { success: true, output: '/usr/local/bin/mise', process: { pid: 123 } }; - if (cmdString === 'axe --version') - return { success: true, output: 'axe version 1.0.0', process: { pid: 123 } }; - if (cmdString === 'xcodebuild -version') - return { - success: true, - output: 'Xcode 15.0\nBuild version 15A240d', - process: { pid: 123 }, - }; - if (cmdString === 'xcode-select -p') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --find xcodebuild') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --version') - return { success: true, output: 'xcrun version 65', process: { pid: 123 } }; - - return { - success: false, - output: '', - error: `Command not found: ${cmdString}`, - process: { pid: 123 }, - }; - }; - - const mockUtilities = createMockUtilities({ - loadPlugins: async () => { - throw new Error('Plugin loading failed'); - }, - }); - - // Mock process.env for clean test - const originalEnv = process.env; - process.env = { - ...originalEnv, - XCODEBUILDMCP_DEBUG: 'true', - INCREMENTAL_BUILDS_ENABLED: '1', - PATH: '/usr/local/bin:/usr/bin:/bin', - DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', - HOME: '/Users/testuser', - USER: 'testuser', - TMPDIR: '/tmp', - NODE_ENV: 'test', - SENTRY_DISABLED: 'false', - }; - - const result = await diagnosticLogic({ enabled: true }, mockExecutor, mockUtilities); - - // Restore process.env - process.env = originalEnv; - - expect(result.content).toEqual([ - { - type: 'text', - text: result.content[0].text, - }, - ]); - expect(typeof result.content[0].text).toBe('string'); - }); - - it('should handle xcode command failure', async () => { - const mockExecutor = async (command: string[]) => { - const cmdString = command.join(' '); - - if (cmdString === 'which axe') - return { success: true, output: '/usr/local/bin/axe', process: { pid: 123 } }; - if (cmdString === 'which xcodemake') - return { success: true, output: '/usr/local/bin/xcodemake', process: { pid: 123 } }; - if (cmdString === 'which mise') - return { success: true, output: '/usr/local/bin/mise', process: { pid: 123 } }; - if (cmdString === 'axe --version') - return { success: true, output: 'axe version 1.0.0', process: { pid: 123 } }; - if (cmdString === 'xcodebuild -version') - return { success: false, output: '', error: 'Xcode not found', process: { pid: 123 } }; - if (cmdString === 'xcode-select -p') - return { success: false, output: '', error: 'Xcode not found', process: { pid: 123 } }; - if (cmdString === 'xcrun --find xcodebuild') - return { success: false, output: '', error: 'Xcode not found', process: { pid: 123 } }; - if (cmdString === 'xcrun --version') - return { success: false, output: '', error: 'Xcode not found', process: { pid: 123 } }; - - return { - success: false, - output: '', - error: `Command not found: ${cmdString}`, - process: { pid: 123 }, - }; - }; - - const mockUtilities = createMockUtilities(); - - // Mock process.env for clean test - const originalEnv = process.env; - process.env = { - ...originalEnv, - XCODEBUILDMCP_DEBUG: 'true', - INCREMENTAL_BUILDS_ENABLED: '1', - PATH: '/usr/local/bin:/usr/bin:/bin', - DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', - HOME: '/Users/testuser', - USER: 'testuser', - TMPDIR: '/tmp', - NODE_ENV: 'test', - SENTRY_DISABLED: 'false', - }; - - const result = await diagnosticLogic({ enabled: true }, mockExecutor, mockUtilities); - - // Restore process.env - process.env = originalEnv; - - expect(result.content).toEqual([ - { - type: 'text', - text: result.content[0].text, - }, - ]); - expect(typeof result.content[0].text).toBe('string'); - }); - - it('should handle xcodemake check failure', async () => { - const mockExecutor = async (command: string[]) => { - const cmdString = command.join(' '); - - if (cmdString === 'which axe') - return { success: true, output: '/usr/local/bin/axe', process: { pid: 123 } }; - if (cmdString === 'which xcodemake') - return { - success: false, - output: '', - error: 'xcodemake not found', - process: { pid: 123 }, - }; - if (cmdString === 'which mise') - return { success: true, output: '/usr/local/bin/mise', process: { pid: 123 } }; - if (cmdString === 'axe --version') - return { success: true, output: 'axe version 1.0.0', process: { pid: 123 } }; - if (cmdString === 'xcodebuild -version') - return { - success: true, - output: 'Xcode 15.0\nBuild version 15A240d', - process: { pid: 123 }, - }; - if (cmdString === 'xcode-select -p') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --find xcodebuild') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --version') - return { success: true, output: 'xcrun version 65', process: { pid: 123 } }; - - return { - success: false, - output: '', - error: `Command not found: ${cmdString}`, - process: { pid: 123 }, - }; - }; - - const mockUtilities = createMockUtilities(); - - // Mock process.env for clean test - const originalEnv = process.env; - process.env = { - ...originalEnv, - XCODEBUILDMCP_DEBUG: 'true', - INCREMENTAL_BUILDS_ENABLED: '1', - PATH: '/usr/local/bin:/usr/bin:/bin', - DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', - HOME: '/Users/testuser', - USER: 'testuser', - TMPDIR: '/tmp', - NODE_ENV: 'test', - SENTRY_DISABLED: 'false', - }; - - const result = await diagnosticLogic({ enabled: true }, mockExecutor, mockUtilities); - - // Restore process.env - process.env = originalEnv; - - expect(result.content).toEqual([ - { - type: 'text', - text: result.content[0].text, - }, - ]); - expect(typeof result.content[0].text).toBe('string'); - }); - - it('should handle axe tools not available', async () => { - const mockExecutor = async (command: string[]) => { - const cmdString = command.join(' '); - - if (cmdString === 'which axe') - return { success: false, output: '', error: 'axe not found', process: { pid: 123 } }; - if (cmdString === 'which xcodemake') - return { - success: false, - output: '', - error: 'xcodemake not found', - process: { pid: 123 }, - }; - if (cmdString === 'which mise') - return { success: true, output: '/usr/local/bin/mise', process: { pid: 123 } }; - if (cmdString === 'axe --version') - return { success: false, output: '', error: 'axe not found', process: { pid: 123 } }; - if (cmdString === 'xcodebuild -version') - return { - success: true, - output: 'Xcode 15.0\nBuild version 15A240d', - process: { pid: 123 }, - }; - if (cmdString === 'xcode-select -p') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --find xcodebuild') - return { - success: true, - output: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', - process: { pid: 123 }, - }; - if (cmdString === 'xcrun --version') - return { success: true, output: 'xcrun version 65', process: { pid: 123 } }; - - return { - success: false, - output: '', - error: `Command not found: ${cmdString}`, - process: { pid: 123 }, - }; - }; - - const mockUtilities = createMockUtilities({ - areAxeToolsAvailable: () => false, - isXcodemakeEnabled: () => false, - isXcodemakeAvailable: async () => false, - }); - - // Mock process.env for clean test - const originalEnv = process.env; - process.env = { - ...originalEnv, - XCODEBUILDMCP_DEBUG: 'true', - INCREMENTAL_BUILDS_ENABLED: '0', - PATH: '/usr/local/bin:/usr/bin:/bin', - DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', - HOME: '/Users/testuser', - USER: 'testuser', - TMPDIR: '/tmp', - NODE_ENV: 'test', - SENTRY_DISABLED: 'true', - }; - - const result = await diagnosticLogic({ enabled: true }, mockExecutor, mockUtilities); - - // Restore process.env - process.env = originalEnv; - - expect(result.content).toEqual([ - { - type: 'text', - text: result.content[0].text, - }, - ]); - expect(typeof result.content[0].text).toBe('string'); - }); - }); -}); diff --git a/src/mcp/tools/diagnostics/diagnostic.ts b/src/mcp/tools/diagnostics/diagnostic.ts deleted file mode 100644 index ba57deb9..00000000 --- a/src/mcp/tools/diagnostics/diagnostic.ts +++ /dev/null @@ -1,526 +0,0 @@ -/** - * Diagnostics Plugin: Diagnostic Tool - * - * Provides comprehensive information about the MCP server environment. - */ - -import { z } from 'zod'; -import { log } from '../../../utils/index.js'; -import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/index.js'; -import { version } from '../../../utils/index.js'; -import { areAxeToolsAvailable } from '../../../utils/index.js'; -import { - isXcodemakeEnabled, - isXcodemakeAvailable, - doesMakefileExist, -} from '../../../utils/index.js'; -import * as os from 'os'; -import { loadPlugins } from '../../../utils/index.js'; -import { ToolResponse } from '../../../types/common.js'; -import { createTypedTool } from '../../../utils/typed-tool-factory.js'; - -// Mock system interface for dependency injection -interface MockSystem { - executor: CommandExecutor; - platform: () => string; - release: () => string; - arch: () => string; - cpus: () => Array<{ model: string }>; - totalmem: () => number; - hostname: () => string; - userInfo: () => { username: string }; - homedir: () => string; - tmpdir: () => string; -} - -// Mock utilities interface for dependency injection -interface MockUtilities { - areAxeToolsAvailable: () => boolean; - isXcodemakeEnabled: () => boolean; - isXcodemakeAvailable: () => Promise; - doesMakefileExist: (path: string) => boolean; - loadPlugins: () => Promise>; -} - -// Constants -const LOG_PREFIX = '[Diagnostic]'; - -/** - * Check if a binary is available in the PATH and attempt to get its version - */ -async function checkBinaryAvailability( - binary: string, - mockSystem?: MockSystem, -): Promise<{ available: boolean; version?: string }> { - const commandExecutor = mockSystem?.executor; - - // Fallback executor for when no mock is provided - const fallbackExecutor = async (): Promise<{ - success: boolean; - output: string; - error: string; - }> => ({ - success: false, - output: '', - error: 'Binary not found', - }); - - // First check if the binary exists at all - try { - const whichResult = await (commandExecutor ?? fallbackExecutor)( - ['which', binary], - 'Check Binary Availability', - ); - if (!whichResult.success) { - return { available: false }; - } - } catch { - // Binary not found in PATH - return { available: false }; - } - - // Binary exists, now try to get version info if possible - let version; - - // Define version commands for specific binaries - const versionCommands: Record = { - axe: 'axe --version', - mise: 'mise --version', - }; - - // Try to get version using binary-specific commands - if (binary in versionCommands) { - try { - const versionResult = await (commandExecutor ?? fallbackExecutor)( - versionCommands[binary]!.split(' '), - 'Get Binary Version', - ); - if (versionResult.success && versionResult.output) { - const output = versionResult.output.trim(); - // For xcodebuild, include both version and build info - if (binary === 'xcodebuild') { - const lines = output.split('\n').slice(0, 2); - version = lines.join(' - '); - } else { - version = output; - } - } - } catch { - // Command failed, continue to generic attempts - } - } - - // We only care about the specific binaries we've defined - return { - available: true, - version: version ?? 'Available (version info not available)', - }; -} - -/** - * Get information about the Xcode installation - */ -async function getXcodeInfo( - mockSystem?: MockSystem, -): Promise< - { version: string; path: string; selectedXcode: string; xcrunVersion: string } | { error: string } -> { - const commandExecutor = mockSystem?.executor; - - // Fallback executor for when no mock is provided - const fallbackExecutor = async (): Promise<{ - success: boolean; - output: string; - error: string; - }> => ({ - success: false, - output: '', - error: 'Xcode tool not found', - }); - - try { - // Get Xcode version info - const xcodebuildResult = await (commandExecutor ?? fallbackExecutor)( - ['xcodebuild', '-version'], - 'Get Xcode Version', - ); - if (!xcodebuildResult.success) { - throw new Error('xcodebuild command failed'); - } - const version = xcodebuildResult.output.trim().split('\n').slice(0, 2).join(' - '); - - // Get Xcode selection info - const pathResult = await (commandExecutor ?? fallbackExecutor)( - ['xcode-select', '-p'], - 'Get Xcode Path', - ); - if (!pathResult.success) { - throw new Error('xcode-select command failed'); - } - const path = pathResult.output.trim(); - - const selectedXcodeResult = await (commandExecutor ?? fallbackExecutor)( - ['xcrun', '--find', 'xcodebuild'], - 'Find Xcodebuild', - ); - if (!selectedXcodeResult.success) { - throw new Error('xcrun --find command failed'); - } - const selectedXcode = selectedXcodeResult.output.trim(); - - // Get xcrun version info - const xcrunVersionResult = await (commandExecutor ?? fallbackExecutor)( - ['xcrun', '--version'], - 'Get Xcrun Version', - ); - if (!xcrunVersionResult.success) { - throw new Error('xcrun --version command failed'); - } - const xcrunVersion = xcrunVersionResult.output.trim(); - - return { version, path, selectedXcode, xcrunVersion }; - } catch (error) { - return { error: error instanceof Error ? error.message : String(error) }; - } -} - -/** - * Get information about the environment variables - */ -function getEnvironmentVariables(): Record { - const relevantVars = [ - 'XCODEBUILDMCP_DEBUG', - 'INCREMENTAL_BUILDS_ENABLED', - 'PATH', - 'DEVELOPER_DIR', - 'HOME', - 'USER', - 'TMPDIR', - 'NODE_ENV', - 'SENTRY_DISABLED', - ]; - - const envVars: Record = {}; - - // Add standard environment variables - for (const varName of relevantVars) { - envVars[varName] = process.env[varName]; - } - - // Add all tool and group environment variables for debugging - Object.keys(process.env).forEach((key) => { - if ( - key.startsWith('XCODEBUILDMCP_TOOL_') || - key.startsWith('XCODEBUILDMCP_GROUP_') || - key.startsWith('XCODEBUILDMCP_') - ) { - envVars[key] = process.env[key]; - } - }); - - return envVars; -} - -/** - * Get system information - */ -function getSystemInfo(mockSystem?: MockSystem): { - platform: string; - release: string; - arch: string; - cpus: string; - memory: string; - hostname: string; - username: string; - homedir: string; - tmpdir: string; -} { - const platformFn = mockSystem?.platform ?? os.platform; - const releaseFn = mockSystem?.release ?? os.release; - const archFn = mockSystem?.arch ?? os.arch; - const cpusFn = mockSystem?.cpus ?? os.cpus; - const totalmemFn = mockSystem?.totalmem ?? os.totalmem; - const hostnameFn = mockSystem?.hostname ?? os.hostname; - const userInfoFn = mockSystem?.userInfo ?? os.userInfo; - const homedirFn = mockSystem?.homedir ?? os.homedir; - const tmpdirFn = mockSystem?.tmpdir ?? os.tmpdir; - - return { - platform: platformFn(), - release: releaseFn(), - arch: archFn(), - cpus: `${cpusFn().length} x ${cpusFn()[0]?.model ?? 'Unknown'}`, - memory: `${Math.round(totalmemFn() / (1024 * 1024 * 1024))} GB`, - hostname: hostnameFn(), - username: userInfoFn().username, - homedir: homedirFn(), - tmpdir: tmpdirFn(), - }; -} - -/** - * Get Node.js information - */ -function getNodeInfo(): { - version: string; - execPath: string; - pid: string; - ppid: string; - platform: string; - arch: string; - cwd: string; - argv: string; -} { - return { - version: process.version, - execPath: process.execPath, - pid: process.pid.toString(), - ppid: process.ppid.toString(), - platform: process.platform, - arch: process.arch, - cwd: process.cwd(), - argv: process.argv.join(' '), - }; -} - -/** - * Get information about loaded plugins and their directories - */ -async function getPluginSystemInfo(mockUtilities?: MockUtilities): Promise< - | { - totalPlugins: number; - pluginDirectories: number; - pluginsByDirectory: Record; - systemMode: string; - } - | { error: string; systemMode: string } -> { - const loadPluginsFn = mockUtilities?.loadPlugins ?? loadPlugins; - - try { - const plugins = await loadPluginsFn(); - - // Group plugins by directory - const pluginsByDirectory: Record = {}; - let totalPlugins = 0; - - for (const plugin of Array.from(plugins.values())) { - totalPlugins++; - const pluginWithPath = plugin as { pluginPath?: string; name: string }; - const pluginPath = pluginWithPath.pluginPath ?? 'unknown'; - const directory = pluginPath.split('/').slice(-2, -1)[0] ?? 'unknown'; - - if (!pluginsByDirectory[directory]) { - pluginsByDirectory[directory] = []; - } - pluginsByDirectory[directory].push(pluginWithPath.name); - } - - return { - totalPlugins, - pluginDirectories: Object.keys(pluginsByDirectory).length, - pluginsByDirectory, - systemMode: 'plugin-based', - }; - } catch (error) { - return { - error: `Failed to load plugins: ${error instanceof Error ? error.message : 'Unknown error'}`, - systemMode: 'error', - }; - } -} - -/** - * Get a list of individually enabled tools via environment variables - */ -function getIndividuallyEnabledTools(): string[] { - return Object.keys(process.env) - .filter((key) => key.startsWith('XCODEBUILDMCP_TOOL_') && process.env[key] === 'true') - .map((key) => key.replace('XCODEBUILDMCP_TOOL_', '')); -} - -// Define schema as ZodObject -const diagnosticSchema = z.object({ - enabled: z.boolean().optional().describe('Optional: dummy parameter to satisfy MCP protocol'), -}); - -// Use z.infer for type safety -type DiagnosticParams = z.infer; - -/** - * Run the diagnostic tool and return the results - */ -export async function diagnosticLogic( - params: DiagnosticParams, - executor: CommandExecutor, - mockUtilities?: MockUtilities, -): Promise { - // Create mock system that uses the provided executor - const mockSystem: MockSystem = { - executor, - platform: os.platform, - release: os.release, - arch: os.arch, - cpus: os.cpus, - totalmem: os.totalmem, - hostname: os.hostname, - userInfo: os.userInfo, - homedir: os.homedir, - tmpdir: os.tmpdir, - }; - log('info', `${LOG_PREFIX}: Running diagnostic tool`); - - // Check for required binaries - const requiredBinaries = ['axe', 'xcodemake', 'mise']; - - const binaryStatus: Record = {}; - - for (const binary of requiredBinaries) { - binaryStatus[binary] = await checkBinaryAvailability(binary, mockSystem); - } - - // Get Xcode information - const xcodeInfo = await getXcodeInfo(mockSystem); - - // Get environment variables - const envVars = getEnvironmentVariables(); - - // Get system information - const systemInfo = getSystemInfo(mockSystem); - - // Get Node.js information - const nodeInfo = getNodeInfo(); - - // Check for axe tools availability - const axeAvailable = mockUtilities?.areAxeToolsAvailable?.() ?? areAxeToolsAvailable(); - - // Get plugin system information - const pluginSystemInfo = await getPluginSystemInfo(mockUtilities); - - // Get individually enabled tools - const individuallyEnabledTools = getIndividuallyEnabledTools(); - - // Check for xcodemake configuration - const xcodemakeEnabled = mockUtilities?.isXcodemakeEnabled?.() ?? isXcodemakeEnabled(); - const xcodemakeAvailable = await (mockUtilities?.isXcodemakeAvailable?.() ?? - isXcodemakeAvailable()); - const makefileExists = mockUtilities?.doesMakefileExist?.('./') ?? doesMakefileExist('./'); - - // Compile the diagnostic information - const diagnosticInfo = { - serverVersion: version, - timestamp: new Date().toISOString(), - system: systemInfo, - node: nodeInfo, - xcode: xcodeInfo, - dependencies: binaryStatus, - environmentVariables: envVars, - features: { - axe: { - available: axeAvailable, - uiAutomationSupported: axeAvailable, - }, - xcodemake: { - enabled: xcodemakeEnabled, - available: xcodemakeAvailable, - makefileExists: makefileExists, - }, - mise: { - running_under_mise: Boolean(process.env.XCODEBUILDMCP_RUNNING_UNDER_MISE), - available: binaryStatus['mise'].available, - }, - }, - pluginSystem: pluginSystemInfo, - individuallyEnabledTools, - }; - - // Format the diagnostic information as a nicely formatted text response - const formattedOutput = [ - `# XcodeBuildMCP Diagnostic Report`, - `\nGenerated: ${diagnosticInfo.timestamp}`, - `Server Version: ${diagnosticInfo.serverVersion}`, - - `\n## System Information`, - ...Object.entries(diagnosticInfo.system).map(([key, value]) => `- ${key}: ${value}`), - - `\n## Node.js Information`, - ...Object.entries(diagnosticInfo.node).map(([key, value]) => `- ${key}: ${value}`), - - `\n## Xcode Information`, - ...('error' in diagnosticInfo.xcode - ? [`- Error: ${diagnosticInfo.xcode.error}`] - : Object.entries(diagnosticInfo.xcode).map(([key, value]) => `- ${key}: ${value}`)), - - `\n## Dependencies`, - ...Object.entries(diagnosticInfo.dependencies).map( - ([binary, status]) => - `- ${binary}: ${status.available ? `✅ ${status.version ?? 'Available'}` : '❌ Not found'}`, - ), - - `\n## Environment Variables`, - ...Object.entries(diagnosticInfo.environmentVariables) - .filter(([key]) => key !== 'PATH' && key !== 'PYTHONPATH') // These are too long, handle separately - .map(([key, value]) => `- ${key}: ${value ?? '(not set)'}`), - - `\n### PATH`, - `\`\`\``, - `${diagnosticInfo.environmentVariables.PATH ?? '(not set)'}`.split(':').join('\n'), - `\`\`\``, - - `\n## Feature Status`, - `\n### UI Automation (axe)`, - `- Available: ${diagnosticInfo.features.axe.available ? '✅ Yes' : '❌ No'}`, - `- UI Automation Supported: ${diagnosticInfo.features.axe.uiAutomationSupported ? '✅ Yes' : '❌ No'}`, - - `\n### Incremental Builds`, - `- Enabled: ${diagnosticInfo.features.xcodemake.enabled ? '✅ Yes' : '❌ No'}`, - `- Available: ${diagnosticInfo.features.xcodemake.available ? '✅ Yes' : '❌ No'}`, - `- Makefile exists: ${diagnosticInfo.features.xcodemake.makefileExists ? '✅ Yes' : '❌ No'}`, - - `\n### Mise Integration`, - `- Running under mise: ${diagnosticInfo.features.mise.running_under_mise ? '✅ Yes' : '❌ No'}`, - `- Mise available: ${diagnosticInfo.features.mise.available ? '✅ Yes' : '❌ No'}`, - - `\n### Available Tools`, - `- Total Plugins: ${'totalPlugins' in diagnosticInfo.pluginSystem ? diagnosticInfo.pluginSystem.totalPlugins : 0}`, - `- Plugin Directories: ${'pluginDirectories' in diagnosticInfo.pluginSystem ? diagnosticInfo.pluginSystem.pluginDirectories : 0}`, - ...('pluginsByDirectory' in diagnosticInfo.pluginSystem && - diagnosticInfo.pluginSystem.pluginsByDirectory - ? Object.entries(diagnosticInfo.pluginSystem.pluginsByDirectory).map( - ([dir, tools]) => `- ${dir}: ${Array.isArray(tools) ? tools.length : 0} tools`, - ) - : ['- No plugin directory information available']), - - `\n## Tool Availability Summary`, - `- Build Tools: ${!('error' in diagnosticInfo.xcode) ? '\u2705 Available' : '\u274c Not available'}`, - `- UI Automation Tools: ${diagnosticInfo.features.axe.uiAutomationSupported ? '\u2705 Available' : '\u274c Not available'}`, - `- Incremental Build Support: ${diagnosticInfo.features.xcodemake.available && diagnosticInfo.features.xcodemake.enabled ? '\u2705 Available & Enabled' : diagnosticInfo.features.xcodemake.available ? '\u2705 Available but Disabled' : '\u274c Not available'}`, - - `\n## Sentry`, - `- Sentry enabled: ${diagnosticInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`, - - `\n## Troubleshooting Tips`, - `- If UI automation tools are not available, install axe: \`brew tap cameroncooke/axe && brew install axe\``, - `- If incremental build support is not available, you can download the tool from https://github.com/cameroncooke/xcodemake. Make sure it's executable and available in your PATH`, - `- To enable xcodemake, set environment variable: \`export INCREMENTAL_BUILDS_ENABLED=1\``, - `- For mise integration, follow instructions in the README.md file`, - `- Use the 'discover_tools' tool to find relevant tools for your task`, - ].join('\n'); - - return { - content: [ - { - type: 'text', - text: formattedOutput, - }, - ], - }; -} - -export default { - name: 'diagnostic', - description: - 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', - schema: diagnosticSchema.shape, // MCP SDK compatibility - handler: createTypedTool(diagnosticSchema, diagnosticLogic, getDefaultCommandExecutor), -}; diff --git a/src/mcp/tools/diagnostics/index.ts b/src/mcp/tools/diagnostics/index.ts deleted file mode 100644 index 30369e85..00000000 --- a/src/mcp/tools/diagnostics/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const workflow = { - name: 'System Diagnostics', - description: - 'Debug tools and system diagnostics for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', - platforms: ['system'], - capabilities: ['diagnostics', 'troubleshooting', 'system-analysis', 'environment-validation'], -}; diff --git a/src/mcp/tools/discovery/__tests__/discover_tools.test.ts b/src/mcp/tools/discovery/__tests__/discover_tools.test.ts index a1eb4885..3e821f96 100644 --- a/src/mcp/tools/discovery/__tests__/discover_tools.test.ts +++ b/src/mcp/tools/discovery/__tests__/discover_tools.test.ts @@ -116,7 +116,7 @@ describe('discover_tools', () => { it('should have correct description', () => { expect(discoverTools.description).toBe( - 'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging, diagnostics) and Swift packages.', + 'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.', ); }); diff --git a/src/mcp/tools/discovery/discover_tools.ts b/src/mcp/tools/discovery/discover_tools.ts index 2ca30330..6e44219a 100644 --- a/src/mcp/tools/discovery/discover_tools.ts +++ b/src/mcp/tools/discovery/discover_tools.ts @@ -173,7 +173,7 @@ Secondary (task-based, no project/workspace needed): - Simulator management (boot, list, open, status bar, appearance, GPS/location): choose "simulator-management" - Logging or log capture (simulator or device): choose "logging" - UI automation/gestures/screenshots on a simulator app: choose "ui-testing" -- System/environment diagnostics or validation: choose "diagnostics" +- System/environment diagnostics or validation: choose "doctor" - Create new iOS/macOS projects from templates: choose "project-scaffolding" All available workflows: @@ -369,7 +369,7 @@ Respond with ONLY a JSON array containing ONE workflow name that best matches th export default { name: 'discover_tools', description: - 'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging, diagnostics) and Swift packages.', + 'Analyzes a natural language task description and enables the most relevant development workflow. Prioritizes project/workspace workflows (simulator/device/macOS) and also supports task-based workflows (simulator-management, logging) and Swift packages.', schema: discoverToolsSchema.shape, // MCP SDK compatibility handler: createTypedTool( discoverToolsSchema, diff --git a/src/mcp/tools/doctor/__tests__/doctor.test.ts b/src/mcp/tools/doctor/__tests__/doctor.test.ts new file mode 100644 index 00000000..3555e281 --- /dev/null +++ b/src/mcp/tools/doctor/__tests__/doctor.test.ts @@ -0,0 +1,302 @@ +/** + * Tests for doctor plugin + * Following CLAUDE.md testing standards with literal validation + * Using dependency injection for deterministic testing + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { z } from 'zod'; +import doctor, { runDoctor, type DoctorDependencies } from '../doctor.ts'; + +function createDeps(overrides?: Partial): DoctorDependencies { + const base: DoctorDependencies = { + binaryChecker: { + async checkBinaryAvailability(binary: string) { + // default: all available with generic version + return { available: true, version: `${binary} version 1.0.0` }; + }, + }, + xcode: { + async getXcodeInfo() { + return { + version: 'Xcode 15.0 - Build version 15A240d', + path: '/Applications/Xcode.app/Contents/Developer', + selectedXcode: '/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild', + xcrunVersion: 'xcrun version 65', + }; + }, + }, + env: { + getEnvironmentVariables() { + const x: Record = { + XCODEBUILDMCP_DEBUG: 'true', + INCREMENTAL_BUILDS_ENABLED: '1', + PATH: '/usr/local/bin:/usr/bin:/bin', + DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', + HOME: '/Users/testuser', + USER: 'testuser', + TMPDIR: '/tmp', + NODE_ENV: 'test', + SENTRY_DISABLED: 'false', + }; + return x; + }, + getSystemInfo() { + return { + platform: 'darwin', + release: '25.0.0', + arch: 'arm64', + cpus: '10 x Apple M3', + memory: '32 GB', + hostname: 'localhost', + username: 'testuser', + homedir: '/Users/testuser', + tmpdir: '/tmp', + }; + }, + getNodeInfo() { + return { + version: 'v22.0.0', + execPath: '/usr/local/bin/node', + pid: '123', + ppid: '1', + platform: 'darwin', + arch: 'arm64', + cwd: '/', + argv: 'node build/index.js', + }; + }, + }, + plugins: { + async getPluginSystemInfo() { + return { + totalPlugins: 1, + pluginDirectories: 1, + pluginsByDirectory: { doctor: ['doctor'] }, + systemMode: 'plugin-based', + }; + }, + }, + features: { + areAxeToolsAvailable: () => true, + isXcodemakeEnabled: () => true, + isXcodemakeAvailable: async () => true, + doesMakefileExist: () => true, + }, + runtime: { + async getRuntimeToolInfo() { + return { + mode: 'static' as const, + enabledWorkflows: ['doctor', 'discovery'], + enabledTools: ['doctor', 'discover_tools'], + totalRegistered: 2, + }; + }, + }, + }; + + return { + ...base, + ...overrides, + binaryChecker: { + ...base.binaryChecker, + ...(overrides?.binaryChecker ?? {}), + }, + xcode: { + ...base.xcode, + ...(overrides?.xcode ?? {}), + }, + env: { + ...base.env, + ...(overrides?.env ?? {}), + }, + plugins: { + ...base.plugins, + ...(overrides?.plugins ?? {}), + }, + features: { + ...base.features, + ...(overrides?.features ?? {}), + }, + }; +} + +describe('doctor tool', () => { + // Reset any state if needed + + describe('Export Field Validation (Literal)', () => { + it('should have correct name', () => { + expect(doctor.name).toBe('doctor'); + }); + + it('should have correct description', () => { + expect(doctor.description).toBe( + 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', + ); + }); + + it('should have handler function', () => { + expect(typeof doctor.handler).toBe('function'); + }); + + it('should have correct schema with enabled boolean field', () => { + const schema = z.object(doctor.schema); + + // Valid inputs + expect(schema.safeParse({ enabled: true }).success).toBe(true); + expect(schema.safeParse({ enabled: false }).success).toBe(true); + expect(schema.safeParse({}).success).toBe(true); // enabled is optional + + // Invalid inputs + expect(schema.safeParse({ enabled: 'true' }).success).toBe(false); + expect(schema.safeParse({ enabled: 1 }).success).toBe(false); + expect(schema.safeParse({ enabled: null }).success).toBe(false); + }); + }); + + describe('Handler Behavior (Complete Literal Returns)', () => { + it('should handle successful doctor execution', async () => { + const deps = createDeps(); + const result = await runDoctor({ enabled: true }, deps); + + expect(result.content).toEqual([ + { + type: 'text', + text: result.content[0].text, + }, + ]); + expect(typeof result.content[0].text).toBe('string'); + }); + + it('should handle plugin loading failure', async () => { + const deps = createDeps({ + plugins: { + async getPluginSystemInfo() { + return { error: 'Plugin loading failed', systemMode: 'error' }; + }, + }, + }); + + const result = await runDoctor({ enabled: true }, deps); + + expect(result.content).toEqual([ + { + type: 'text', + text: result.content[0].text, + }, + ]); + expect(typeof result.content[0].text).toBe('string'); + }); + + it('should handle xcode command failure', async () => { + const deps = createDeps({ + xcode: { + async getXcodeInfo() { + return { error: 'Xcode not found' }; + }, + }, + }); + const result = await runDoctor({ enabled: true }, deps); + + expect(result.content).toEqual([ + { + type: 'text', + text: result.content[0].text, + }, + ]); + expect(typeof result.content[0].text).toBe('string'); + }); + + it('should handle xcodemake check failure', async () => { + const deps = createDeps({ + features: { + areAxeToolsAvailable: () => true, + isXcodemakeEnabled: () => true, + isXcodemakeAvailable: async () => false, + doesMakefileExist: () => true, + }, + binaryChecker: { + async checkBinaryAvailability(binary: string) { + if (binary === 'xcodemake') return { available: false }; + return { available: true, version: `${binary} version 1.0.0` }; + }, + }, + }); + const result = await runDoctor({ enabled: true }, deps); + + expect(result.content).toEqual([ + { + type: 'text', + text: result.content[0].text, + }, + ]); + expect(typeof result.content[0].text).toBe('string'); + }); + + it('should handle axe tools not available', async () => { + const deps = createDeps({ + features: { + areAxeToolsAvailable: () => false, + isXcodemakeEnabled: () => false, + isXcodemakeAvailable: async () => false, + doesMakefileExist: () => false, + }, + binaryChecker: { + async checkBinaryAvailability(binary: string) { + if (binary === 'axe') return { available: false }; + if (binary === 'xcodemake') return { available: false }; + if (binary === 'mise') return { available: true, version: 'mise 1.0.0' }; + return { available: true }; + }, + }, + env: { + getEnvironmentVariables() { + const x: Record = { + XCODEBUILDMCP_DEBUG: 'true', + INCREMENTAL_BUILDS_ENABLED: '0', + PATH: '/usr/local/bin:/usr/bin:/bin', + DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', + HOME: '/Users/testuser', + USER: 'testuser', + TMPDIR: '/tmp', + NODE_ENV: 'test', + SENTRY_DISABLED: 'true', + }; + return x; + }, + getSystemInfo: () => ({ + platform: 'darwin', + release: '25.0.0', + arch: 'arm64', + cpus: '10 x Apple M3', + memory: '32 GB', + hostname: 'localhost', + username: 'testuser', + homedir: '/Users/testuser', + tmpdir: '/tmp', + }), + getNodeInfo: () => ({ + version: 'v22.0.0', + execPath: '/usr/local/bin/node', + pid: '123', + ppid: '1', + platform: 'darwin', + arch: 'arm64', + cwd: '/', + argv: 'node build/index.js', + }), + }, + }); + + const result = await runDoctor({ enabled: true }, deps); + + expect(result.content).toEqual([ + { + type: 'text', + text: result.content[0].text, + }, + ]); + expect(typeof result.content[0].text).toBe('string'); + }); + }); +}); diff --git a/src/mcp/tools/diagnostics/__tests__/index.test.ts b/src/mcp/tools/doctor/__tests__/index.test.ts similarity index 83% rename from src/mcp/tools/diagnostics/__tests__/index.test.ts rename to src/mcp/tools/doctor/__tests__/index.test.ts index ee915db6..c373767c 100644 --- a/src/mcp/tools/diagnostics/__tests__/index.test.ts +++ b/src/mcp/tools/doctor/__tests__/index.test.ts @@ -1,10 +1,10 @@ /** - * Tests for diagnostics workflow metadata + * Tests for doctor workflow metadata */ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; -describe('diagnostics workflow metadata', () => { +describe('doctor workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); @@ -14,12 +14,12 @@ describe('diagnostics workflow metadata', () => { }); it('should have correct workflow name', () => { - expect(workflow.name).toBe('System Diagnostics'); + expect(workflow.name).toBe('System Doctor'); }); it('should have correct description', () => { expect(workflow.description).toBe( - 'Debug tools and system diagnostics for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', + 'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', ); }); @@ -29,7 +29,8 @@ describe('diagnostics workflow metadata', () => { it('should have correct capabilities array', () => { expect(workflow.capabilities).toEqual([ - 'diagnostics', + 'doctor', + 'server-diagnostics', 'troubleshooting', 'system-analysis', 'environment-validation', @@ -58,7 +59,8 @@ describe('diagnostics workflow metadata', () => { }); it('should contain expected capability values', () => { - expect(workflow.capabilities).toContain('diagnostics'); + expect(workflow.capabilities).toContain('doctor'); + expect(workflow.capabilities).toContain('server-diagnostics'); expect(workflow.capabilities).toContain('troubleshooting'); expect(workflow.capabilities).toContain('system-analysis'); expect(workflow.capabilities).toContain('environment-validation'); diff --git a/src/mcp/tools/doctor/doctor.ts b/src/mcp/tools/doctor/doctor.ts new file mode 100644 index 00000000..0aaef4b5 --- /dev/null +++ b/src/mcp/tools/doctor/doctor.ts @@ -0,0 +1,278 @@ +/** + * Doctor Plugin: Doctor Tool + * + * Provides comprehensive information about the MCP server environment. + */ + +import { z } from 'zod'; +import { log } from '../../../utils/index.js'; +import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/index.js'; +import { version } from '../../../utils/index.js'; +import { ToolResponse } from '../../../types/common.js'; +import { createTypedTool } from '../../../utils/typed-tool-factory.js'; +import { type DoctorDependencies, createDoctorDependencies } from './lib/doctor.deps.ts'; + +// Constants +const LOG_PREFIX = '[Doctor]'; + +// Define schema as ZodObject +const doctorSchema = z.object({ + enabled: z.boolean().optional().describe('Optional: dummy parameter to satisfy MCP protocol'), +}); + +// Use z.infer for type safety +type DoctorParams = z.infer; + +/** + * Run the doctor tool and return the results + */ +export async function runDoctor( + params: DoctorParams, + deps: DoctorDependencies, + showAsciiLogo = false, +): Promise { + const prevSilence = process.env.XCODEBUILDMCP_SILENCE_LOGS; + process.env.XCODEBUILDMCP_SILENCE_LOGS = 'true'; + log('info', `${LOG_PREFIX}: Running doctor tool`); + + const requiredBinaries = ['axe', 'xcodemake', 'mise']; + const binaryStatus: Record = {}; + for (const binary of requiredBinaries) { + binaryStatus[binary] = await deps.binaryChecker.checkBinaryAvailability(binary); + } + + const xcodeInfo = await deps.xcode.getXcodeInfo(); + const envVars = deps.env.getEnvironmentVariables(); + const systemInfo = deps.env.getSystemInfo(); + const nodeInfo = deps.env.getNodeInfo(); + const axeAvailable = deps.features.areAxeToolsAvailable(); + const pluginSystemInfo = await deps.plugins.getPluginSystemInfo(); + const runtimeInfo = await deps.runtime.getRuntimeToolInfo(); + const xcodemakeEnabled = deps.features.isXcodemakeEnabled(); + const xcodemakeAvailable = await deps.features.isXcodemakeAvailable(); + const makefileExists = deps.features.doesMakefileExist('./'); + + const doctorInfo = { + serverVersion: version, + timestamp: new Date().toISOString(), + system: systemInfo, + node: nodeInfo, + xcode: xcodeInfo, + dependencies: binaryStatus, + environmentVariables: envVars, + features: { + axe: { + available: axeAvailable, + uiAutomationSupported: axeAvailable, + }, + xcodemake: { + enabled: xcodemakeEnabled, + available: xcodemakeAvailable, + makefileExists: makefileExists, + }, + mise: { + running_under_mise: Boolean(process.env.XCODEBUILDMCP_RUNNING_UNDER_MISE), + available: binaryStatus['mise'].available, + }, + }, + pluginSystem: pluginSystemInfo, + } as const; + + // Custom ASCII banner (multiline) + const asciiLogo = ` +██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██╗██╗██╗ ██████╗ ███╗ ███╗ ██████╗██████╗ +╚██╗██╔╝██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██║██║██║ ██╔══██╗████╗ ████║██╔════╝██╔══██╗ + ╚███╔╝ ██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║██║██║ ██║ ██║██╔████╔██║██║ ██████╔╝ + ██╔██╗ ██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║██║██║ ██║ ██║██║╚██╔╝██║██║ ██╔═══╝ +██╔╝ ██╗╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝╚██████╔╝██║███████╗██████╔╝██║ ╚═╝ ██║╚██████╗██║ +╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ + +██████╗ ██████╗ ██████╗████████╗ ██████╗ ██████╗ +██╔══██╗██╔═══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗ +██║ ██║██║ ██║██║ ██║ ██║ ██║██████╔╝ +██║ ██║██║ ██║██║ ██║ ██║ ██║██╔══██╗ +██████╔╝╚██████╔╝╚██████╗ ██║ ╚██████╔╝██║ ██║ +╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ +`; + + const RESET = '\x1b[0m'; + // 256-color: orangey-pink foreground and lighter shade for outlines + const FOREGROUND = '\x1b[38;5;209m'; + const SHADOW = '\x1b[38;5;217m'; + + function colorizeAsciiArt(ascii: string): string { + const lines = ascii.split('\n'); + const coloredLines: string[] = []; + const shadowChars = new Set([ + '╔', + '╗', + '╝', + '╚', + '═', + '║', + '╦', + '╩', + '╠', + '╣', + '╬', + '┌', + '┐', + '└', + '┘', + '│', + '─', + ]); + for (const line of lines) { + let colored = ''; + for (const ch of line) { + if (ch === '█') { + colored += `${FOREGROUND}${ch}${RESET}`; + } else if (shadowChars.has(ch)) { + colored += `${SHADOW}${ch}${RESET}`; + } else { + colored += ch; + } + } + coloredLines.push(colored + RESET); + } + return coloredLines.join('\n'); + } + + const outputLines = []; + + // Only show ASCII logo when explicitly requested (CLI usage) + if (showAsciiLogo) { + outputLines.push(colorizeAsciiArt(asciiLogo)); + } + + outputLines.push( + 'XcodeBuildMCP Doctor', + `\nGenerated: ${doctorInfo.timestamp}`, + `Server Version: ${doctorInfo.serverVersion}`, + ); + + const formattedOutput = [ + ...outputLines, + + `\n## System Information`, + ...Object.entries(doctorInfo.system).map(([key, value]) => `- ${key}: ${value}`), + + `\n## Node.js Information`, + ...Object.entries(doctorInfo.node).map(([key, value]) => `- ${key}: ${value}`), + + `\n## Xcode Information`, + ...('error' in doctorInfo.xcode + ? [`- Error: ${doctorInfo.xcode.error}`] + : Object.entries(doctorInfo.xcode).map(([key, value]) => `- ${key}: ${value}`)), + + `\n## Dependencies`, + ...Object.entries(doctorInfo.dependencies).map( + ([binary, status]) => + `- ${binary}: ${status.available ? `✅ ${status.version ?? 'Available'}` : '❌ Not found'}`, + ), + + `\n## Environment Variables`, + ...Object.entries(doctorInfo.environmentVariables) + .filter(([key]) => key !== 'PATH' && key !== 'PYTHONPATH') // These are too long, handle separately + .map(([key, value]) => `- ${key}: ${value ?? '(not set)'}`), + + `\n### PATH`, + `\`\`\``, + `${doctorInfo.environmentVariables.PATH ?? '(not set)'}`.split(':').join('\n'), + `\`\`\``, + + `\n## Feature Status`, + `\n### UI Automation (axe)`, + `- Available: ${doctorInfo.features.axe.available ? '✅ Yes' : '❌ No'}`, + `- UI Automation Supported: ${doctorInfo.features.axe.uiAutomationSupported ? '✅ Yes' : '❌ No'}`, + + `\n### Incremental Builds`, + `- Enabled: ${doctorInfo.features.xcodemake.enabled ? '✅ Yes' : '❌ No'}`, + `- Available: ${doctorInfo.features.xcodemake.available ? '✅ Yes' : '❌ No'}`, + `- Makefile exists: ${doctorInfo.features.xcodemake.makefileExists ? '✅ Yes' : '❌ No'}`, + + `\n### Mise Integration`, + `- Running under mise: ${doctorInfo.features.mise.running_under_mise ? '✅ Yes' : '❌ No'}`, + `- Mise available: ${doctorInfo.features.mise.available ? '✅ Yes' : '❌ No'}`, + + `\n### Available Tools`, + `- Total Plugins: ${'totalPlugins' in doctorInfo.pluginSystem ? doctorInfo.pluginSystem.totalPlugins : 0}`, + `- Plugin Directories: ${'pluginDirectories' in doctorInfo.pluginSystem ? doctorInfo.pluginSystem.pluginDirectories : 0}`, + ...('pluginsByDirectory' in doctorInfo.pluginSystem && + doctorInfo.pluginSystem.pluginDirectories > 0 + ? Object.entries(doctorInfo.pluginSystem.pluginsByDirectory).map( + ([dir, tools]) => `- ${dir}: ${Array.isArray(tools) ? tools.length : 0} tools`, + ) + : ['- Plugin directory grouping unavailable in this build']), + + `\n### Runtime Tool Registration`, + `- Mode: ${runtimeInfo.mode}`, + `- Enabled Workflows: ${runtimeInfo.enabledWorkflows.length}`, + `- Registered Tools: ${runtimeInfo.totalRegistered}`, + ...(runtimeInfo.enabledWorkflows.length > 0 + ? [`- Workflows: ${runtimeInfo.enabledWorkflows.join(', ')}`] + : []), + + `\n## Tool Availability Summary`, + `- Build Tools: ${!('error' in doctorInfo.xcode) ? '\u2705 Available' : '\u274c Not available'}`, + `- UI Automation Tools: ${doctorInfo.features.axe.uiAutomationSupported ? '\u2705 Available' : '\u274c Not available'}`, + `- Incremental Build Support: ${doctorInfo.features.xcodemake.available && doctorInfo.features.xcodemake.enabled ? '\u2705 Available & Enabled' : doctorInfo.features.xcodemake.available ? '\u2705 Available but Disabled' : '\u274c Not available'}`, + + `\n## Sentry`, + `- Sentry enabled: ${doctorInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`, + + `\n## Troubleshooting Tips`, + `- If UI automation tools are not available, install axe: \`brew tap cameroncooke/axe && brew install axe\``, + `- If incremental build support is not available, you can download the tool from https://github.com/cameroncooke/xcodemake. Make sure it's executable and available in your PATH`, + `- To enable xcodemake, set environment variable: \`export INCREMENTAL_BUILDS_ENABLED=1\``, + `- For mise integration, follow instructions in the README.md file`, + ...(process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true' + ? [ + `- Dynamic mode is enabled. Use 'discover_tools' to enable workflows relevant to your task`, + ] + : []), + ].join('\n'); + + const result: ToolResponse = { + content: [ + { + type: 'text', + text: formattedOutput, + }, + ], + }; + // Restore previous silence flag + if (prevSilence === undefined) { + delete process.env.XCODEBUILDMCP_SILENCE_LOGS; + } else { + process.env.XCODEBUILDMCP_SILENCE_LOGS = prevSilence; + } + return result; +} + +export async function doctorLogic( + params: DoctorParams, + executor: CommandExecutor, + showAsciiLogo = false, +): Promise { + const deps = createDoctorDependencies(executor); + return runDoctor(params, deps, showAsciiLogo); +} + +// MCP wrapper that ensures ASCII logo is never shown for MCP server calls +async function doctorMcpHandler( + params: DoctorParams, + executor: CommandExecutor, +): Promise { + return doctorLogic(params, executor, false); // Always false for MCP +} + +export default { + name: 'doctor', + description: + 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', + schema: doctorSchema.shape, // MCP SDK compatibility + handler: createTypedTool(doctorSchema, doctorMcpHandler, getDefaultCommandExecutor), +}; + +export type { DoctorDependencies } from './lib/doctor.deps.ts'; diff --git a/src/mcp/tools/doctor/index.ts b/src/mcp/tools/doctor/index.ts new file mode 100644 index 00000000..9eff7aa0 --- /dev/null +++ b/src/mcp/tools/doctor/index.ts @@ -0,0 +1,13 @@ +export const workflow = { + name: 'System Doctor', + description: + 'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.', + platforms: ['system'], + capabilities: [ + 'doctor', + 'server-diagnostics', + 'troubleshooting', + 'system-analysis', + 'environment-validation', + ], +}; diff --git a/src/mcp/tools/doctor/lib/doctor.deps.ts b/src/mcp/tools/doctor/lib/doctor.deps.ts new file mode 100644 index 00000000..ff8c3206 --- /dev/null +++ b/src/mcp/tools/doctor/lib/doctor.deps.ts @@ -0,0 +1,282 @@ +import * as os from 'os'; +import { + CommandExecutor, + loadWorkflowGroups, + loadPlugins, + areAxeToolsAvailable, + isXcodemakeEnabled, + isXcodemakeAvailable, + doesMakefileExist, + getEnabledWorkflows, +} from '../../../../utils/index.js'; +import { getTrackedToolNames } from '../../../../utils/tool-registry.js'; + +export interface BinaryChecker { + checkBinaryAvailability(binary: string): Promise<{ available: boolean; version?: string }>; +} + +export interface XcodeInfoProvider { + getXcodeInfo(): Promise< + | { version: string; path: string; selectedXcode: string; xcrunVersion: string } + | { error: string } + >; +} + +export interface EnvironmentInfoProvider { + getEnvironmentVariables(): Record; + getSystemInfo(): { + platform: string; + release: string; + arch: string; + cpus: string; + memory: string; + hostname: string; + username: string; + homedir: string; + tmpdir: string; + }; + getNodeInfo(): { + version: string; + execPath: string; + pid: string; + ppid: string; + platform: string; + arch: string; + cwd: string; + argv: string; + }; +} + +export interface PluginInfoProvider { + getPluginSystemInfo(): Promise< + | { + totalPlugins: number; + pluginDirectories: number; + pluginsByDirectory: Record; + systemMode: string; + } + | { error: string; systemMode: string } + >; +} + +export interface RuntimeInfoProvider { + getRuntimeToolInfo(): Promise< + | { + mode: 'dynamic'; + enabledWorkflows: string[]; + enabledTools: string[]; + totalRegistered: number; + } + | { + mode: 'static'; + enabledWorkflows: string[]; + enabledTools: string[]; + totalRegistered: number; + } + >; +} + +export interface FeatureDetector { + areAxeToolsAvailable(): boolean; + isXcodemakeEnabled(): boolean; + isXcodemakeAvailable(): Promise; + doesMakefileExist(path: string): boolean; +} + +export interface DoctorDependencies { + binaryChecker: BinaryChecker; + xcode: XcodeInfoProvider; + env: EnvironmentInfoProvider; + plugins: PluginInfoProvider; + runtime: RuntimeInfoProvider; + features: FeatureDetector; +} + +export function createDoctorDependencies(executor: CommandExecutor): DoctorDependencies { + const binaryChecker: BinaryChecker = { + async checkBinaryAvailability(binary: string) { + // If bundled axe is available, reflect that in dependencies even if not on PATH + if (binary === 'axe' && areAxeToolsAvailable()) { + return { available: true, version: 'Bundled' }; + } + try { + const which = await executor(['which', binary], 'Check Binary Availability'); + if (!which.success) { + return { available: false }; + } + } catch { + return { available: false }; + } + + let version: string | undefined; + const versionCommands: Record = { + axe: 'axe --version', + mise: 'mise --version', + }; + + if (binary in versionCommands) { + try { + const res = await executor(versionCommands[binary]!.split(' '), 'Get Binary Version'); + if (res.success && res.output) { + version = res.output.trim(); + } + } catch { + // ignore + } + } + + return { available: true, version: version ?? 'Available (version info not available)' }; + }, + }; + + const xcode: XcodeInfoProvider = { + async getXcodeInfo() { + try { + const xcodebuild = await executor(['xcodebuild', '-version'], 'Get Xcode Version'); + if (!xcodebuild.success) throw new Error('xcodebuild command failed'); + const version = xcodebuild.output.trim().split('\n').slice(0, 2).join(' - '); + + const pathRes = await executor(['xcode-select', '-p'], 'Get Xcode Path'); + if (!pathRes.success) throw new Error('xcode-select command failed'); + const path = pathRes.output.trim(); + + const selected = await executor(['xcrun', '--find', 'xcodebuild'], 'Find Xcodebuild'); + if (!selected.success) throw new Error('xcrun --find command failed'); + const selectedXcode = selected.output.trim(); + + const xcrun = await executor(['xcrun', '--version'], 'Get Xcrun Version'); + if (!xcrun.success) throw new Error('xcrun --version command failed'); + const xcrunVersion = xcrun.output.trim(); + + return { version, path, selectedXcode, xcrunVersion }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } + }, + }; + + const env: EnvironmentInfoProvider = { + getEnvironmentVariables() { + const relevantVars = [ + 'INCREMENTAL_BUILDS_ENABLED', + 'PATH', + 'DEVELOPER_DIR', + 'HOME', + 'USER', + 'TMPDIR', + 'NODE_ENV', + 'SENTRY_DISABLED', + ]; + + const envVars: Record = {}; + for (const varName of relevantVars) { + envVars[varName] = process.env[varName]; + } + + Object.keys(process.env).forEach((key) => { + if (key.startsWith('XCODEBUILDMCP_')) { + envVars[key] = process.env[key]; + } + }); + + return envVars; + }, + + getSystemInfo() { + return { + platform: os.platform(), + release: os.release(), + arch: os.arch(), + cpus: `${os.cpus().length} x ${os.cpus()[0]?.model ?? 'Unknown'}`, + memory: `${Math.round(os.totalmem() / (1024 * 1024 * 1024))} GB`, + hostname: os.hostname(), + username: os.userInfo().username, + homedir: os.homedir(), + tmpdir: os.tmpdir(), + }; + }, + + getNodeInfo() { + return { + version: process.version, + execPath: process.execPath, + pid: process.pid.toString(), + ppid: process.ppid.toString(), + platform: process.platform, + arch: process.arch, + cwd: process.cwd(), + argv: process.argv.join(' '), + }; + }, + }; + + const plugins: PluginInfoProvider = { + async getPluginSystemInfo() { + try { + const workflows = await loadWorkflowGroups(); + const pluginsByDirectory: Record = {}; + let totalPlugins = 0; + + for (const [dirName, wf] of workflows.entries()) { + const toolNames = wf.tools.map((t) => t.name).filter(Boolean) as string[]; + totalPlugins += toolNames.length; + pluginsByDirectory[dirName] = toolNames; + } + + return { + totalPlugins, + pluginDirectories: workflows.size, + pluginsByDirectory, + systemMode: 'plugin-based', + }; + } catch (error) { + return { + error: `Failed to load plugins: ${error instanceof Error ? error.message : 'Unknown error'}`, + systemMode: 'error', + }; + } + }, + }; + + const runtime: RuntimeInfoProvider = { + async getRuntimeToolInfo() { + const dynamic = process.env.XCODEBUILDMCP_DYNAMIC_TOOLS === 'true'; + + if (dynamic) { + const enabledWf = getEnabledWorkflows(); + const enabledTools = getTrackedToolNames(); + return { + mode: 'dynamic', + enabledWorkflows: enabledWf, + enabledTools, + totalRegistered: enabledTools.length, + }; + } + + // Static mode: all tools are registered + const workflows = await loadWorkflowGroups(); + const enabledWorkflows = Array.from(workflows.keys()); + const plugins = await loadPlugins(); + const enabledTools = Array.from(plugins.keys()); + return { + mode: 'static', + enabledWorkflows, + enabledTools, + totalRegistered: enabledTools.length, + }; + }, + }; + + const features: FeatureDetector = { + areAxeToolsAvailable, + isXcodemakeEnabled, + isXcodemakeAvailable, + doesMakefileExist, + }; + + return { binaryChecker, xcode, env, plugins, runtime, features }; +} + +export type { CommandExecutor }; + +export default {} as const; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 3d973f64..aca0760a 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -32,7 +32,11 @@ if (!SENTRY_ENABLED) { */ export function log(level: string, message: string): void { // Suppress logging during tests to keep test output clean - if (process.env.VITEST === 'true' || process.env.NODE_ENV === 'test') { + if ( + process.env.VITEST === 'true' || + process.env.NODE_ENV === 'test' || + process.env.XCODEBUILDMCP_SILENCE_LOGS === 'true' + ) { return; } diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts index 852b879d..a7ff0476 100644 --- a/src/utils/sentry.ts +++ b/src/utils/sentry.ts @@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node'; import { version } from '../version.js'; import { execSync } from 'child_process'; -// Inlined diagnostic functions to avoid circular dependencies +// Inlined system info functions to avoid circular dependencies function getXcodeInfo(): { version: string; path: string; selectedXcode: string; error?: string } { try { const xcodebuildOutput = execSync('xcodebuild -version', { encoding: 'utf8' }).trim(); @@ -32,6 +32,7 @@ function getEnvironmentVariables(): Record { const relevantVars = [ 'XCODEBUILDMCP_DEBUG', 'INCREMENTAL_BUILDS_ENABLED', + 'XCODEBUILDMCP_DYNAMIC_TOOLS', 'PATH', 'DEVELOPER_DIR', 'HOME', diff --git a/tsup.config.ts b/tsup.config.ts index 79dd9357..304b59e3 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -5,7 +5,7 @@ import { createPluginDiscoveryPlugin } from './build-plugins/plugin-discovery.js export default defineConfig({ entry: { index: 'src/index.ts', - 'diagnostic-cli': 'src/diagnostic-cli.ts', + 'doctor-cli': 'src/doctor-cli.ts', }, format: ['esm'], target: 'node18', @@ -25,13 +25,13 @@ export default defineConfig({ esbuildPlugins: [createPluginDiscoveryPlugin()], onSuccess: async () => { console.log('✅ Build complete!'); - + // Set executable permissions for built files if (existsSync('build/index.js')) { chmodSync('build/index.js', '755'); } - if (existsSync('build/diagnostic-cli.js')) { - chmodSync('build/diagnostic-cli.js', '755'); + if (existsSync('build/doctor-cli.js')) { + chmodSync('build/doctor-cli.js', '755'); } }, }); \ No newline at end of file