Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

### Fixed

- Fixed remaining `/bin/sh -c` shell-injection sites in bundle ID extraction and macOS launch flows by invoking `defaults` and `PlistBuddy` directly with argv arrays so user-supplied app paths are no longer interpreted by a shell ([#367](https://github.com/getsentry/XcodeBuildMCP/issues/367)).
- Fixed simulator test JSONL accuracy by keeping preflight discovery observational, preserving only user-supplied test selectors, discovering multiline parameterized Swift Testing tests, and parsing destination-suffixed xcodebuild test result lines.
- Removed stale physical-device log session status and shutdown cleanup for deprecated standalone device log capture, and corrected the device build-and-run tool description.
- Fixed mixed Swift Testing and XCTest summaries so simulator test text output no longer overcounts parameterized Swift Testing results or issue lines.
- Fixed CLI test summaries showing false-positive compiler errors from xcodebuild NSError dump lines, and added compiler-error snapshot coverage for simulator, device, and macOS build-style flows ([#383](https://github.com/getsentry/XcodeBuildMCP/issues/383)).
- Fixed simulator OSLog helper cleanup so server and daemon startup reconcile same-workspace orphaned log streams without stopping helpers owned by live sessions in other workspaces ([#382](https://github.com/getsentry/XcodeBuildMCP/issues/382)).
- Fixed Weather example test discovery and made CLI test progress visible while tests are running instead of leaving the last build phase displayed.

## [2.5.0-beta.1]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
BuildableName = "Weather.app"
BlueprintName = "Weather"
ReferencedContainer = "container:Weather.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B92914D2FA3FCC400B2E371"
BuildableName = "WeatherTests.xctest"
BlueprintName = "WeatherTests"
ReferencedContainer = "container:Weather.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B9291572FA3FCC400B2E371"
BuildableName = "WeatherUITests.xctest"
BlueprintName = "WeatherUITests"
ReferencedContainer = "container:Weather.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
BuildableName = "Weather.app"
BlueprintName = "Weather"
ReferencedContainer = "container:Weather.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8B9291402FA3FCC300B2E371"
BuildableName = "Weather.app"
BlueprintName = "Weather"
ReferencedContainer = "container:Weather.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
10 changes: 5 additions & 5 deletions src/mcp/tools/device/__tests__/build_run_device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('build_run_device tool', () => {
});
}

if (command[0] === '/bin/sh') {
if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') {
return createMockCommandResponse({ success: true, output: 'io.sentry.MyApp' });
}

Expand Down Expand Up @@ -143,7 +143,7 @@ describe('build_run_device tool', () => {
});
}

if (command[0] === '/bin/sh') {
if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') {
return createMockCommandResponse({ success: true, output: 'io.sentry.MyApp' });
}

Expand Down Expand Up @@ -177,7 +177,7 @@ describe('build_run_device tool', () => {
});
}

if (command[0] === '/bin/sh') {
if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') {
return createMockCommandResponse({ success: true, output: 'io.sentry.MyApp' });
}

Expand Down Expand Up @@ -218,7 +218,7 @@ describe('build_run_device tool', () => {
});
}

if (command[0] === '/bin/sh') {
if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') {
return createMockCommandResponse({ success: true, output: 'io.sentry.MyApp' });
}

Expand Down Expand Up @@ -258,7 +258,7 @@ describe('build_run_device tool', () => {
});
}

if (command[0] === '/bin/sh') {
if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') {
return createMockCommandResponse({ success: true, output: 'io.sentry.MyWatchApp' });
}

Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools/macos/build_macos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function createBuildMacOSExecutor(
);

const plistResult = await executor(
['/bin/sh', '-c', `defaults read "${appPath}/Contents/Info" CFBundleIdentifier`],
['defaults', 'read', `${appPath}/Contents/Info`, 'CFBundleIdentifier'],
'Extract Bundle ID',
false,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('get_app_bundle_id plugin', () => {

it('should return success with bundle ID using defaults read', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': 'io.sentry.MyApp',
'defaults read /path/to/MyApp.app/Info CFBundleIdentifier': 'io.sentry.MyApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
existsSync: () => true,
Expand All @@ -116,10 +116,10 @@ describe('get_app_bundle_id plugin', () => {

it('should fallback to PlistBuddy when defaults read fails', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
'defaults read /path/to/MyApp.app/Info CFBundleIdentifier': new Error(
'defaults read failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /path/to/MyApp.app/Info.plist':
'io.sentry.MyApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -145,10 +145,10 @@ describe('get_app_bundle_id plugin', () => {

it('should return error when both extraction methods fail', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
'defaults read /path/to/MyApp.app/Info CFBundleIdentifier': new Error(
'defaults read failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /path/to/MyApp.app/Info.plist':
new Error('Command failed'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -169,10 +169,10 @@ describe('get_app_bundle_id plugin', () => {

it('keeps extraction errors short and preserves diagnostics', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': new Error(
'defaults read /path/to/MyApp.app/Info CFBundleIdentifier': new Error(
'defaults read failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /path/to/MyApp.app/Info.plist':
new Error('Command failed'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('get_mac_bundle_id plugin', () => {

it('should return success with bundle ID using defaults read', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier':
'defaults read /Applications/MyApp.app/Contents/Info CFBundleIdentifier':
'io.sentry.MyMacApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -96,10 +96,10 @@ describe('get_mac_bundle_id plugin', () => {

it('should fallback to PlistBuddy when defaults read fails', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'defaults read /Applications/MyApp.app/Contents/Info CFBundleIdentifier': new Error(
'defaults read failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /Applications/MyApp.app/Contents/Info.plist':
'io.sentry.MyMacApp',
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -123,10 +123,10 @@ describe('get_mac_bundle_id plugin', () => {

it('should return error when both extraction methods fail', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'defaults read /Applications/MyApp.app/Contents/Info CFBundleIdentifier': new Error(
'Command failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /Applications/MyApp.app/Contents/Info.plist':
new Error('Command failed'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -147,10 +147,10 @@ describe('get_mac_bundle_id plugin', () => {

it('keeps extraction errors short and preserves diagnostics', async () => {
const mockExecutor = createMockExecutorForCommands({
'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': new Error(
'defaults read /Applications/MyApp.app/Contents/Info CFBundleIdentifier': new Error(
'Command failed',
),
'/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"':
'/usr/libexec/PlistBuddy -c Print :CFBundleIdentifier /Applications/MyApp.app/Contents/Info.plist':
new Error('Command failed'),
});
const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand Down
17 changes: 11 additions & 6 deletions src/mcp/tools/project-discovery/get_mac_bundle_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
import { toErrorMessage } from '../../../utils/errors.ts';
import { createBasicDiagnostics } from '../../../utils/diagnostics.ts';

async function executeSyncCommand(command: string, executor: CommandExecutor): Promise<string> {
const result = await executor(['/bin/sh', '-c', command], 'macOS Bundle ID Extraction');
async function runSpawn(command: string[], executor: CommandExecutor): Promise<string> {
const result = await executor(command, 'macOS Bundle ID Extraction', false);
if (!result.success) {
throw new Error(result.error ?? 'Command failed');
}
Expand Down Expand Up @@ -49,14 +49,19 @@ export function createGetMacBundleIdExecutor(
let bundleId: string;

try {
bundleId = await executeSyncCommand(
`defaults read "${appPath}/Contents/Info" CFBundleIdentifier`,
bundleId = await runSpawn(
['defaults', 'read', `${appPath}/Contents/Info`, 'CFBundleIdentifier'],
executor,
);
} catch {
try {
bundleId = await executeSyncCommand(
`/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${appPath}/Contents/Info.plist"`,
bundleId = await runSpawn(
[
'/usr/libexec/PlistBuddy',
'-c',
'Print :CFBundleIdentifier',
`${appPath}/Contents/Info.plist`,
],
executor,
);
} catch (innerError) {
Expand Down
Loading
Loading