diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb19a02..dabd4f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] + +### Added + +- Added environment variable support for all session defaults (e.g., `XCODEBUILDMCP_WORKSPACE_PATH`, `XCODEBUILDMCP_SCHEME`, `XCODEBUILDMCP_PLATFORM`), enabling full configuration via the MCP client `env` field without requiring a config file ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)). +- Added `--format mcp-json` flag to `xcodebuildmcp setup` that outputs a ready-to-paste MCP client config JSON block instead of writing `config.yaml` ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)). +- Added copy-pastable MCP config examples for macOS, iOS, multi-platform, tvOS, and watchOS projects to [docs/CONFIGURATION.md](docs/CONFIGURATION.md) ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)). + +### Changed + +- Environment variables are now documented as the recommended configuration method for MCP client integration, replacing the previous "legacy" designation. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)). + ## [2.2.1] - Fix AXe bundling issue. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7f3cddd0..2c6426e7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1,10 +1,12 @@ # Configuration -XcodeBuildMCP reads configuration from a project config file. The config file is optional but provides deterministic, repo-scoped behavior for every session. +XcodeBuildMCP reads configuration from environment variables and/or a project config file. Both are optional but provide deterministic behavior for every session. ## Contents +- [Environment variables](#environment-variables) - [Config file](#config-file) +- [Configuration layering](#configuration-layering) - [Session defaults](#session-defaults) - [Workflow selection](#workflow-selection) - [Build settings](#build-settings) @@ -13,13 +15,164 @@ XcodeBuildMCP reads configuration from a project config file. The config file is - [Templates](#templates) - [Telemetry](#telemetry) - [Quick reference](#quick-reference) -- [Environment variables (legacy)](#environment-variables-legacy) + +--- + +## Environment variables + +Environment variables are the recommended configuration method for MCP client integration. Set them in the `env` field of your MCP client config (e.g., `mcp_config.json` for Windsurf, `.vscode/mcp.json` for VS Code, `claude_desktop_config.json` for Claude Desktop). + +This approach works reliably across all MCP clients regardless of working directory, and avoids the need for filesystem-based config discovery. + +### General settings + +| Config option | Environment variable | +|---------------|---------------------| +| `enabledWorkflows` | `XCODEBUILDMCP_ENABLED_WORKFLOWS` (comma-separated) | +| `experimentalWorkflowDiscovery` | `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY` | +| `disableSessionDefaults` | `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS` | +| `disableXcodeAutoSync` | `XCODEBUILDMCP_DISABLE_XCODE_AUTO_SYNC` | +| `incrementalBuildsEnabled` | `INCREMENTAL_BUILDS_ENABLED` | +| `debug` | `XCODEBUILDMCP_DEBUG` | +| `sentryDisabled` | `XCODEBUILDMCP_SENTRY_DISABLED` | +| `debuggerBackend` | `XCODEBUILDMCP_DEBUGGER_BACKEND` | +| `dapRequestTimeoutMs` | `XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS` | +| `dapLogEvents` | `XCODEBUILDMCP_DAP_LOG_EVENTS` | +| `launchJsonWaitMs` | `XBMCP_LAUNCH_JSON_WAIT_MS` | +| `uiDebuggerGuardMode` | `XCODEBUILDMCP_UI_DEBUGGER_GUARD_MODE` | +| `axePath` | `XCODEBUILDMCP_AXE_PATH` | +| `iosTemplatePath` | `XCODEBUILDMCP_IOS_TEMPLATE_PATH` | +| `iosTemplateVersion` | `XCODEBUILD_MCP_IOS_TEMPLATE_VERSION` | +| `macosTemplatePath` | `XCODEBUILDMCP_MACOS_TEMPLATE_PATH` | +| `macosTemplateVersion` | `XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION` | + +### Session default settings + +| Session default | Environment variable | +|----------------|---------------------| +| `workspacePath` | `XCODEBUILDMCP_WORKSPACE_PATH` | +| `projectPath` | `XCODEBUILDMCP_PROJECT_PATH` | +| `scheme` | `XCODEBUILDMCP_SCHEME` | +| `configuration` | `XCODEBUILDMCP_CONFIGURATION` | +| `simulatorName` | `XCODEBUILDMCP_SIMULATOR_NAME` | +| `simulatorId` | `XCODEBUILDMCP_SIMULATOR_ID` | +| `simulatorPlatform` | `XCODEBUILDMCP_SIMULATOR_PLATFORM` | +| `deviceId` | `XCODEBUILDMCP_DEVICE_ID` | +| `platform` | `XCODEBUILDMCP_PLATFORM` | +| `useLatestOS` | `XCODEBUILDMCP_USE_LATEST_OS` | +| `arch` | `XCODEBUILDMCP_ARCH` | +| `suppressWarnings` | `XCODEBUILDMCP_SUPPRESS_WARNINGS` | +| `derivedDataPath` | `XCODEBUILDMCP_DERIVED_DATA_PATH` | +| `preferXcodebuild` | `XCODEBUILDMCP_PREFER_XCODEBUILD` | +| `bundleId` | `XCODEBUILDMCP_BUNDLE_ID` | + +### Example MCP configs + +Use one of these as a starting point and fill in your workspace path and scheme. + +**macOS app** + +```json +{ + "mcpServers": { + "XcodeBuildMCP": { + "command": "npx", + "args": ["-y", "xcodebuildmcp@latest", "mcp"], + "env": { + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "coverage,debugging,doctor,logging,macos,project-discovery,project-scaffolding,swift-package,ui-automation,utilities,xcode-ide", + "XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/MyApp/MyApp.xcworkspace", + "XCODEBUILDMCP_SCHEME": "MyApp", + "XCODEBUILDMCP_PLATFORM": "macOS" + } + } + } +} +``` + +> `macos` provides build/run/test/stop tools for macOS apps. No simulator workflow needed — macOS apps run natively. + +--- + +**iOS app** + +```json +{ + "mcpServers": { + "XcodeBuildMCP": { + "command": "npx", + "args": ["-y", "xcodebuildmcp@latest", "mcp"], + "env": { + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "coverage,debugging,doctor,logging,project-discovery,project-scaffolding,simulator,swift-package,ui-automation,utilities,xcode-ide", + "XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/MyApp/MyApp.xcworkspace", + "XCODEBUILDMCP_SCHEME": "MyApp", + "XCODEBUILDMCP_PLATFORM": "iOS Simulator", + "XCODEBUILDMCP_SIMULATOR_NAME": "iPhone 16 Pro" + } + } + } +} +``` + +> `simulator` provides build/run/test/install tools targeting iOS Simulator. Use `XCODEBUILDMCP_SIMULATOR_NAME` or `XCODEBUILDMCP_SIMULATOR_ID` to pin the target device. + +--- + +**iOS + macOS (multi-platform or Catalyst)** + +```json +{ + "mcpServers": { + "XcodeBuildMCP": { + "command": "npx", + "args": ["-y", "xcodebuildmcp@latest", "mcp"], + "env": { + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "coverage,debugging,doctor,logging,macos,project-discovery,project-scaffolding,simulator,swift-package,ui-automation,utilities,xcode-ide", + "XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/MyApp/MyApp.xcworkspace", + "XCODEBUILDMCP_SCHEME": "MyApp" + } + } + } +} +``` + +> Include both `simulator` and `macos` when the project supports multiple platforms. Omit `XCODEBUILDMCP_PLATFORM` to let the agent choose per-command. + +--- + +**tvOS or watchOS app** + +```json +{ + "mcpServers": { + "XcodeBuildMCP": { + "command": "npx", + "args": ["-y", "xcodebuildmcp@latest", "mcp"], + "env": { + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "debugging,doctor,logging,project-discovery,simulator,swift-package,utilities,xcode-ide", + "XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/MyApp/MyApp.xcworkspace", + "XCODEBUILDMCP_SCHEME": "MyApp", + "XCODEBUILDMCP_PLATFORM": "tvOS Simulator" + } + } + } +} +``` + +> Replace `tvOS Simulator` with `watchOS Simulator` for watchOS. Coverage and UI automation are not available on these platforms. + +--- + +You can also generate a config block interactively: + +```bash +xcodebuildmcp setup --format mcp-json +``` --- ## Config file -Create a config file at your workspace root: +The config file provides repo-scoped, version-controllable configuration. Create it at your workspace root: ``` /.xcodebuildmcp/config.yaml @@ -337,30 +490,19 @@ Notes: --- -## Environment variables (legacy) +## Configuration layering -Environment variables are supported for backwards compatibility but the config file is preferred. +When multiple configuration sources are present, they are merged with clear precedence: -| Config option | Environment variable | -|---------------|---------------------| -| `enabledWorkflows` | `XCODEBUILDMCP_ENABLED_WORKFLOWS` (comma-separated) | -| `experimentalWorkflowDiscovery` | `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY` | -| `disableSessionDefaults` | `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS` | -| `incrementalBuildsEnabled` | `INCREMENTAL_BUILDS_ENABLED` | -| `debug` | `XCODEBUILDMCP_DEBUG` | -| `sentryDisabled` | `XCODEBUILDMCP_SENTRY_DISABLED` | -| `debuggerBackend` | `XCODEBUILDMCP_DEBUGGER_BACKEND` | -| `dapRequestTimeoutMs` | `XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS` | -| `dapLogEvents` | `XCODEBUILDMCP_DAP_LOG_EVENTS` | -| `launchJsonWaitMs` | `XBMCP_LAUNCH_JSON_WAIT_MS` | -| `uiDebuggerGuardMode` | `XCODEBUILDMCP_UI_DEBUGGER_GUARD_MODE` | -| `axePath` | `XCODEBUILDMCP_AXE_PATH` | -| `iosTemplatePath` | `XCODEBUILDMCP_IOS_TEMPLATE_PATH` | -| `iosTemplateVersion` | `XCODEBUILD_MCP_IOS_TEMPLATE_VERSION` | -| `macosTemplatePath` | `XCODEBUILDMCP_MACOS_TEMPLATE_PATH` | -| `macosTemplateVersion` | `XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION` | +1. **`session_set_defaults` tool** (highest) — agent runtime overrides, set during a session +2. **Config file** — project-local config (`config.yaml`), committed to repo +3. **Environment variables** (lowest) — MCP client integration, set in `mcp_config.json` + +This follows the same pattern as tools like `git config` (`--flag` > `--local` > `--global`). Each layer serves a different context: -Config file takes precedence over environment variables when both are set. +- **Env vars** are the portable MCP client integration path — they work regardless of working directory and are supported by every MCP client. +- **Config file** is for repo-scoped, version-controlled settings and interactive CLI usage. +- **Tool calls** are for agent-driven runtime adjustments. --- diff --git a/src/cli/commands/__tests__/setup.test.ts b/src/cli/commands/__tests__/setup.test.ts index 189bf49b..298c933c 100644 --- a/src/cli/commands/__tests__/setup.test.ts +++ b/src/cli/commands/__tests__/setup.test.ts @@ -248,6 +248,91 @@ describe('setup command', () => { ).rejects.toThrow('Setup prerequisites failed'); }); + it('outputs MCP config JSON when format is mcp-json', async () => { + const fs = createMockFileSystemExecutor({ + existsSync: () => false, + stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }), + readdir: async (targetPath) => { + if (targetPath === cwd) { + return [ + { + name: 'App.xcworkspace', + isDirectory: () => true, + isSymbolicLink: () => false, + }, + ]; + } + + return []; + }, + readFile: async () => '', + writeFile: async () => {}, + }); + + const executor: CommandExecutor = async (command) => { + if (command.includes('--json')) { + return createMockCommandResponse({ + success: true, + output: JSON.stringify({ + devices: { + 'iOS 17.0': [ + { + name: 'iPhone 15', + udid: 'SIM-1', + state: 'Shutdown', + isAvailable: true, + }, + ], + }, + }), + }); + } + + if (command[0] === 'xcrun') { + return createMockCommandResponse({ + success: true, + output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`, + }); + } + + return createMockCommandResponse({ + success: true, + output: `Information about workspace "App":\n Schemes:\n App`, + }); + }; + + const result = await runSetupWizard({ + cwd, + fs, + executor, + prompter: createTestPrompter(), + quietOutput: true, + outputFormat: 'mcp-json', + }); + + expect(result.configPath).toBeUndefined(); + expect(result.mcpConfigJson).toBeDefined(); + + const parsed = JSON.parse(result.mcpConfigJson!) as { + mcpServers: { + XcodeBuildMCP: { + command: string; + args: string[]; + env: Record; + }; + }; + }; + + const serverConfig = parsed.mcpServers.XcodeBuildMCP; + expect(serverConfig.command).toBe('npx'); + expect(serverConfig.args).toEqual(['-y', 'xcodebuildmcp@latest', 'mcp']); + expect(serverConfig.env.XCODEBUILDMCP_ENABLED_WORKFLOWS).toBeDefined(); + expect(serverConfig.env.XCODEBUILDMCP_WORKSPACE_PATH).toBe(path.join(cwd, 'App.xcworkspace')); + expect(serverConfig.env.XCODEBUILDMCP_SCHEME).toBe('App'); + expect(serverConfig.env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1'); + expect(serverConfig.env.XCODEBUILDMCP_SIMULATOR_NAME).toBe('iPhone 15'); + }); + it('fails in non-interactive mode', async () => { Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true }); diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index 00a660b1..a97968fe 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -34,17 +34,21 @@ interface SetupSelection { simulatorName: string; } +type SetupOutputFormat = 'yaml' | 'mcp-json'; + interface SetupDependencies { cwd: string; fs: FileSystemExecutor; executor: CommandExecutor; prompter: Prompter; quietOutput: boolean; + outputFormat: SetupOutputFormat; } export interface SetupRunResult { - configPath: string; + configPath?: string; changedFields: string[]; + mcpConfigJson?: string; } const WORKFLOW_EXCLUDES = new Set(['session-management', 'workflow-discovery']); @@ -480,6 +484,44 @@ async function collectSetupSelection( }; } +function selectionToMcpConfigJson(selection: SetupSelection): string { + const env: Record = {}; + + if (selection.enabledWorkflows.length > 0) { + env.XCODEBUILDMCP_ENABLED_WORKFLOWS = selection.enabledWorkflows.join(','); + } + + if (selection.debug) { + env.XCODEBUILDMCP_DEBUG = 'true'; + } + + if (selection.sentryDisabled) { + env.XCODEBUILDMCP_SENTRY_DISABLED = 'true'; + } + + if (selection.workspacePath) { + env.XCODEBUILDMCP_WORKSPACE_PATH = selection.workspacePath; + } else if (selection.projectPath) { + env.XCODEBUILDMCP_PROJECT_PATH = selection.projectPath; + } + + env.XCODEBUILDMCP_SCHEME = selection.scheme; + env.XCODEBUILDMCP_SIMULATOR_ID = selection.simulatorId; + env.XCODEBUILDMCP_SIMULATOR_NAME = selection.simulatorName; + + const mcpConfig = { + mcpServers: { + XcodeBuildMCP: { + command: 'npx', + args: ['-y', 'xcodebuildmcp@latest', 'mcp'], + env, + }, + }, + }; + + return JSON.stringify(mcpConfig, null, 2); +} + export async function runSetupWizard(deps?: Partial): Promise { const isTTY = isInteractiveTTY(); if (!isTTY) { @@ -492,16 +534,28 @@ export async function runSetupWizard(deps?: Partial): Promise executor: deps?.executor ?? getDefaultCommandExecutor(), prompter: deps?.prompter ?? createPrompter(), quietOutput: deps?.quietOutput ?? false, + outputFormat: deps?.outputFormat ?? 'yaml', }; + const isMcpJson = resolvedDeps.outputFormat === 'mcp-json'; + if (!resolvedDeps.quietOutput) { clack.intro('XcodeBuildMCP Setup'); - clack.log.info( - 'This wizard will configure your project defaults for XcodeBuildMCP.\n' + - 'You will select a project or workspace, scheme, simulator, and\n' + - 'which workflows to enable. Settings are saved to\n' + - '.xcodebuildmcp/config.yaml in your project directory.', - ); + if (isMcpJson) { + clack.log.info( + 'This wizard will configure your project defaults for XcodeBuildMCP.\n' + + 'You will select a project or workspace, scheme, simulator, and\n' + + 'which workflows to enable. A ready-to-paste MCP config JSON\n' + + 'block will be printed at the end.', + ); + } else { + clack.log.info( + 'This wizard will configure your project defaults for XcodeBuildMCP.\n' + + 'You will select a project or workspace, scheme, simulator, and\n' + + 'which workflows to enable. Settings are saved to\n' + + '.xcodebuildmcp/config.yaml in your project directory.', + ); + } } await ensureSetupPrerequisites({ @@ -515,6 +569,26 @@ export async function runSetupWizard(deps?: Partial): Promise const selection = await collectSetupSelection(beforeConfig, resolvedDeps); + if (isMcpJson) { + const mcpConfigJson = selectionToMcpConfigJson(selection); + + if (!resolvedDeps.quietOutput) { + clack.log.info( + 'Copy the following JSON block into your MCP client config\n' + + '(e.g. mcp_config.json for Windsurf, .vscode/mcp.json for VS Code,\n' + + 'claude_desktop_config.json for Claude Desktop):', + ); + // Print raw JSON to stdout so it can be piped/copied + console.log(mcpConfigJson); + clack.outro('Setup complete.'); + } + + return { + changedFields: [], + mcpConfigJson, + }; + } + const deleteSessionDefaultKeys: Array<'projectPath' | 'workspacePath'> = selection.workspacePath != null ? ['projectPath'] : ['workspacePath']; @@ -570,10 +644,17 @@ export async function runSetupWizard(deps?: Partial): Promise export function registerSetupCommand(app: Argv): void { app.command( 'setup', - 'Interactively create or update .xcodebuildmcp/config.yaml', - (yargs) => yargs, - async () => { - await runSetupWizard(); + 'Interactively configure XcodeBuildMCP project defaults', + (yargs) => + yargs.option('format', { + type: 'string', + choices: ['yaml', 'mcp-json'] as const, + default: 'yaml', + describe: + 'Output format: yaml writes .xcodebuildmcp/config.yaml, mcp-json prints a ready-to-paste MCP client config block', + }), + async (argv) => { + await runSetupWizard({ outputFormat: argv.format as SetupOutputFormat }); }, ); } diff --git a/src/utils/__tests__/config-store.test.ts b/src/utils/__tests__/config-store.test.ts index c815faef..c1a1c275 100644 --- a/src/utils/__tests__/config-store.test.ts +++ b/src/utils/__tests__/config-store.test.ts @@ -262,6 +262,55 @@ describe('config-store', () => { expect(getConfig().sessionDefaults?.scheme).toBe('App'); }); + it('reads session defaults from env vars', async () => { + const env = { + XCODEBUILDMCP_WORKSPACE_PATH: '/path/to/App.xcworkspace', + XCODEBUILDMCP_SCHEME: 'MyApp', + XCODEBUILDMCP_PLATFORM: 'macOS', + XCODEBUILDMCP_SUPPRESS_WARNINGS: 'true', + XCODEBUILDMCP_DERIVED_DATA_PATH: '/tmp/dd', + XCODEBUILDMCP_USE_LATEST_OS: 'true', + XCODEBUILDMCP_ARCH: 'arm64', + XCODEBUILDMCP_SIMULATOR_NAME: 'iPhone 17', + XCODEBUILDMCP_BUNDLE_ID: 'com.example.app', + }; + + await initConfigStore({ cwd, fs: createFs(), env }); + + const config = getConfig(); + expect(config.sessionDefaults?.workspacePath).toBe('/path/to/App.xcworkspace'); + expect(config.sessionDefaults?.scheme).toBe('MyApp'); + expect(config.sessionDefaults?.platform).toBe('macOS'); + expect(config.sessionDefaults?.suppressWarnings).toBe(true); + expect(config.sessionDefaults?.derivedDataPath).toBe('/tmp/dd'); + expect(config.sessionDefaults?.useLatestOS).toBe(true); + expect(config.sessionDefaults?.arch).toBe('arm64'); + expect(config.sessionDefaults?.simulatorName).toBe('iPhone 17'); + expect(config.sessionDefaults?.bundleId).toBe('com.example.app'); + }); + + it('file config session defaults take precedence over env var session defaults', async () => { + const yaml = [ + 'schemaVersion: 1', + 'sessionDefaults:', + ' scheme: "FromFile"', + ' workspacePath: "./FromFile.xcworkspace"', + '', + ].join('\n'); + const env = { + XCODEBUILDMCP_SCHEME: 'FromEnv', + XCODEBUILDMCP_WORKSPACE_PATH: '/env/path/App.xcworkspace', + XCODEBUILDMCP_PLATFORM: 'iOS', + }; + + await initConfigStore({ cwd, fs: createFs(yaml), env }); + + const config = getConfig(); + expect(config.sessionDefaults?.scheme).toBe('FromFile'); + expect(config.sessionDefaults?.workspacePath).toBe('/repo/FromFile.xcworkspace'); + expect(config.sessionDefaults?.platform).toBe('iOS'); + }); + it('keeps non-session config immutable after init when persisting active profile', async () => { let persistedYaml = 'schemaVersion: 1\n'; const fs = createMockFileSystemExecutor({ diff --git a/src/utils/config-store.ts b/src/utils/config-store.ts index 3e23ed9b..5b70c5f8 100644 --- a/src/utils/config-store.ts +++ b/src/utils/config-store.ts @@ -233,6 +233,57 @@ function readEnvConfig(env: NodeJS.ProcessEnv): RuntimeConfigOverrides { return config; } +function readEnvSessionDefaults(env: NodeJS.ProcessEnv): Partial | undefined { + const defaults: Partial = {}; + let hasAny = false; + + function setString(key: K, value: string | undefined): void { + if (value) { + (defaults as Record)[key] = value; + hasAny = true; + } + } + + function setBool(key: K, value: string | undefined): void { + const parsed = parseBoolean(value); + if (parsed !== undefined) { + (defaults as Record)[key] = parsed; + hasAny = true; + } + } + + setString('workspacePath', env.XCODEBUILDMCP_WORKSPACE_PATH); + setString('projectPath', env.XCODEBUILDMCP_PROJECT_PATH); + setString('scheme', env.XCODEBUILDMCP_SCHEME); + setString('configuration', env.XCODEBUILDMCP_CONFIGURATION); + setString('simulatorName', env.XCODEBUILDMCP_SIMULATOR_NAME); + setString('simulatorId', env.XCODEBUILDMCP_SIMULATOR_ID); + setString('deviceId', env.XCODEBUILDMCP_DEVICE_ID); + setString('derivedDataPath', env.XCODEBUILDMCP_DERIVED_DATA_PATH); + setString('platform', env.XCODEBUILDMCP_PLATFORM); + setString('bundleId', env.XCODEBUILDMCP_BUNDLE_ID); + setBool('useLatestOS', env.XCODEBUILDMCP_USE_LATEST_OS); + setBool('suppressWarnings', env.XCODEBUILDMCP_SUPPRESS_WARNINGS); + setBool('preferXcodebuild', env.XCODEBUILDMCP_PREFER_XCODEBUILD); + + const simulatorPlatform = env.XCODEBUILDMCP_SIMULATOR_PLATFORM; + if (simulatorPlatform) { + const valid = ['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator']; + if (valid.includes(simulatorPlatform)) { + defaults.simulatorPlatform = simulatorPlatform as SessionDefaults['simulatorPlatform']; + hasAny = true; + } + } + + const arch = env.XCODEBUILDMCP_ARCH; + if (arch === 'arm64' || arch === 'x86_64') { + defaults.arch = arch; + hasAny = true; + } + + return hasAny ? defaults : undefined; +} + function resolveFromLayers(opts: { key: keyof RuntimeConfigOverrides; overrides?: RuntimeConfigOverrides; @@ -270,11 +321,13 @@ function resolveFromLayers(opts: { function resolveSessionDefaults(opts: { overrides?: RuntimeConfigOverrides; fileConfig?: ProjectConfig; + env?: NodeJS.ProcessEnv; }): Partial | undefined { const overrideDefaults = opts.overrides?.sessionDefaults; const fileDefaults = opts.fileConfig?.sessionDefaults; - if (!overrideDefaults && !fileDefaults) return undefined; - return { ...(fileDefaults ?? {}), ...(overrideDefaults ?? {}) }; + const envDefaults = readEnvSessionDefaults(opts.env ?? process.env); + if (!overrideDefaults && !fileDefaults && !envDefaults) return undefined; + return { ...(envDefaults ?? {}), ...(fileDefaults ?? {}), ...(overrideDefaults ?? {}) }; } function resolveSessionDefaultsProfiles(opts: { @@ -501,6 +554,7 @@ function resolveConfig(opts: { sessionDefaults: resolveSessionDefaults({ overrides: opts.overrides, fileConfig: opts.fileConfig, + env: opts.env, }), sessionDefaultsProfiles: resolveSessionDefaultsProfiles({ overrides: opts.overrides,