Skip to content

Commit 48129b4

Browse files
author
Ismar Iljazovic
committed
Add platform-aware setup wizard and mcp-json output
Introduce platform selection to the setup wizard, recommend workflows per platform, and avoid prompting for a simulator when macOS is the only platform selected. Add helpers and constants (SetupPlatform, PLATFORM_WORKFLOWS, PLATFORM_OPTIONS, infer/derive/filter helpers), a multi-select platform prompt, and make the setup flow platform-aware (seed workflow defaults, filter simulators, preserve platform in sessionDefaults). Add selectionToMcpConfigJson() and a --format mcp-json option to print a ready-to-paste MCP client config JSON block (runSetupWizard supports 'mcp-json' early-exit). Update tests (createPlatformPrompter and four platform-aware cases) and CHANGELOG.md to document the new behavior.
1 parent b7f8a89 commit 48129b4

File tree

3 files changed

+502
-35
lines changed

3 files changed

+502
-35
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- Added platform selection step to the `xcodebuildmcp setup` wizard. You now choose which platforms you are developing for (macOS, iOS, tvOS, watchOS, visionOS) before selecting workflows. Based on the selection, the wizard automatically recommends the appropriate workflow set.
8+
9+
### Changed
10+
11+
- The `setup` wizard no longer prompts for a simulator or device when macOS is the only selected platform — macOS apps run natively and do not require a simulator or physical device.
12+
- When a single platform is selected, `xcodebuildmcp setup` now writes `platform` to `sessionDefaults` in `config.yaml` and includes `XCODEBUILDMCP_PLATFORM` in `--format mcp-json` output. For multi-platform projects the platform key is omitted so the agent can choose per-command.
13+
- The `setup` wizard remembers previous choices on re-run: existing `config.yaml` values (including the new `platform`) are pre-loaded as defaults for every prompt.
14+
315
## [2.3.0]
416

517
### Added

src/cli/commands/__tests__/setup.test.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ function createTestPrompter(): Prompter {
108108
};
109109
}
110110

