From 534aaa837e93c7293bee6263a8df2fb164fbc7e4 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 14 May 2025 22:48:06 +0100 Subject: [PATCH] Refactor command execution to use unified executeCommand function and update documentation --- .windsurf/workflows/build-app.md | 4 +- src/tools/app_path.ts | 5 +- src/tools/build_ios_device.ts | 6 +- src/tools/build_ios_simulator.ts | 9 ++- src/tools/build_macos.ts | 9 ++- src/tools/build_settings.ts | 6 +- src/tools/clean.ts | 4 +- src/tools/idb.ts | 102 +++++++++---------------- src/tools/simulator.ts | 16 ++-- src/types/common.ts | 5 -- src/utils/build-utils.ts | 7 +- src/utils/command.ts | 47 ++++++++++-- src/utils/xcode.ts | 70 +----------------- src/utils/xcodemake.ts | 123 +++---------------------------- 14 files changed, 120 insertions(+), 293 deletions(-) diff --git a/.windsurf/workflows/build-app.md b/.windsurf/workflows/build-app.md index b7f2563d..55d56bff 100644 --- a/.windsurf/workflows/build-app.md +++ b/.windsurf/workflows/build-app.md @@ -2,6 +2,6 @@ description: Build and run the app on the iOS simulator --- -1. Build the app +1. Build the iOS app 2. Run the app on the simulator -3. Only use tools exposed by XcodeBuildMCP \ No newline at end of file +3. Only use tools exposed by "xcode-mcp-server-dev" \ No newline at end of file diff --git a/src/tools/app_path.ts b/src/tools/app_path.ts index 3d0e0d8f..6995e3b9 100644 --- a/src/tools/app_path.ts +++ b/src/tools/app_path.ts @@ -16,7 +16,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { log } from '../utils/logger.js'; import { validateRequiredParam, createTextResponse } from '../utils/validation.js'; import { ToolResponse, XcodePlatform } from '../types/common.js'; -import { executeXcodeCommand, constructDestinationString } from '../utils/xcode.js'; +import { executeCommand } from '../utils/command.js'; +import { constructDestinationString } from '../utils/xcode.js'; import { registerTool, workspacePathSchema, @@ -117,7 +118,7 @@ async function _handleGetAppPathLogic(params: { command.push('-destination', destinationString); // Execute the command directly - const result = await executeXcodeCommand(command, 'Get App Path'); + const result = await executeCommand(command, 'Get App Path'); if (!result.success) { return createTextResponse(`Failed to get app path: ${result.error}`, true); diff --git a/src/tools/build_ios_device.ts b/src/tools/build_ios_device.ts index 41275d30..64546fba 100644 --- a/src/tools/build_ios_device.ts +++ b/src/tools/build_ios_device.ts @@ -14,7 +14,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { XcodePlatform } from '../utils/xcode.js'; import { validateRequiredParam } from '../utils/validation.js'; -import { executeXcodeBuild } from '../utils/build-utils.js'; +import { executeXcodeBuildCommand } from '../utils/build-utils.js'; import { registerTool, workspacePathSchema, @@ -54,7 +54,7 @@ export function registerIOSDeviceBuildWorkspaceTool(server: McpServer): void { const schemeValidation = validateRequiredParam('scheme', params.scheme); if (!schemeValidation.isValid) return schemeValidation.errorResponse!; - return executeXcodeBuild( + return executeXcodeBuildCommand( { ...params, configuration: params.configuration ?? 'Debug', // Default config @@ -94,7 +94,7 @@ export function registerIOSDeviceBuildProjectTool(server: McpServer): void { const schemeValidation = validateRequiredParam('scheme', params.scheme); if (!schemeValidation.isValid) return schemeValidation.errorResponse!; - return executeXcodeBuild( + return executeXcodeBuildCommand( { ...params, configuration: params.configuration ?? 'Debug', // Default config diff --git a/src/tools/build_ios_simulator.ts b/src/tools/build_ios_simulator.ts index 3eb3a762..f5539a15 100644 --- a/src/tools/build_ios_simulator.ts +++ b/src/tools/build_ios_simulator.ts @@ -14,10 +14,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { log } from '../utils/logger.js'; -import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js'; +import { XcodePlatform } from '../utils/xcode.js'; +import { executeCommand } from '../utils/command.js'; import { validateRequiredParam, createTextResponse } from '../utils/validation.js'; import { ToolResponse } from '../types/common.js'; -import { executeXcodeBuild } from '../utils/build-utils.js'; +import { executeXcodeBuildCommand } from '../utils/build-utils.js'; import { registerTool, workspacePathSchema, @@ -52,7 +53,7 @@ async function _handleIOSSimulatorBuildLogic(params: { }): Promise { log('info', `Starting iOS Simulator build for scheme ${params.scheme} (internal)`); - return executeXcodeBuild( + return executeXcodeBuildCommand( { ...params, }, @@ -134,7 +135,7 @@ async function _handleIOSSimulatorBuildAndRunLogic(params: { } // Execute the command directly - const result = await executeXcodeCommand(command, 'Get App Path'); + const result = await executeCommand(command, 'Get App Path'); // If there was an error with the command execution, return it if (!result.success) { diff --git a/src/tools/build_macos.ts b/src/tools/build_macos.ts index cee6c8b5..e1a48bda 100644 --- a/src/tools/build_macos.ts +++ b/src/tools/build_macos.ts @@ -16,10 +16,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { log } from '../utils/logger.js'; -import { XcodePlatform, executeXcodeCommand } from '../utils/xcode.js'; +import { XcodePlatform } from '../utils/xcode.js'; +import { executeCommand } from '../utils/command.js'; import { createTextResponse } from '../utils/validation.js'; import { ToolResponse } from '../types/common.js'; -import { executeXcodeBuild } from '../utils/build-utils.js'; +import { executeXcodeBuildCommand } from '../utils/build-utils.js'; import { z } from 'zod'; import { registerTool, @@ -55,7 +56,7 @@ async function _handleMacOSBuildLogic(params: { }): Promise { log('info', `Starting macOS build for scheme ${params.scheme} (internal)`); - return executeXcodeBuild( + return executeXcodeBuildCommand( { ...params, }, @@ -104,7 +105,7 @@ async function _getAppPathFromBuildSettings(params: { } // Execute the command directly - const result = await executeXcodeCommand(command, 'Get Build Settings for Launch'); + const result = await executeCommand(command, 'Get Build Settings for Launch'); if (!result.success) { return { diff --git a/src/tools/build_settings.ts b/src/tools/build_settings.ts index c742f019..b03f2bab 100644 --- a/src/tools/build_settings.ts +++ b/src/tools/build_settings.ts @@ -14,7 +14,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { log } from '../utils/logger.js'; -import { executeXcodeCommand } from '../utils/xcode.js'; +import { executeCommand } from '../utils/command.js'; import { validateRequiredParam, createTextResponse } from '../utils/validation.js'; import { ToolResponse } from '../types/common.js'; import { @@ -53,7 +53,7 @@ async function _handleShowBuildSettingsLogic(params: { command.push('-scheme', params.scheme); // Execute the command directly - const result = await executeXcodeCommand(command, 'Show Build Settings'); + const result = await executeCommand(command, 'Show Build Settings'); if (!result.success) { return createTextResponse(`Failed to show build settings: ${result.error}`, true); @@ -98,7 +98,7 @@ async function _handleListSchemesLogic(params: { command.push('-project', params.projectPath); } // No else needed, one path is guaranteed by callers - const result = await executeXcodeCommand(command, 'List Schemes'); + const result = await executeCommand(command, 'List Schemes'); if (!result.success) { return createTextResponse(`Failed to list schemes: ${result.error}`, true); diff --git a/src/tools/clean.ts b/src/tools/clean.ts index 08e9ac2f..b1d7b56d 100644 --- a/src/tools/clean.ts +++ b/src/tools/clean.ts @@ -17,7 +17,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { log } from '../utils/logger.js'; import { XcodePlatform } from '../utils/xcode.js'; import { ToolResponse } from '../types/common.js'; -import { executeXcodeBuild } from '../utils/build-utils.js'; +import { executeXcodeBuildCommand } from '../utils/build-utils.js'; // --- Private Helper Function --- @@ -35,7 +35,7 @@ async function _handleCleanLogic(params: { log('info', 'Starting xcodebuild clean request (internal)'); // For clean operations, we need to provide a default platform and configuration - return executeXcodeBuild( + return executeXcodeBuildCommand( { ...params, scheme: params.scheme || '', // Empty string if not provided diff --git a/src/tools/idb.ts b/src/tools/idb.ts index 63d20c7d..fdb5fcf0 100644 --- a/src/tools/idb.ts +++ b/src/tools/idb.ts @@ -8,7 +8,6 @@ import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs/promises'; -import { spawn } from 'child_process'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid'; @@ -16,6 +15,7 @@ import { ToolResponse } from '../types/common.js'; import { log } from '../utils/logger.js'; import { createTextResponse, validateRequiredParam } from '../utils/validation.js'; import { DependencyError, IdbError, SystemError, createErrorResponse } from '../utils/errors.js'; +import { executeCommand } from '../utils/command.js'; import { createIdbNotAvailableResponse } from '../utils/idb-setup.js'; import { areIdbToolsAvailable } from '../utils/idb-setup.js'; @@ -43,79 +43,43 @@ async function executeIdbCommand( } } - const commandString = `${IDB_COMMAND} ${fullArgs.join(' ')}`; - log('info', `${LOG_PREFIX}: Executing: ${commandString}`); - - return new Promise((resolve, reject) => { - let stdoutData = ''; - let stderrData = ''; - let processError: Error | null = null; - - try { - const idbProcess = spawn(IDB_COMMAND, fullArgs, { - shell: false, // Use direct execution, assumes idb is in PATH - }); - - idbProcess.stdout.on('data', (data) => { - stdoutData += data.toString(); - }); - - idbProcess.stderr.on('data', (data) => { - stderrData += data.toString(); - }); - - idbProcess.on('error', (err) => { - log('error', `${LOG_PREFIX}: Failed to spawn idb: ${err.message}`); - processError = err; - // reject will be called in 'close' handler - }); - - idbProcess.on('close', (code) => { - log('debug', `${LOG_PREFIX}: Command "${commandName}" exited with code ${code}`); - log('debug', `${LOG_PREFIX}: stdout:\n${stdoutData}`); - if (stderrData) { - log('warning', `${LOG_PREFIX}: stderr:\n${stderrData}`); - } + // Construct the full command array with IDB_COMMAND as the first element + const fullCommand = [IDB_COMMAND, ...fullArgs]; - if (processError) { - return reject( - new SystemError(`Failed to start idb command: ${processError.message}`, processError), - ); - } + try { + const result = await executeCommand(fullCommand, `${LOG_PREFIX}: ${commandName}`, false); - if (code !== 0) { - return reject( - new IdbError( - `idb command '${commandName}' failed with exit code ${code}.`, - commandName, - stderrData || stdoutData, // Provide output for context - simulatorUuid, - ), - ); - } - - // Some idb commands might print warnings or non-fatal errors to stderr. - // We resolve successfully but log the stderr. Consider if specific commands - // should treat stderr as a failure. For now, only non-zero exit code is fatal. - if (stderrData) { - log( - 'warn', - `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${stderrData}`, - ); - } + if (!result.success) { + throw new IdbError( + `idb command '${commandName}' failed.`, + commandName, + result.error || result.output, + simulatorUuid, + ); + } - resolve(stdoutData.trim()); - }); - } catch (error) { - // Catch synchronous errors during spawn setup - log('error', `${LOG_PREFIX}: Error setting up idb spawn: ${error}`); - reject( - new SystemError( - `Failed to initiate idb command execution: ${error instanceof Error ? error.message : String(error)}`, - ), + // Check for stderr output in successful commands + if (result.error) { + log( + 'warn', + `${LOG_PREFIX}: Command '${commandName}' produced stderr output but exited successfully. Output: ${result.error}`, ); } - }); + + return result.output.trim(); + } catch (error) { + if (error instanceof Error) { + if (error instanceof IdbError) { + throw error; + } + + // Otherwise wrap it in a SystemError + throw new SystemError(`Failed to execute idb command: ${error.message}`, error); + } + + // For any other type of error + throw new SystemError(`Failed to execute idb command: ${String(error)}`); + } } // --- Registration Function --- diff --git a/src/tools/simulator.ts b/src/tools/simulator.ts index 8751be81..2708fb2d 100644 --- a/src/tools/simulator.ts +++ b/src/tools/simulator.ts @@ -17,7 +17,7 @@ import { z } from 'zod'; import { execSync } from 'child_process'; import { log } from '../utils/logger.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { executeXcodeCommand } from '../utils/xcode.js'; +import { executeCommand } from '../utils/command.js'; import { validateRequiredParam, validateFileExists } from '../utils/validation.js'; import { ToolResponse } from '../types/common.js'; import { createTextContent } from './common.js'; @@ -45,7 +45,7 @@ export function registerBootSimulatorTool(server: McpServer): void { try { const command = ['xcrun', 'simctl', 'boot', params.simulatorUuid]; - const result = await executeXcodeCommand(command, 'Boot Simulator'); + const result = await executeCommand(command, 'Boot Simulator'); if (!result.success) { return { @@ -104,7 +104,7 @@ export function registerListSimulatorsTool(server: McpServer): void { try { const command = ['xcrun', 'simctl', 'list', 'devices', 'available', '--json']; - const result = await executeXcodeCommand(command, 'List Simulators'); + const result = await executeCommand(command, 'List Simulators'); if (!result.success) { return { @@ -211,7 +211,7 @@ export function registerInstallAppInSimulatorTool(server: McpServer): void { try { const command = ['xcrun', 'simctl', 'install', params.simulatorUuid, params.appPath]; - const result = await executeXcodeCommand(command, 'Install App in Simulator'); + const result = await executeCommand(command, 'Install App in Simulator'); if (!result.success) { return { @@ -299,7 +299,7 @@ export function registerLaunchAppInSimulatorTool(server: McpServer): void { params.bundleId, 'app', ]; - const getAppContainerResult = await executeXcodeCommand( + const getAppContainerResult = await executeCommand( getAppContainerCmd, 'Check App Installed', ); @@ -333,7 +333,7 @@ export function registerLaunchAppInSimulatorTool(server: McpServer): void { command.push(...params.args); } - const result = await executeXcodeCommand(command, 'Launch App in Simulator'); + const result = await executeCommand(command, 'Launch App in Simulator'); if (!result.success) { return { @@ -446,7 +446,7 @@ export function registerOpenSimulatorTool(server: McpServer): void { try { const command = ['open', '-a', 'Simulator']; - const result = await executeXcodeCommand(command, 'Open Simulator'); + const result = await executeCommand(command, 'Open Simulator'); if (!result.success) { return { @@ -523,7 +523,7 @@ async function executeSimctlCommandAndRespond( try { const command = ['xcrun', 'simctl', ...simctlSubCommand]; - const result = await executeXcodeCommand(command, operationDescriptionForXcodeCommand); + const result = await executeCommand(command, operationDescriptionForXcodeCommand); if (!result.success) { const fullFailureMessage = `${failureMessagePrefix}: ${result.error}`; diff --git a/src/types/common.ts b/src/types/common.ts index 4b2ae8f8..d2f3ccd2 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -72,11 +72,6 @@ export interface CommandResponse { error?: string; } -/** - * XcodeCommandResponse - Result of xcodebuild command execution - */ -export type XcodeCommandResponse = CommandResponse; - /** * Interface for shared build parameters */ diff --git a/src/utils/build-utils.ts b/src/utils/build-utils.ts index fde65027..87ea4d8a 100644 --- a/src/utils/build-utils.ts +++ b/src/utils/build-utils.ts @@ -18,7 +18,8 @@ */ import { log } from './logger.js'; -import { executeXcodeCommand, XcodePlatform, constructDestinationString } from './xcode.js'; +import { XcodePlatform, constructDestinationString } from './xcode.js'; +import { executeCommand } from './command.js'; import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.js'; import { createTextResponse } from './validation.js'; import { @@ -39,7 +40,7 @@ import path from 'path'; * @param buildAction The xcodebuild action to perform (e.g., 'build', 'clean', 'test') * @returns Promise resolving to tool response */ -export async function executeXcodeBuild( +export async function executeXcodeBuildCommand( params: SharedBuildParams, platformOptions: PlatformBuildOptions, preferXcodebuild: boolean = false, @@ -206,7 +207,7 @@ export async function executeXcodeBuild( } } else { // Use standard xcodebuild - result = await executeXcodeCommand(command, platformOptions.logPrefix); + result = await executeCommand(command, platformOptions.logPrefix); } // Grep warnings and errors from stdout (build output) diff --git a/src/utils/command.ts b/src/utils/command.ts index c777c6dc..3ba4f626 100644 --- a/src/utils/command.ts +++ b/src/utils/command.ts @@ -9,7 +9,7 @@ * - Managing process spawning, output capture, and error handling */ -import { spawn } from 'child_process'; +import { spawn, ChildProcess } from 'child_process'; import { log } from './logger.js'; /** @@ -19,18 +19,50 @@ export interface CommandResponse { success: boolean; output: string; error?: string; + process: ChildProcess; } /** - * Execute a shell command - * @param command Command string to execute - * @returns Promise resolving to command response + * Execute a command + * @param command An array of command and arguments + * @param logPrefix Prefix for logging + * @param useShell Whether to use shell execution (true) or direct execution (false) + * @returns Promise resolving to command response with the process */ -export async function executeCommand(command: string): Promise { - log('info', `Executing command: ${command}`); +export async function executeCommand( + command: string[], + logPrefix?: string, + useShell: boolean = true, +): Promise { + // Properly escape arguments for shell + let escapedCommand = command; + if (useShell) { + // For shell execution, we need to format as ['sh', '-c', 'full command string'] + const commandString = command + .map((arg) => { + // If the argument contains spaces or special characters, wrap it in quotes + // Ensure existing quotes are escaped + if (/[\s,"'=]/.test(arg) && !/^".*"$/.test(arg)) { + // Check if needs quoting and isn't already quoted + return `"${arg.replace(/(["\\])/g, '\\$1')}"`; // Escape existing quotes and backslashes + } + return arg; + }) + .join(' '); + + escapedCommand = ['sh', '-c', commandString]; + } + + // Log the actual command that will be executed + const displayCommand = + useShell && escapedCommand.length === 3 ? escapedCommand[2] : escapedCommand.join(' '); + log('info', `Executing ${logPrefix || ''} command: ${displayCommand}`); return new Promise((resolve, reject) => { - const process = spawn('sh', ['-c', command], { + const executable = escapedCommand[0]; + const args = escapedCommand.slice(1); + + const process = spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr }); @@ -51,6 +83,7 @@ export async function executeCommand(command: string): Promise success, output: stdout, error: success ? undefined : stderr, + process, }; resolve(response); diff --git a/src/utils/xcode.ts b/src/utils/xcode.ts index 1c4afb55..9dc663fd 100644 --- a/src/utils/xcode.ts +++ b/src/utils/xcode.ts @@ -2,86 +2,22 @@ * Xcode Utilities - Core infrastructure for interacting with Xcode tools * * This utility module provides the foundation for all Xcode interactions across the codebase. - * It offers low-level command execution, platform-specific utilities, and common functionality - * that can be used by any module requiring Xcode tool integration. + * It offers platform-specific utilities, and common functionality that can be used by any module + * requiring Xcode tool integration. * * Responsibilities: - * - Executing xcodebuild commands with proper argument handling (executeXcodeCommand) - * - Managing process spawning, output capture, and error handling * - Constructing platform-specific destination strings (constructDestinationString) - * - Defining common parameter interfaces for Xcode operations - * * This file serves as the foundation layer for more specialized utilities like build-utils.ts, * which build upon these core functions to provide higher-level abstractions. */ -import { spawn } from 'child_process'; import { log } from './logger.js'; -import { XcodePlatform, XcodeCommandResponse } from '../types/common.js'; +import { XcodePlatform } from '../types/common.js'; // Re-export XcodePlatform for use in other modules export { XcodePlatform }; -/** - * Execute an xcodebuild command - * @param command Command array to execute - * @param logPrefix Prefix for logging - * @returns Promise resolving to command response - */ -export async function executeXcodeCommand( - command: string[], - logPrefix: string, -): Promise { - // Properly escape arguments for shell - const escapedCommand = command.map((arg) => { - // If the argument contains spaces or special characters, wrap it in quotes - // Ensure existing quotes are escaped - if (/[\s,"'=]/.test(arg) && !/^".*"$/.test(arg)) { - // Check if needs quoting and isn't already quoted - return `"${arg.replace(/(["\\])/g, '\\$1')}"`; // Escape existing quotes and backslashes - } - return arg; - }); - - const commandString = escapedCommand.join(' '); - log('info', `Executing ${logPrefix} command: ${commandString}`); - log('debug', `DEBUG - Raw command array: ${JSON.stringify(command)}`); - - return new Promise((resolve, reject) => { - // Using 'sh -c' to handle complex commands and quoting properly - const process = spawn('sh', ['-c', commandString], { - stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr - }); - - let stdout = ''; - let stderr = ''; - - process.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - process.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - process.on('close', (code) => { - const success = code === 0; - const response: XcodeCommandResponse = { - success, - output: stdout, - error: success ? undefined : stderr, - }; - - resolve(response); - }); - - process.on('error', (err) => { - reject(err); - }); - }); -} - /** * Constructs a destination string for xcodebuild from platform and simulator parameters * @param platform The target platform diff --git a/src/utils/xcodemake.ts b/src/utils/xcodemake.ts index 87277b5b..54797ecc 100644 --- a/src/utils/xcodemake.ts +++ b/src/utils/xcodemake.ts @@ -13,11 +13,9 @@ * - Auto-downloading xcodemake if enabled but not found */ -import { spawn, exec } from 'child_process'; import { log } from './logger.js'; -import { XcodeCommandResponse, CommandResponse } from '../types/common.js'; +import { executeCommand, CommandResponse } from './command.js'; import { existsSync, readdirSync } from 'fs'; -import { promisify } from 'util'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs/promises'; @@ -37,32 +35,6 @@ export function isXcodemakeEnabled(): boolean { return envValue === '1' || envValue === 'true' || envValue === 'yes'; } -/** - * Execute a shell command - * @param command Command string to execute - * @returns Promise resolving to command response - */ -async function executeCommand(command: string): Promise { - log('info', `Executing command: ${command}`); - const execPromise = promisify(exec); - - try { - const { stdout, stderr } = await execPromise(command); - return { - success: true, - output: stdout, - error: stderr.length > 0 ? stderr : undefined, - }; - } catch (error) { - const err = error as { message: string; stderr?: string }; - return { - success: false, - output: '', - error: err.stderr || err.message, - }; - } -} - /** * Get the xcodemake command to use * @returns The command string for xcodemake @@ -144,7 +116,7 @@ export async function isXcodemakeAvailable(): Promise { } // Check if xcodemake is available in PATH - const result = await executeCommand('which xcodemake'); + const result = await executeCommand(['which', 'xcodemake']); if (result.success) { log('debug', 'xcodemake found in PATH'); return true; @@ -232,61 +204,16 @@ export async function executeXcodemakeCommand( projectDir: string, buildArgs: string[], logPrefix: string, -): Promise { +): Promise { // Change directory to project directory, this is needed for xcodemake to work process.chdir(projectDir); const xcodemakeCommand = [getXcodemakeCommand(), ...buildArgs]; - // Properly escape arguments for shell - const escapedCommand = xcodemakeCommand.map((arg) => { - // Remove projectDir from arguments - arg = arg.replace(projectDir + '/', ''); + // Remove projectDir from arguments + const command = xcodemakeCommand.map((arg) => arg.replace(projectDir + '/', '')); - // If the argument contains spaces or special characters, wrap it in quotes - // Ensure existing quotes are escaped - if (/[\s,"'=]/.test(arg) && !/^".*"$/.test(arg)) { - // Check if needs quoting and isn't already quoted - return `"${arg.replace(/(["\\])/g, '\\$1')}"`; // Escape existing quotes and backslashes - } - return arg; - }); - - const commandString = escapedCommand.join(' '); - log('info', `Executing ${logPrefix} command with xcodemake: ${commandString}`); - - return new Promise((resolve, reject) => { - // Using 'sh -c' to handle complex commands and quoting properly - const process = spawn('sh', ['-c', commandString], { - stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr - }); - - let stdout = ''; - let stderr = ''; - - process.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - process.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - process.on('close', (code) => { - const success = code === 0; - const response: XcodeCommandResponse = { - success, - output: stdout, - error: success ? undefined : stderr, - }; - - resolve(response); - }); - - process.on('error', (err) => { - reject(err); - }); - }); + return executeCommand(command, logPrefix); } /** @@ -298,39 +225,7 @@ export async function executeXcodemakeCommand( export async function executeMakeCommand( projectDir: string, logPrefix: string, -): Promise { - const command = `cd "${projectDir}" && make`; - log('info', `Executing ${logPrefix} command with make: ${command}`); - - return new Promise((resolve, reject) => { - const process = spawn('sh', ['-c', command], { - stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr - }); - - let stdout = ''; - let stderr = ''; - - process.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - process.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - process.on('close', (code) => { - const success = code === 0; - const response: XcodeCommandResponse = { - success, - output: stdout, - error: success ? undefined : stderr, - }; - - resolve(response); - }); - - process.on('error', (err) => { - reject(err); - }); - }); +): Promise { + const command = ['cd', projectDir, '&&', 'make']; + return executeCommand(command, logPrefix); }