import process from 'node:process';
import {
initConfigStore,
getConfig,
type RuntimeConfigOverrides,
type ResolvedRuntimeConfig,
} from '../utils/config-store.ts';
import { sessionStore, type SessionDefaults } from '../utils/session-store.ts';
import { getDefaultFileSystemExecutor } from '../utils/command.ts';
import { log } from '../utils/logger.ts';
import type { FileSystemExecutor } from '../utils/FileSystemExecutor.ts';
import { scheduleSimulatorDefaultsRefresh } from '../utils/simulator-defaults-refresh.ts';
export type RuntimeKind = 'cli' | 'daemon' | 'mcp';
export interface BootstrapRuntimeOptions {
runtime: RuntimeKind;
cwd?: string;
fs?: FileSystemExecutor;
configOverrides?: RuntimeConfigOverrides;
}
export interface BootstrappedRuntime {
runtime: RuntimeKind;
cwd: string;
config: ResolvedRuntimeConfig;
}
export interface BootstrapRuntimeResult {
runtime: BootstrappedRuntime;
configFound: boolean;
configPath?: string;
notices: string[];
}
interface MCPSessionHydrationResult {
hydrated: boolean;
refreshScheduled: boolean;
}
/**
* Hydrates MCP session defaults and reports whether a background simulator refresh was scheduled.
*/
function hydrateSessionDefaultsForMcp(
defaults: Partial<SessionDefaults> | undefined,
): MCPSessionHydrationResult {
const hydratedDefaults = { ...(defaults ?? {}) };
if (Object.keys(hydratedDefaults).length === 0) {
return { hydrated: false, refreshScheduled: false };
}
sessionStore.setDefaults(hydratedDefaults);
const revision = sessionStore.getRevision();
const refreshScheduled = scheduleSimulatorDefaultsRefresh({
expectedRevision: revision,
reason: 'startup-hydration',
persist: true,
simulatorId: hydratedDefaults.simulatorId,
simulatorName: hydratedDefaults.simulatorName,
recomputePlatform: true,
});
return { hydrated: true, refreshScheduled };
}
export async function bootstrapRuntime(
opts: BootstrapRuntimeOptions,
): Promise<BootstrapRuntimeResult> {
process.env.XCODEBUILDMCP_RUNTIME = opts.runtime;
const cwd = opts.cwd ?? process.cwd();
const fs = opts.fs ?? getDefaultFileSystemExecutor();
const configResult = await initConfigStore({
cwd,
fs,
overrides: opts.configOverrides,
});
if (configResult.found) {
log('info', `Loaded project config from ${configResult.path} (cwd: ${cwd})`);
} else {
log('info', `No project config found (cwd: ${cwd}).`);
}
const config = getConfig();
if (opts.runtime === 'mcp') {
const hydration = hydrateSessionDefaultsForMcp(config.sessionDefaults);
if (hydration.hydrated && hydration.refreshScheduled) {
log('info', '[Session] Hydrated MCP session defaults; simulator metadata refresh scheduled.');
} else if (hydration.hydrated) {
log(
'info',
'[Session] Hydrated MCP session defaults; simulator metadata refresh not scheduled.',
);
}
}
return {
runtime: {
runtime: opts.runtime,
cwd,
config,
},
configFound: configResult.found,
configPath: configResult.path,
notices: configResult.notices,
};
}