From b74aacc3f08c93d8eca21ec27921b365bff850cd Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 21 Jul 2026 18:47:36 +0100 Subject: [PATCH 1/7] fix(macos): Target app processes exactly Stop macOS apps by exact executable name instead of matching the app name against every process argument. Reject empty app names and unsafe process IDs before command execution. Fixes #306 --- CHANGELOG.md | 1 + .../macos/__tests__/stop_mac_app.test.ts | 90 +++++++++++++------ src/mcp/tools/macos/stop_mac_app.ts | 12 ++- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dae332aa7..d3078831b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fixed `suppressWarnings` being ignored in settled build, build-run, and test output. The flag was honored only while streaming, so warnings still reached the final MCP tool response ([#447](https://github.com/getsentry/XcodeBuildMCP/issues/447)). - Fixed iOS scaffold orientation and device-family settings, LLDB command isolation and argument escaping, run-destination parsing without an active scheme, concurrent working-directory mutations, blocking physical-device name lookup, and unverified `xcodemake` downloads ([#459](https://github.com/getsentry/XcodeBuildMCP/issues/459)). - Fixed simulator UI launching and keyboard controls to prefer Xcode 27's Device Hub when available, with Simulator.app as the legacy fallback. +- Fixed `stop_mac_app` app-name targeting so it no longer terminates unrelated processes whose command lines contain the app name, and reject unsafe process IDs before execution ([#306](https://github.com/getsentry/XcodeBuildMCP/issues/306)). ## [2.6.2] diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index bb93564c2..6782d651e 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { schema, handler, stop_mac_appLogic } from '../stop_mac_app.ts'; import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; +import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; describe('stop_mac_app plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -17,15 +18,18 @@ describe('stop_mac_app plugin', () => { // Test invalid inputs expect(schema.appName.safeParse(null).success).toBe(false); + expect(schema.appName.safeParse('').success).toBe(false); expect(schema.processId.safeParse('not-number').success).toBe(false); expect(schema.processId.safeParse(null).success).toBe(false); + expect(schema.processId.safeParse(0).success).toBe(false); + expect(schema.processId.safeParse(-1).success).toBe(false); + expect(schema.processId.safeParse(1.5).success).toBe(false); }); }); describe('Input Validation', () => { it('should return exact validation error for missing parameters', async () => { - const mockExecutor = async () => ({ success: true, output: '', process: {} as any }); - const result = await runLogic(() => stop_mac_appLogic({}, mockExecutor)); + const result = await runLogic(() => stop_mac_appLogic({}, createNoopExecutor())); expect(result.isError).toBe(true); expect(allText(result)).toContain('appName or processId'); @@ -34,11 +38,10 @@ describe('stop_mac_app plugin', () => { describe('Command Generation', () => { it('should generate correct command for process ID', async () => { - const calls: any[] = []; - const mockExecutor = async (command: string[]) => { - calls.push({ command }); - return { success: true, output: '', process: {} as any }; - }; + const calls: string[][] = []; + const mockExecutor = createMockExecutor({ + onExecute: (command) => calls.push(command), + }); await runLogic(() => stop_mac_appLogic( @@ -50,35 +53,70 @@ describe('stop_mac_app plugin', () => { ); expect(calls).toHaveLength(1); - expect(calls[0].command).toEqual(['kill', '1234']); + expect(calls[0]).toEqual(['kill', '1234']); }); - it('should generate correct command for app name', async () => { - const calls: any[] = []; - const mockExecutor = async (command: string[]) => { - calls.push({ command }); - return { success: true, output: '', process: {} as any }; - }; + it('should avoid matching app names in unrelated process arguments', async () => { + const calls: string[][] = []; + const mockExecutor = createMockExecutor({ + onExecute: (command) => calls.push(command), + }); await runLogic(() => stop_mac_appLogic( { - appName: 'Calculator', + appName: 'Brimday', }, mockExecutor, ), ); expect(calls).toHaveLength(1); - expect(calls[0].command).toEqual(['pkill', '-f', 'Calculator']); + expect(calls[0]).toEqual(['pkill', '-x', '--', 'Brimday']); + expect(calls[0]).not.toContain('-f'); + }); + + it('should preserve long app executable names', async () => { + const calls: string[][] = []; + const mockExecutor = createMockExecutor({ + onExecute: (command) => calls.push(command), + }); + + await runLogic(() => + stop_mac_appLogic( + { + appName: 'ThisIsAVeryLongApplicationName', + }, + mockExecutor, + ), + ); + + expect(calls[0]).toEqual(['pkill', '-x', '--', 'ThisIsAVeryLongApplicationName']); + }); + + it('should treat app names as literal process names', async () => { + const calls: string[][] = []; + const mockExecutor = createMockExecutor({ + onExecute: (command) => calls.push(command), + }); + + await runLogic(() => + stop_mac_appLogic( + { + appName: '-Example.*[Test]', + }, + mockExecutor, + ), + ); + + expect(calls[0]).toEqual(['pkill', '-x', '--', '-Example\\.\\*\\[Test\\]']); }); it('should prioritize processId over appName', async () => { - const calls: any[] = []; - const mockExecutor = async (command: string[]) => { - calls.push({ command }); - return { success: true, output: '', process: {} as any }; - }; + const calls: string[][] = []; + const mockExecutor = createMockExecutor({ + onExecute: (command) => calls.push(command), + }); await runLogic(() => stop_mac_appLogic( @@ -91,13 +129,13 @@ describe('stop_mac_app plugin', () => { ); expect(calls).toHaveLength(1); - expect(calls[0].command).toEqual(['kill', '1234']); + expect(calls[0]).toEqual(['kill', '1234']); }); }); describe('Response Processing', () => { it('should return exact successful stop response by app name', async () => { - const mockExecutor = async () => ({ success: true, output: '', process: {} as any }); + const mockExecutor = createMockExecutor({}); const result = await runLogic(() => stop_mac_appLogic( @@ -112,7 +150,7 @@ describe('stop_mac_app plugin', () => { }); it('should return exact successful stop response with both parameters (processId takes precedence)', async () => { - const mockExecutor = async () => ({ success: true, output: '', process: {} as any }); + const mockExecutor = createMockExecutor({}); const result = await runLogic(() => stop_mac_appLogic( @@ -128,9 +166,7 @@ describe('stop_mac_app plugin', () => { }); it('should handle execution errors', async () => { - const mockExecutor = async () => { - throw new Error('Process not found'); - }; + const mockExecutor = createMockExecutor(new Error('Process not found')); const result = await runLogic(() => stop_mac_appLogic( diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index 2f4cc2d49..2e1a794fa 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -14,13 +14,17 @@ import { } from '../../../utils/app-lifecycle-results.ts'; const stopMacAppSchema = z.object({ - appName: z.string().optional(), - processId: z.number().optional(), + appName: z.string().min(1).optional(), + processId: z.number().int().positive().optional(), }); type StopMacAppParams = z.infer; type StopMacAppResult = StopResultDomainResult; +function escapeExtendedRegularExpression(value: string): string { + return value.replace(/[\\.^$*+?()[\]{}|]/g, '\\$&'); +} + export async function stop_mac_appLogic( params: StopMacAppParams, executor: CommandExecutor, @@ -60,14 +64,14 @@ export function createStopMacAppExecutor( return buildStopFailure(artifacts, 'Either appName or processId must be provided.'); } - const target = params.processId ? `PID ${params.processId}` : params.appName!; + const target = params.processId !== undefined ? `PID ${params.processId}` : params.appName!; log('info', `Stopping macOS app: ${target}`); try { const command = params.processId !== undefined ? ['kill', String(params.processId)] - : ['pkill', '-f', params.appName!]; + : ['pkill', '-x', '--', escapeExtendedRegularExpression(params.appName!)]; const result = await executor(command, 'Stop macOS App'); if (!result.success) { From 4dab1fb6122b842f755797489329eef0ebc93182 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 21 Jul 2026 19:21:38 +0100 Subject: [PATCH 2/7] fix(macos): Validate process IDs at execution boundary Reject unsafe process IDs before constructing or invoking kill, even when callers bypass the typed-tool schema. Refs #306 --- .../tools/macos/__tests__/stop_mac_app.test.ts | 16 ++++++++++++++++ src/mcp/tools/macos/stop_mac_app.ts | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index 6782d651e..25ca00cbc 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -24,6 +24,8 @@ describe('stop_mac_app plugin', () => { expect(schema.processId.safeParse(0).success).toBe(false); expect(schema.processId.safeParse(-1).success).toBe(false); expect(schema.processId.safeParse(1.5).success).toBe(false); + expect(schema.processId.safeParse(Number.NaN).success).toBe(false); + expect(schema.processId.safeParse(Number.MAX_SAFE_INTEGER + 1).success).toBe(false); }); }); @@ -34,6 +36,20 @@ describe('stop_mac_app plugin', () => { expect(result.isError).toBe(true); expect(allText(result)).toContain('appName or processId'); }); + + it.each([0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])( + 'should reject unsafe process ID %s at the execution boundary', + async (processId) => { + const calls: string[][] = []; + const executor = createMockExecutor({ onExecute: (command) => calls.push(command) }); + + const result = await runLogic(() => stop_mac_appLogic({ processId }, executor)); + + expect(result.isError).toBe(true); + expect(allText(result)).toContain('processId must be a positive safe integer'); + expect(calls).toHaveLength(0); + }, + ); }); describe('Command Generation', () => { diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index 2e1a794fa..1dff32997 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -64,6 +64,13 @@ export function createStopMacAppExecutor( return buildStopFailure(artifacts, 'Either appName or processId must be provided.'); } + if ( + params.processId !== undefined && + (!Number.isSafeInteger(params.processId) || params.processId <= 0) + ) { + return buildStopFailure(artifacts, 'processId must be a positive safe integer.'); + } + const target = params.processId !== undefined ? `PID ${params.processId}` : params.appName!; log('info', `Stopping macOS app: ${target}`); From 77586cb47ea46ad2c5e8aa713dfe63f7be8bad7b Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 21 Jul 2026 19:48:10 +0100 Subject: [PATCH 3/7] fix: reject unsafe macOS process IDs in schema --- src/mcp/tools/macos/stop_mac_app.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index 1dff32997..51875a137 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -15,7 +15,12 @@ import { const stopMacAppSchema = z.object({ appName: z.string().min(1).optional(), - processId: z.number().int().positive().optional(), + processId: z + .number() + .int() + .positive() + .refine(Number.isSafeInteger, 'processId must be a positive safe integer.') + .optional(), }); type StopMacAppParams = z.infer; From 1e0eeeb46b9baa0b0755210c0ba767c4c6c230e1 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 22 Jul 2026 09:47:10 +0100 Subject: [PATCH 4/7] fix: support long macOS app names --- src/mcp/tools/macos/__tests__/stop_mac_app.test.ts | 7 +++---- src/mcp/tools/macos/stop_mac_app.ts | 7 ++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index 25ca00cbc..1f5608839 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -88,8 +88,7 @@ describe('stop_mac_app plugin', () => { ); expect(calls).toHaveLength(1); - expect(calls[0]).toEqual(['pkill', '-x', '--', 'Brimday']); - expect(calls[0]).not.toContain('-f'); + expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?Brimday( |$)']); }); it('should preserve long app executable names', async () => { @@ -107,7 +106,7 @@ describe('stop_mac_app plugin', () => { ), ); - expect(calls[0]).toEqual(['pkill', '-x', '--', 'ThisIsAVeryLongApplicationName']); + expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?ThisIsAVeryLongApplicationName( |$)']); }); it('should treat app names as literal process names', async () => { @@ -125,7 +124,7 @@ describe('stop_mac_app plugin', () => { ), ); - expect(calls[0]).toEqual(['pkill', '-x', '--', '-Example\\.\\*\\[Test\\]']); + expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?-Example\\.\\*\\[Test\\]( |$)']); }); it('should prioritize processId over appName', async () => { diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index 51875a137..283398da7 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -83,7 +83,12 @@ export function createStopMacAppExecutor( const command = params.processId !== undefined ? ['kill', String(params.processId)] - : ['pkill', '-x', '--', escapeExtendedRegularExpression(params.appName!)]; + : [ + 'pkill', + '-f', + '--', + `^(.*/)?${escapeExtendedRegularExpression(params.appName!)}( |$)`, + ]; const result = await executor(command, 'Stop macOS App'); if (!result.success) { From 02aa2a9380ec77dfcaf6b3c5e3abe7816b69f4b1 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 22 Jul 2026 11:02:16 +0100 Subject: [PATCH 5/7] fix(macos): Match app names by process name Use killall instead of full-command regular expressions so later arguments cannot select unrelated processes. Refs #306 --- src/mcp/tools/macos/__tests__/stop_mac_app.test.ts | 8 ++++---- src/mcp/tools/macos/stop_mac_app.ts | 11 +---------- .../__tests__/e2e-mcp-device-macos.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index 1f5608839..1f49c4b60 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -72,7 +72,7 @@ describe('stop_mac_app plugin', () => { expect(calls[0]).toEqual(['kill', '1234']); }); - it('should avoid matching app names in unrelated process arguments', async () => { + it('should target app names by literal process name', async () => { const calls: string[][] = []; const mockExecutor = createMockExecutor({ onExecute: (command) => calls.push(command), @@ -88,7 +88,7 @@ describe('stop_mac_app plugin', () => { ); expect(calls).toHaveLength(1); - expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?Brimday( |$)']); + expect(calls[0]).toEqual(['killall', '--', 'Brimday']); }); it('should preserve long app executable names', async () => { @@ -106,7 +106,7 @@ describe('stop_mac_app plugin', () => { ), ); - expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?ThisIsAVeryLongApplicationName( |$)']); + expect(calls[0]).toEqual(['killall', '--', 'ThisIsAVeryLongApplicationName']); }); it('should treat app names as literal process names', async () => { @@ -124,7 +124,7 @@ describe('stop_mac_app plugin', () => { ), ); - expect(calls[0]).toEqual(['pkill', '-f', '--', '^(.*/)?-Example\\.\\*\\[Test\\]( |$)']); + expect(calls[0]).toEqual(['killall', '--', '-Example.*[Test]']); }); it('should prioritize processId over appName', async () => { diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index 283398da7..f5c578a02 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -26,10 +26,6 @@ const stopMacAppSchema = z.object({ type StopMacAppParams = z.infer; type StopMacAppResult = StopResultDomainResult; -function escapeExtendedRegularExpression(value: string): string { - return value.replace(/[\\.^$*+?()[\]{}|]/g, '\\$&'); -} - export async function stop_mac_appLogic( params: StopMacAppParams, executor: CommandExecutor, @@ -83,12 +79,7 @@ export function createStopMacAppExecutor( const command = params.processId !== undefined ? ['kill', String(params.processId)] - : [ - 'pkill', - '-f', - '--', - `^(.*/)?${escapeExtendedRegularExpression(params.appName!)}( |$)`, - ]; + : ['killall', '--', params.appName!]; const result = await executor(command, 'Stop macOS App'); if (!result.success) { diff --git a/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts b/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts index fa048fc47..f4a395de5 100644 --- a/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts +++ b/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts @@ -20,7 +20,7 @@ beforeAll(async () => { 'xctrace list devices': { success: true, output: 'No devices found.' }, open: { success: true, output: '' }, kill: { success: true, output: '' }, - pkill: { success: true, output: '' }, + killall: { success: true, output: '' }, 'defaults read': { success: true, output: 'io.sentry.MyApp' }, PlistBuddy: { success: true, output: 'io.sentry.MyApp' }, xcresulttool: { success: true, output: '{}' }, @@ -328,7 +328,7 @@ describe('MCP Device and macOS Tool Invocation (e2e)', () => { expect(commandStrs.some((c) => c.includes('kill') && c.includes('54321'))).toBe(true); }); - it('stop_mac_app captures pkill command with appName', async () => { + it('stop_mac_app captures killall command with appName', async () => { harness.resetCapturedCommands(); const result = await harness.client.callTool({ name: 'stop_mac_app', @@ -338,7 +338,7 @@ describe('MCP Device and macOS Tool Invocation (e2e)', () => { expectContent(result); const commandStrs = harness.capturedCommands.map((c) => c.command.join(' ')); - expect(commandStrs.some((c) => c.includes('MyMacApp'))).toBe(true); + expect(commandStrs.some((c) => c === 'killall -- MyMacApp')).toBe(true); }); it('get_mac_app_path captures xcodebuild showBuildSettings command', async () => { From 083a50cae24bf92a1fe93be96676144feafe5ab4 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 22 Jul 2026 11:09:10 +0100 Subject: [PATCH 6/7] test(macos): Clarify long app name coverage Name the unit test for its command-construction boundary; host-level validation covers actual process selection. Refs #306 --- src/mcp/tools/macos/__tests__/stop_mac_app.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index 1f49c4b60..a2a473fce 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -91,7 +91,7 @@ describe('stop_mac_app plugin', () => { expect(calls[0]).toEqual(['killall', '--', 'Brimday']); }); - it('should preserve long app executable names', async () => { + it('should pass long app executable names to killall', async () => { const calls: string[][] = []; const mockExecutor = createMockExecutor({ onExecute: (command) => calls.push(command), From 334fd76d3cdaf7a1921536d05842b3ded12080b0 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 22 Jul 2026 13:56:05 +0100 Subject: [PATCH 7/7] fix(macos): Sanitize invalid stop artifacts --- .../macos/__tests__/stop_mac_app.test.ts | 23 ++++++++++++++----- src/mcp/tools/macos/stop_mac_app.ts | 10 ++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index a2a473fce..fc43d486c 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect } from 'vitest'; import { schema, handler, stop_mac_appLogic } from '../stop_mac_app.ts'; -import { allText, runLogic } from '../../../../test-utils/test-helpers.ts'; +import { + allText, + createMockToolHandlerContext, + runLogic, +} from '../../../../test-utils/test-helpers.ts'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; describe('stop_mac_app plugin', () => { @@ -42,11 +46,18 @@ describe('stop_mac_app plugin', () => { async (processId) => { const calls: string[][] = []; const executor = createMockExecutor({ onExecute: (command) => calls.push(command) }); - - const result = await runLogic(() => stop_mac_appLogic({ processId }, executor)); - - expect(result.isError).toBe(true); - expect(allText(result)).toContain('processId must be a positive safe integer'); + const { ctx, result, run } = createMockToolHandlerContext(); + + await run(() => stop_mac_appLogic({ processId }, executor)); + + expect(result.isError()).toBe(true); + expect(result.text()).toContain('processId must be a positive safe integer'); + const structuredResult = ctx.structuredOutput?.result; + expect(structuredResult?.kind).toBe('stop-result'); + if (structuredResult?.kind !== 'stop-result') { + throw new Error('Expected stop-result structured output.'); + } + expect(structuredResult.artifacts).toEqual({ appName: '' }); expect(calls).toHaveLength(0); }, ); diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index f5c578a02..9b70c263b 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -59,19 +59,21 @@ export function createStopMacAppExecutor( executor: CommandExecutor, ): NonStreamingExecutor { return async (params) => { - const artifacts = createStopMacAppArtifacts(params); - if (!params.appName && params.processId === undefined) { - return buildStopFailure(artifacts, 'Either appName or processId must be provided.'); + return buildStopFailure({ appName: '' }, 'Either appName or processId must be provided.'); } if ( params.processId !== undefined && (!Number.isSafeInteger(params.processId) || params.processId <= 0) ) { - return buildStopFailure(artifacts, 'processId must be a positive safe integer.'); + return buildStopFailure( + { appName: params.appName ?? '' }, + 'processId must be a positive safe integer.', + ); } + const artifacts = createStopMacAppArtifacts(params); const target = params.processId !== undefined ? `PID ${params.processId}` : params.appName!; log('info', `Stopping macOS app: ${target}`);