111+
function createPlatformPrompter(platforms: string[]): Prompter {
112+
let selectManyCalls = 0;
113+
return {
114+
selectOne: async <T>(opts: { options: Array<{ value: T }> }) => {
115+
const preferredOption = opts.options.find((option) => option.value != null);
116+
return (preferredOption ?? opts.options[0]).value;
117+
},
118+
selectMany: async <T>(opts: { options: Array<{ value: T }> }) => {
119+
selectManyCalls++;
120+
if (selectManyCalls === 1) {
121+
return opts.options
122+
.filter((option) => platforms.includes(String(option.value)))
123+
.map((option) => option.value);
124+
}
125+
return opts.options.map((option) => option.value);
126+
},
127+
confirm: async (opts: { defaultValue: boolean }) => opts.defaultValue,
128+
};
129+
}
130+
111131
describe('setup command', () => {
112132
const originalStdinIsTTY = process.stdin.isTTY;
113133
const originalStdoutIsTTY = process.stdout.isTTY;
@@ -1054,4 +1074,230 @@ sessionDefaults:
10541074

10551075
await expect(runSetupWizard()).rejects.toThrow('requires an interactive TTY');
10561076
});
1077+
1078+
it('skips simulator and sets platform for macOS-only selection', async () => {
1079+
let storedConfig = '';
1080+
1081+
const fs = createMockFileSystemExecutor({
1082+
existsSync: (targetPath) => targetPath === configPath && storedConfig.length > 0,
1083+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1084+
readdir: async (targetPath) => {
1085+
if (targetPath === cwd) {
1086+
return [
1087+
{
1088+
name: 'App.xcworkspace',
1089+
isDirectory: () => true,
1090+
isSymbolicLink: () => false,
1091+
},
1092+
];
1093+
}
1094+
return [];
1095+
},
1096+
readFile: async (targetPath) => {
1097+
if (targetPath !== configPath) throw new Error(`Unexpected read path: ${targetPath}`);
1098+
return storedConfig;
1099+
},
1100+
writeFile: async (targetPath, content) => {
1101+
if (targetPath !== configPath) throw new Error(`Unexpected write path: ${targetPath}`);
1102+
storedConfig = content;
1103+
},
1104+
});
1105+
1106+
const executor: CommandExecutor = async () =>
1107+
createMockCommandResponse({
1108+
success: true,
1109+
output: `Information about workspace "App":\n Schemes:\n App`,
1110+
});
1111+
1112+
await runSetupWizard({
1113+
cwd,
1114+
fs,
1115+
executor,
1116+
prompter: createPlatformPrompter(['macOS']),
1117+
quietOutput: true,
1118+
});
1119+
1120+
const parsed = parseYaml(storedConfig) as {
1121+
sessionDefaults?: Record<string, unknown>;
1122+
};
1123+
1124+
expect(parsed.sessionDefaults?.platform).toBe('macOS');
1125+
expect(parsed.sessionDefaults?.simulatorId).toBeUndefined();
1126+
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
1127+
});
1128+
1129+
it('outputs XCODEBUILDMCP_PLATFORM=macOS and no simulator fields for macOS-only mcp-json', async () => {
1130+
const fs = createMockFileSystemExecutor({
1131+
existsSync: () => false,
1132+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1133+
readdir: async (targetPath) => {
1134+
if (targetPath === cwd) {
1135+
return [
1136+
{
1137+
name: 'App.xcworkspace',
1138+
isDirectory: () => true,
1139+
isSymbolicLink: () => false,
1140+
},
1141+
];
1142+
}
1143+
return [];
1144+
},
1145+
readFile: async () => '',
1146+
writeFile: async () => {},
1147+
});
1148+
1149+
const executor: CommandExecutor = async () =>
1150+
createMockCommandResponse({
1151+
success: true,
1152+
output: `Information about workspace "App":\n Schemes:\n App`,
1153+
});
1154+
1155+
const result = await runSetupWizard({
1156+
cwd,
1157+
fs,
1158+
executor,
1159+
prompter: createPlatformPrompter(['macOS']),
1160+
quietOutput: true,
1161+
outputFormat: 'mcp-json',
1162+
});
1163+
1164+
expect(result.mcpConfigJson).toBeDefined();
1165+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1166+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1167+
};
1168+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1169+
1170+
expect(env.XCODEBUILDMCP_PLATFORM).toBe('macOS');
1171+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBeUndefined();
1172+
expect(env.XCODEBUILDMCP_SIMULATOR_NAME).toBeUndefined();
1173+
});
1174+
1175+
it('outputs XCODEBUILDMCP_PLATFORM=iOS Simulator and simulator fields for iOS-only mcp-json', async () => {
1176+
const fs = createMockFileSystemExecutor({
1177+
existsSync: () => false,
1178+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1179+
readdir: async (targetPath) => {
1180+
if (targetPath === cwd) {
1181+
return [
1182+
{
1183+
name: 'App.xcworkspace',
1184+
isDirectory: () => true,
1185+
isSymbolicLink: () => false,
1186+
},
1187+
];
1188+
}
1189+
return [];
1190+
},
1191+
readFile: async () => '',
1192+
writeFile: async () => {},
1193+
});
1194+
1195+
const executor: CommandExecutor = async (command) => {
1196+
if (command.includes('--json')) {
1197+
return createMockCommandResponse({
1198+
success: true,
1199+
output: JSON.stringify({
1200+
devices: {
1201+
'iOS 17.0': [
1202+
{ name: 'iPhone 15', udid: 'SIM-1', state: 'Shutdown', isAvailable: true },
1203+
],
1204+
},
1205+
}),
1206+
});
1207+
}
1208+
if (command[0] === 'xcrun') {
1209+
return createMockCommandResponse({
1210+
success: true,
1211+
output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`,
1212+
});
1213+
}
1214+
return createMockCommandResponse({
1215+
success: true,
1216+
output: `Information about workspace "App":\n Schemes:\n App`,
1217+
});
1218+
};
1219+
1220+
const result = await runSetupWizard({
1221+
cwd,
1222+
fs,
1223+
executor,
1224+
prompter: createPlatformPrompter(['iOS']),
1225+
quietOutput: true,
1226+
outputFormat: 'mcp-json',
1227+
});
1228+
1229+
expect(result.mcpConfigJson).toBeDefined();
1230+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1231+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1232+
};
1233+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1234+
1235+
expect(env.XCODEBUILDMCP_PLATFORM).toBe('iOS Simulator');
1236+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1');
1237+
expect(env.XCODEBUILDMCP_SIMULATOR_NAME).toBe('iPhone 15');
1238+
});
1239+
1240+
it('omits XCODEBUILDMCP_PLATFORM for multi-platform mcp-json', async () => {
1241+
const fs = createMockFileSystemExecutor({
1242+
existsSync: () => false,
1243+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1244+
readdir: async (targetPath) => {
1245+
if (targetPath === cwd) {
1246+
return [
1247+
{
1248+
name: 'App.xcworkspace',
1249+
isDirectory: () => true,
1250+
isSymbolicLink: () => false,
1251+
},
1252+
];
1253+
}
1254+
return [];
1255+
},
1256+
readFile: async () => '',
1257+
writeFile: async () => {},
1258+
});
1259+
1260+
const executor: CommandExecutor = async (command) => {
1261+
if (command.includes('--json')) {
1262+
return createMockCommandResponse({
1263+
success: true,
1264+
output: JSON.stringify({
1265+
devices: {
1266+
'iOS 17.0': [
1267+
{ name: 'iPhone 15', udid: 'SIM-1', state: 'Shutdown', isAvailable: true },
1268+
],
1269+
},
1270+
}),
1271+
});
1272+
}
1273+
if (command[0] === 'xcrun') {
1274+
return createMockCommandResponse({
1275+
success: true,
1276+
output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`,
1277+
});
1278+
}
1279+
return createMockCommandResponse({
1280+
success: true,
1281+
output: `Information about workspace "App":\n Schemes:\n App`,
1282+
});
1283+
};
1284+
1285+
const result = await runSetupWizard({
1286+
cwd,
1287+
fs,
1288+
executor,
1289+
prompter: createPlatformPrompter(['macOS', 'iOS']),
1290+
quietOutput: true,
1291+
outputFormat: 'mcp-json',
1292+
});
1293+
1294+
expect(result.mcpConfigJson).toBeDefined();
1295+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1296+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1297+
};
1298+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1299+
1300+
expect(env.XCODEBUILDMCP_PLATFORM).toBeUndefined();
1301+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1');
1302+
});
10571303
});

0 commit comments

Comments
 (0)