@mgcrea/mcp-ios-simulator 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { N as BUILD_INFO, a as createServer, n as loadConfig, t as defaultConfigPath } from "./config-D0JgmT_U.js";
2
+ import { N as BUILD_INFO, a as createServer, n as loadConfig, t as defaultConfigPath } from "./config-HmqayUrt.js";
3
3
  import { ZodError } from "zod";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
5
- //#region src/cli.ts
5
+ // src/cli.ts
6
6
  const stderrLogger = {
7
7
  debug: (...args) => {
8
8
  if (process.env.IOS_SIMULATOR_DEBUG) console.error("[ios-simulator-mcp]", ...args);
@@ -41,7 +41,6 @@ main().catch((err) => {
41
41
  console.error(`[ios-simulator-mcp] fatal: ${describeFatal(err)}`);
42
42
  process.exit(1);
43
43
  });
44
- //#endregion
45
44
  export {};
46
45
 
47
46
  //# sourceMappingURL=cli.js.map
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { CommandError, CommandTimeoutError, DEFAULT_CRASH_KINDS, IosError, IosError as IosError$1, ToolchainError, WdaClient, WdaError, WdaUnavailableError, assertNoShellMetachars, crashKind, crashProcessName, createArgs, createExec, readLogFile, registerInputTools, registerScreenTools, summarizeCrashReport, wrap } from "@mgcrea/mcp-ios-core";
5
5
  import { mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
@@ -7,7 +7,7 @@ import { homedir, tmpdir } from "node:os";
7
7
  import { z } from "zod";
8
8
  import { spawn } from "node:child_process";
9
9
  import { fileURLToPath } from "node:url";
10
- //#region src/build-info.ts
10
+ // src/build-info.ts
11
11
  const readPackageJson = () => {
12
12
  try {
13
13
  const pkgUrl = new URL("../package.json", import.meta.url);
@@ -23,11 +23,10 @@ const pkg = readPackageJson();
23
23
  const BUILD_INFO = {
24
24
  name: pkg.name,
25
25
  version: pkg.version,
26
- gitCommit: "b6a6636",
27
- gitCommitDate: "2026-10-08T23:42:11+02:00"
26
+ gitCommit: "5584c7c",
27
+ gitCommitDate: "2026-10-29T22:11:35+01:00"
28
28
  };
29
- //#endregion
30
- //#region src/client/errors.ts
29
+ // src/client/errors.ts
31
30
  /** Raising the budget, or noticing the simulator died mid-call. */
32
31
  const TIMEOUT_REMEDY = "Raise IOS_SIMULATOR_TIMEOUT_MS if this is a slow operation (a boot or a large `install` genuinely takes tens of seconds), or check the simulator is still booted.";
33
32
  const TOOLCHAIN_REMEDY = "Install Xcode and run `xcode-select --install`, or point IOS_SIMULATOR_XCRUN_PATH at a different xcrun.";
@@ -99,8 +98,7 @@ var WritesDisabledError = class extends IosError {
99
98
  };
100
99
  /** A shut-down simulator captures nothing, and that is the usual cause here. */
101
100
  const EMPTY_SCREENSHOT_REMEDY = "The simulator produced an empty capture. Check it is actually booted with ios_simulator_diagnostics, and that its runtime is installed.";
102
- //#endregion
103
- //#region src/client/display.ts
101
+ // src/client/display.ts
104
102
  /**
105
103
  * Screen geometry for a simulator, which has no `devicectl device info displays`.
106
104
  *
@@ -191,10 +189,11 @@ const pngDimensions = (png) => {
191
189
  * screen, so mapping it would be a lie.
192
190
  */
193
191
  const toDisplayInfo = (profile, capture) => {
194
- const scale = profile.mainScreenScale ?? 1;
192
+ const scale = profile.mainScreenScale;
195
193
  const pixelWidth = capture?.width ?? profile.mainScreenWidth;
196
194
  const pixelHeight = capture?.height ?? profile.mainScreenHeight;
197
195
  if (!pixelWidth || !pixelHeight) throw new IosError$1("Could not determine the simulator's screen geometry.", { remedy: "The device type's profile.plist could not be read and no screenshot was available. Run ios_simulator_diagnostics — an unavailable runtime is the usual cause." });
196
+ if (!scale) throw new IosError$1("Could not determine the simulator's screen scale.", { remedy: "The device type's profile.plist could not be read, so points cannot be told from pixels. Run ios_simulator_diagnostics — an unavailable runtime is the usual cause." });
198
197
  const landscape = profile.mainScreenWidth !== void 0 && profile.mainScreenHeight !== void 0 ? pixelWidth > pixelHeight && profile.mainScreenWidth < profile.mainScreenHeight : pixelWidth > pixelHeight;
199
198
  return {
200
199
  pixelWidth,
@@ -205,8 +204,7 @@ const toDisplayInfo = (profile, capture) => {
205
204
  orientation: capture ? landscape ? "landscape" : "portrait" : "unknown"
206
205
  };
207
206
  };
208
- //#endregion
209
- //#region src/client/logs.ts
207
+ // src/client/logs.ts
210
208
  const defaultProcessAlive = (pid) => {
211
209
  try {
212
210
  process.kill(pid, 0);
@@ -251,8 +249,7 @@ var LaunchCaptures = class {
251
249
  };
252
250
  /** True when `path` exists on this Mac. */
253
251
  const exists = async (path) => stat(path).then(() => true, () => false);
254
- //#endregion
255
- //#region src/client/shape.ts
252
+ // src/client/shape.ts
256
253
  /**
257
254
  * Flatten the runtime-keyed map `list devices` returns.
258
255
  *
@@ -307,8 +304,7 @@ const toListRow = (sim) => ({
307
304
  ...sim.available ? {} : { available: false },
308
305
  ...sim.unavailableReason ? { unavailableReason: sim.unavailableReason } : {}
309
306
  });
310
- //#endregion
311
- //#region src/client/simctl.ts
307
+ // src/client/simctl.ts
312
308
  /**
313
309
  * Pull the human sentence out of a CoreSimulator error envelope.
314
310
  *
@@ -358,6 +354,12 @@ var Simctl = class {
358
354
  exec;
359
355
  /** Device types change only when Xcode is updated; re-reading is pure cost. */
360
356
  deviceTypes;
357
+ /**
358
+ * Read once, like device types: both change only when Xcode or a runtime is
359
+ * installed, and every tool call resolves a simulator. The cost is that a
360
+ * runtime added or deleted while this server runs is seen after a restart.
361
+ */
362
+ runtimes;
361
363
  constructor(opts) {
362
364
  this.opts = opts;
363
365
  this.exec = opts.exec ?? createExec({
@@ -367,9 +369,9 @@ var Simctl = class {
367
369
  });
368
370
  }
369
371
  /** Raw stdout, for the subcommands that answer in prose. */
370
- async run(args) {
372
+ async run(args, timeoutMs = this.opts.timeoutMs) {
371
373
  this.opts.logger?.debug?.("simctl", ...args);
372
- const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args], this.opts.timeoutMs);
374
+ const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args], timeoutMs);
373
375
  return stdout;
374
376
  }
375
377
  /**
@@ -426,7 +428,8 @@ var Simctl = class {
426
428
  return (await this.runJson(["list", "devices"])).devices ?? {};
427
429
  }
428
430
  async listRuntimes() {
429
- return (await this.runJson(["list", "runtimes"])).runtimes ?? [];
431
+ this.runtimes ??= (await this.runJson(["list", "runtimes"])).runtimes ?? [];
432
+ return this.runtimes;
430
433
  }
431
434
  async listDeviceTypes() {
432
435
  this.deviceTypes ??= (await this.runJson(["list", "devicetypes"])).devicetypes ?? [];
@@ -436,6 +439,16 @@ var Simctl = class {
436
439
  assertNotBulkTarget(udid);
437
440
  await this.run(["boot", udid]);
438
441
  }
442
+ /**
443
+ * Wait until a booting simulator has finished, or `timeoutMs` passes (a
444
+ * `CommandTimeoutError`). Not the state in `list`: measured on an iOS 27.0
445
+ * runtime, that reads "Booted" 0.4s after `boot` returns, while this returns
446
+ * 17s later, once the system is up.
447
+ */
448
+ async bootstatus(udid, timeoutMs) {
449
+ assertNotBulkTarget(udid);
450
+ await this.run(["bootstatus", udid], timeoutMs);
451
+ }
439
452
  async shutdown(udid) {
440
453
  assertNotBulkTarget(udid);
441
454
  await this.run(["shutdown", udid]);
@@ -702,8 +715,7 @@ var Simctl = class {
702
715
  }
703
716
  }
704
717
  };
705
- //#endregion
706
- //#region src/client/simulator.ts
718
+ // src/client/simulator.ts
707
719
  /**
708
720
  * What to say when WebDriverAgent fails, on a simulator.
709
721
  *
@@ -767,6 +779,13 @@ var SimulatorClient = class {
767
779
  return simulators;
768
780
  }
769
781
  /**
782
+ * Drop the cached list. Called after a tool changes a simulator's state, so
783
+ * the next call within the cache window does not act on the state before it.
784
+ */
785
+ forgetSimulators() {
786
+ this.cache = void 0;
787
+ }
788
+ /**
770
789
  * Resolve a hint to exactly one simulator, or refuse.
771
790
  *
772
791
  * The device server's rule — never guess between two — transfers, but
@@ -835,7 +854,7 @@ var SimulatorClient = class {
835
854
  exec: this.exec,
836
855
  timeoutMs: this.execTimeoutMs
837
856
  }) : {};
838
- if (id) this.profiles.set(id, profile);
857
+ if (id && profile.mainScreenScale !== void 0) this.profiles.set(id, profile);
839
858
  }
840
859
  const capture = capturePng ? pngDimensions(Buffer.from(capturePng, "base64")) : void 0;
841
860
  return toDisplayInfo(profile, capture);
@@ -894,6 +913,10 @@ var SimulatorClient = class {
894
913
  wdaUrl() {
895
914
  return this.opts.wdaUrl ?? `http://127.0.0.1:${this.opts.wdaPort}`;
896
915
  }
916
+ /** The simulator `resolveTarget` falls back to when no hint is given. */
917
+ get defaultSimulatorId() {
918
+ return this.opts.defaultSimulatorId;
919
+ }
897
920
  get wdaPort() {
898
921
  return this.opts.wdaPort;
899
922
  }
@@ -912,8 +935,7 @@ var SimulatorClient = class {
912
935
  return client;
913
936
  }
914
937
  };
915
- //#endregion
916
- //#region src/tools/util.ts
938
+ // src/tools/util.ts
917
939
  /**
918
940
  * How the shared tools name themselves and each other in this server.
919
941
  *
@@ -944,8 +966,7 @@ const { bundleIdArg, confirmArg, detailArg, screenshotArg, settleArg, xArg, yArg
944
966
  * worse than either alone.
945
967
  */
946
968
  const deviceArg = args.targetArg;
947
- //#endregion
948
- //#region src/tools/apps.ts
969
+ // src/tools/apps.ts
949
970
  /** `io.mgcrea.Canopy: 12345` is the whole of what `simctl launch` prints. */
950
971
  const parsePid = (output) => {
951
972
  const pid = /:\s*(\d+)\s*$/.exec(output.trim())?.[1];
@@ -1130,8 +1151,7 @@ const registerAppTools = (server, client, launchArgs, outputDir) => {
1130
1151
  return { opened: url };
1131
1152
  }));
1132
1153
  };
1133
- //#endregion
1134
- //#region src/tools/diagnostics.ts
1154
+ // src/tools/diagnostics.ts
1135
1155
  /**
1136
1156
  * Which simulator owns the WebDriverAgent port.
1137
1157
  *
@@ -1219,7 +1239,7 @@ const diagnose = async (client, ctx, deviceHint) => {
1219
1239
  try {
1220
1240
  target = await client.resolveTarget(deviceHint);
1221
1241
  } catch (err) {
1222
- if (booted.length > 0) {
1242
+ if (booted.length > 0 || (deviceHint ?? client.defaultSimulatorId) !== void 0) {
1223
1243
  problems.push(err instanceof Error ? err.message : String(err));
1224
1244
  const remedy = err.remedy;
1225
1245
  if (remedy) nextSteps.push(remedy);
@@ -1289,8 +1309,7 @@ const registerDiagnosticsTools = (server, client, ctx) => {
1289
1309
  text: JSON.stringify(await diagnose(client, ctx))
1290
1310
  }] }));
1291
1311
  };
1292
- //#endregion
1293
- //#region src/tools/environment.ts
1312
+ // src/tools/environment.ts
1294
1313
  /** simctl's own list, verbatim — a value it does not accept is not worth guessing at. */
1295
1314
  const PRIVACY_SERVICES = [
1296
1315
  "all",
@@ -1366,6 +1385,26 @@ const registerEnvironmentTools = (server, client) => {
1366
1385
  }
1367
1386
  }, async ({ device, appearance, content_size, increase_contrast, status_bar, clear_status_bar, location, clear_location, permission }) => wrap(async () => {
1368
1387
  const target = client.requireBooted(await client.resolveTarget(device));
1388
+ const statusBarFlags = status_bar ? [
1389
+ ...status_bar.time ? ["--time", status_bar.time] : [],
1390
+ ...status_bar.wifi_bars !== void 0 ? [
1391
+ "--wifiMode",
1392
+ "active",
1393
+ "--wifiBars",
1394
+ String(status_bar.wifi_bars)
1395
+ ] : [],
1396
+ ...status_bar.cellular_bars !== void 0 ? [
1397
+ "--cellularMode",
1398
+ "active",
1399
+ "--cellularBars",
1400
+ String(status_bar.cellular_bars)
1401
+ ] : [],
1402
+ ...status_bar.operator_name !== void 0 ? ["--operatorName", status_bar.operator_name] : [],
1403
+ ...status_bar.battery_level !== void 0 ? ["--batteryLevel", String(status_bar.battery_level)] : [],
1404
+ ...status_bar.battery_state ? ["--batteryState", status_bar.battery_state] : []
1405
+ ] : [];
1406
+ if (status_bar && statusBarFlags.length === 0) throw new IosError$1("`status_bar` was given with no fields to override.", { remedy: "Set at least one field, or use `clear_status_bar` to drop the overrides." });
1407
+ if (permission && permission.action !== "reset" && !permission.bundle_id) throw new IosError$1(`\`${permission.action}\` needs a bundle_id.`, { remedy: "Pass the app's bundle id, or use `reset`, which does not need one." });
1369
1408
  const applied = [];
1370
1409
  if (appearance) {
1371
1410
  await client.simctl.uiSet(target.id, "appearance", appearance);
@@ -1384,26 +1423,7 @@ const registerEnvironmentTools = (server, client) => {
1384
1423
  applied.push("status_bar cleared");
1385
1424
  }
1386
1425
  if (status_bar) {
1387
- const flags = [
1388
- ...status_bar.time ? ["--time", status_bar.time] : [],
1389
- ...status_bar.wifi_bars !== void 0 ? [
1390
- "--wifiMode",
1391
- "active",
1392
- "--wifiBars",
1393
- String(status_bar.wifi_bars)
1394
- ] : [],
1395
- ...status_bar.cellular_bars !== void 0 ? [
1396
- "--cellularMode",
1397
- "active",
1398
- "--cellularBars",
1399
- String(status_bar.cellular_bars)
1400
- ] : [],
1401
- ...status_bar.operator_name !== void 0 ? ["--operatorName", status_bar.operator_name] : [],
1402
- ...status_bar.battery_level !== void 0 ? ["--batteryLevel", String(status_bar.battery_level)] : [],
1403
- ...status_bar.battery_state ? ["--batteryState", status_bar.battery_state] : []
1404
- ];
1405
- if (flags.length === 0) throw new IosError$1("`status_bar` was given with no fields to override.", { remedy: "Set at least one field, or use `clear_status_bar` to drop the overrides." });
1406
- await client.simctl.statusBarOverride(target.id, flags);
1426
+ await client.simctl.statusBarOverride(target.id, statusBarFlags);
1407
1427
  applied.push("status_bar overridden");
1408
1428
  }
1409
1429
  if (clear_location) {
@@ -1415,17 +1435,21 @@ const registerEnvironmentTools = (server, client) => {
1415
1435
  applied.push(`location=${location.latitude},${location.longitude}`);
1416
1436
  }
1417
1437
  if (permission) {
1418
- if (permission.action !== "reset" && !permission.bundle_id) throw new IosError$1(`\`${permission.action}\` needs a bundle_id.`, { remedy: "Pass the app's bundle id, or use `reset`, which does not need one." });
1419
1438
  await client.simctl.privacy(target.id, permission.action, permission.service, permission.bundle_id);
1420
1439
  applied.push(`permission ${permission.action} ${permission.service}`);
1421
1440
  }
1422
1441
  if (applied.length === 0) throw new IosError$1("Nothing to do — no field was given.", { remedy: "Set at least one of appearance, content_size, status_bar, location, permission." });
1442
+ const [appearanceNow, contentSize, statusBar] = await Promise.all([
1443
+ client.simctl.uiGet(target.id, "appearance"),
1444
+ client.simctl.uiGet(target.id, "content_size"),
1445
+ client.simctl.statusBarList(target.id)
1446
+ ]);
1423
1447
  return {
1424
1448
  applied,
1425
1449
  state: {
1426
- appearance: await client.simctl.uiGet(target.id, "appearance"),
1427
- contentSize: await client.simctl.uiGet(target.id, "content_size"),
1428
- statusBar: await client.simctl.statusBarList(target.id)
1450
+ appearance: appearanceNow,
1451
+ contentSize,
1452
+ statusBar
1429
1453
  }
1430
1454
  };
1431
1455
  }));
@@ -1475,11 +1499,7 @@ const registerEnvironmentTools = (server, client) => {
1475
1499
  };
1476
1500
  }));
1477
1501
  };
1478
- //#endregion
1479
- //#region src/tools/lifecycle.ts
1480
- /** Booting is slow and visible; this is how long `power` waits to see it land. */
1481
- const BOOT_POLL_MS = 500;
1482
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1502
+ // src/tools/lifecycle.ts
1483
1503
  const registerListTool = (server, client) => {
1484
1504
  server.registerTool("ios_simulator_list", {
1485
1505
  title: "iOS Simulator: List",
@@ -1524,13 +1544,14 @@ const registerLifecycleTools = (server, client) => {
1524
1544
  }, async ({ device, state, open_window, wait_ms }) => wrap(async () => {
1525
1545
  const target = await client.resolveTarget(device);
1526
1546
  if (state === "shutdown") {
1527
- if (target.state !== "Booted") return {
1547
+ if (target.state === "Shutdown") return {
1528
1548
  udid: target.id,
1529
1549
  name: target.name,
1530
1550
  state: "Shutdown",
1531
1551
  alreadyThere: true
1532
1552
  };
1533
1553
  await client.simctl.shutdown(target.id);
1554
+ client.forgetSimulators();
1534
1555
  return {
1535
1556
  udid: target.id,
1536
1557
  name: target.name,
@@ -1538,7 +1559,10 @@ const registerLifecycleTools = (server, client) => {
1538
1559
  };
1539
1560
  }
1540
1561
  const already = target.state === "Booted";
1541
- if (!already) await client.simctl.boot(target.id);
1562
+ if (!already) {
1563
+ await client.simctl.boot(target.id);
1564
+ client.forgetSimulators();
1565
+ }
1542
1566
  let windowWarning;
1543
1567
  let windowNote;
1544
1568
  if (open_window) await client.simctl.openApp(client.openPath).then((app) => {
@@ -1546,20 +1570,22 @@ const registerLifecycleTools = (server, client) => {
1546
1570
  }).catch((err) => {
1547
1571
  windowWarning = `Booted, but no window could be opened: ${err instanceof Error ? err.message : String(err)}`;
1548
1572
  });
1549
- const deadline = Date.now() + wait_ms;
1550
- let observed = target.state;
1551
- while (Date.now() < deadline) {
1552
- observed = (await client.listSimulators({ fresh: true })).find((sim) => sim.id === target.id)?.state ?? observed;
1553
- if (observed === "Booted") break;
1554
- await sleep(BOOT_POLL_MS);
1573
+ let ready = already;
1574
+ let waitWarning = `Not waited for, since wait_ms is 0: it may still be starting up.`;
1575
+ if (!already && wait_ms > 0) try {
1576
+ await client.simctl.bootstatus(target.id, wait_ms);
1577
+ ready = true;
1578
+ } catch (err) {
1579
+ waitWarning = err instanceof CommandTimeoutError ? `Not finished booting after ${wait_ms}ms. Raise wait_ms, or check Xcode.` : `Booted, but could not confirm it finished: ${err instanceof Error ? err.message : String(err)}`;
1555
1580
  }
1581
+ const observed = Object.values(await client.simctl.listDevices()).flat().find((sim) => sim.udid === target.id)?.state ?? target.state;
1556
1582
  return {
1557
1583
  udid: target.id,
1558
1584
  name: target.name,
1559
1585
  state: observed,
1560
1586
  ...already ? { alreadyThere: true } : {},
1561
1587
  ...windowNote ? { note: windowNote } : {},
1562
- ...observed === "Booted" ? windowWarning ? { warning: windowWarning } : {} : { warning: `Still ${observed} after ${wait_ms}ms. Raise wait_ms, or check Xcode.` }
1588
+ ...ready ? windowWarning ? { warning: windowWarning } : {} : { warning: waitWarning }
1563
1589
  };
1564
1590
  }));
1565
1591
  server.registerTool("ios_simulator_erase", {
@@ -1580,6 +1606,7 @@ const registerLifecycleTools = (server, client) => {
1580
1606
  if (wasBooted) await client.simctl.shutdown(target.id);
1581
1607
  await client.simctl.erase(target.id);
1582
1608
  if (wasBooted) await client.simctl.boot(target.id);
1609
+ client.forgetSimulators();
1583
1610
  return {
1584
1611
  udid: target.id,
1585
1612
  name: target.name,
@@ -1588,8 +1615,7 @@ const registerLifecycleTools = (server, client) => {
1588
1615
  };
1589
1616
  }));
1590
1617
  };
1591
- //#endregion
1592
- //#region src/client/crash.ts
1618
+ // src/client/crash.ts
1593
1619
  /**
1594
1620
  * Simulator crash reports, out of this Mac's own DiagnosticReports.
1595
1621
  *
@@ -1660,6 +1686,13 @@ const listIps = async (dir) => {
1660
1686
  return [...top.filter((name) => name.endsWith(".ips")), ...retired.filter((name) => name.endsWith(".ips")).map((name) => `Retired/${name}`)];
1661
1687
  };
1662
1688
  /**
1689
+ * What each report's head said, by path; `null` for one that is not from a
1690
+ * simulator. A written `.ips` never changes and its name carries a timestamp,
1691
+ * so after the first list only new reports cost a read. A Mac with Xcode
1692
+ * holds hundreds of them, and every list used to open each one.
1693
+ */
1694
+ const scanned = /* @__PURE__ */ new Map();
1695
+ /**
1663
1696
  * Every simulator report in `dir`, newest first. Reads the first 8 KB of each
1664
1697
  * `.ips`, never the whole file, except to find the UDID in the rare report
1665
1698
  * whose `coalitionName` did not fit in that.
@@ -1670,12 +1703,18 @@ const listSimulatorCrashReports = async (dir) => {
1670
1703
  for (let i = 0; i < names.length; i += CONCURRENCY) {
1671
1704
  const batch = await Promise.all(names.slice(i, i + CONCURRENCY).map(async (name) => {
1672
1705
  const path = join(dir, name);
1706
+ const known = scanned.get(path);
1707
+ if (known !== void 0) return known ?? void 0;
1673
1708
  const head = await readHead(path).catch(() => "");
1674
1709
  const header = parseHeader(head);
1675
- if (!header || !SIMULATOR_PLATFORMS.has(Number(header.platform))) return void 0;
1710
+ if (!header) return void 0;
1711
+ if (!SIMULATOR_PLATFORMS.has(Number(header.platform))) {
1712
+ scanned.set(path, null);
1713
+ return;
1714
+ }
1676
1715
  let udid = COALITION.exec(head)?.[1];
1677
1716
  if (!udid) udid = COALITION.exec(await readFile(path, "utf8").catch(() => ""))?.[1];
1678
- return {
1717
+ const row = {
1679
1718
  name,
1680
1719
  process: header.app_name || header.name || crashProcessName(name),
1681
1720
  bundleId: header.bundleID,
@@ -1683,6 +1722,8 @@ const listSimulatorCrashReports = async (dir) => {
1683
1722
  timestamp: header.timestamp,
1684
1723
  udid: udid?.toUpperCase()
1685
1724
  };
1725
+ scanned.set(path, row);
1726
+ return row;
1686
1727
  }));
1687
1728
  for (const row of batch) if (row) rows.push(row);
1688
1729
  }
@@ -1707,8 +1748,7 @@ const isSimulatorReport = (text) => {
1707
1748
  };
1708
1749
  /** The simulator a full report came from, when its body says. */
1709
1750
  const reportUdid = (text) => COALITION.exec(text)?.[1]?.toUpperCase();
1710
- //#endregion
1711
- //#region src/tools/logs.ts
1751
+ // src/tools/logs.ts
1712
1752
  /** UDID → name, for labelling reports. Best effort: a Mac with no Xcode still has reports. */
1713
1753
  const simulatorNames = async (client) => {
1714
1754
  try {
@@ -1738,8 +1778,8 @@ const registerCrashLogTools = (server, client, ctx) => {
1738
1778
  const target = device ? await client.resolveTarget(device) : void 0;
1739
1779
  const wanted = kinds ?? DEFAULT_CRASH_KINDS;
1740
1780
  const needle = process?.toLowerCase();
1741
- const matching = (await listSimulatorCrashReports(dir)).filter((report) => (!target || report.udid === target.id.toUpperCase()) && (wanted.includes("all") || wanted.includes(report.kind)) && (!needle || report.process.toLowerCase().includes(needle) || (report.bundleId?.toLowerCase().includes(needle) ?? false)));
1742
- const names = target ? void 0 : await simulatorNames(client);
1781
+ const [all, names] = await Promise.all([listSimulatorCrashReports(dir), target ? void 0 : simulatorNames(client)]);
1782
+ const matching = all.filter((report) => (!target || report.udid === target.id.toUpperCase()) && (wanted.includes("all") || wanted.includes(report.kind)) && (!needle || report.process.toLowerCase().includes(needle) || (report.bundleId?.toLowerCase().includes(needle) ?? false)));
1743
1783
  return {
1744
1784
  ...target ? {
1745
1785
  simulator: target.name,
@@ -1829,8 +1869,7 @@ const registerLogReadTools = (server, client) => {
1829
1869
  };
1830
1870
  }));
1831
1871
  };
1832
- //#endregion
1833
- //#region src/tools/runner.ts
1872
+ // src/tools/runner.ts
1834
1873
  const defaultSpawnRunner = async (command, args, { env, logPath }) => {
1835
1874
  await mkdir(dirname(logPath), { recursive: true });
1836
1875
  const log = await open(logPath, "a");
@@ -1852,9 +1891,22 @@ const defaultSpawnRunner = async (command, args, { env, logPath }) => {
1852
1891
  };
1853
1892
  const scriptPath = () => {
1854
1893
  const here = dirname(fileURLToPath(import.meta.url));
1855
- for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) return candidate;
1894
+ for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) if (existsSync(candidate)) return candidate;
1856
1895
  throw new IosError$1("Could not find `scripts/wda.sh` beside this module.");
1857
1896
  };
1897
+ /**
1898
+ * Whether `wda.sh run` has anything to run: the same directory and glob it
1899
+ * looks in. Without a build it exits with "no build found" into a log nobody
1900
+ * reads, while a detached spawn has already been reported as started.
1901
+ */
1902
+ const runnerBuilt = async () => {
1903
+ const wdaDir = process.env["WDA_DIR"] || join(homedir(), ".cache", "mcp-ios-simulator", "WebDriverAgent");
1904
+ const products = join(wdaDir, ".build", "Build", "Products");
1905
+ return {
1906
+ built: (await readdir(products).catch(() => [])).some((name) => name.startsWith("WebDriverAgentRunner_iphonesimulator") && name.endsWith(".xctestrun")),
1907
+ products
1908
+ };
1909
+ };
1858
1910
  const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpawnRunner) => {
1859
1911
  server.registerTool("ios_simulator_restart_wda", {
1860
1912
  title: "iOS Simulator: Restart WebDriverAgent",
@@ -1871,12 +1923,16 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
1871
1923
  }, async ({ device, port }) => wrap(async () => {
1872
1924
  const target = client.requireBooted(await client.resolveTarget(device));
1873
1925
  const usePort = port ?? client.wdaPort;
1926
+ const { built, products } = await runnerBuilt();
1927
+ if (!built) throw new IosError$1(`No WebDriverAgent build in ${products}, so there is nothing to start.`, { remedy: "Build it once with `scripts/wda.sh setup` (or `npx -p @mgcrea/mcp-ios-simulator ios-simulator-wda setup` from an npm install), then call ios_simulator_restart_wda again." });
1874
1928
  const logPath = join(outputDir, "wda.log");
1875
1929
  const pid = await spawnRunner("/bin/bash", [scriptPath(), "run"], {
1876
1930
  env: {
1877
1931
  IOS_SIMULATOR_ID: target.id,
1878
1932
  IOS_SIMULATOR_WDA_PORT: String(usePort),
1879
- USE_PORT: String(usePort)
1933
+ USE_PORT: String(usePort),
1934
+ NODE: process.execPath,
1935
+ PATH: `${dirname(process.execPath)}:${process.env["PATH"] ?? ""}`
1880
1936
  },
1881
1937
  logPath
1882
1938
  });
@@ -1891,8 +1947,7 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
1891
1947
  };
1892
1948
  }));
1893
1949
  };
1894
- //#endregion
1895
- //#region src/tools/index.ts
1950
+ // src/tools/index.ts
1896
1951
  /**
1897
1952
  * All capability decisions in one place, so "why can't I call that" is answered
1898
1953
  * by one file.
@@ -1943,8 +1998,7 @@ const registerTools = (server, client, ctx, spawnRunner) => {
1943
1998
  registerEnvironmentTools(server, client);
1944
1999
  registerRunnerTools(server, client, ctx.config.outputDir, spawnRunner);
1945
2000
  };
1946
- //#endregion
1947
- //#region src/server.ts
2001
+ // src/server.ts
1948
2002
  const SERVER_NAME = BUILD_INFO.name;
1949
2003
  const SERVER_VERSION = BUILD_INFO.version;
1950
2004
  /**
@@ -1985,8 +2039,7 @@ const createServer = (opts) => {
1985
2039
  client
1986
2040
  };
1987
2041
  };
1988
- //#endregion
1989
- //#region src/config.ts
2042
+ // src/config.ts
1990
2043
  /**
1991
2044
  * There are no credentials here, and unlike the device server there is barely
1992
2045
  * any setup either. Two lanes reach a simulator and both are local:
@@ -2112,7 +2165,6 @@ const loadConfig = (env = process.env, configPath = defaultConfigPath(env)) => {
2112
2165
  openPath: pick(trimmed(env.IOS_SIMULATOR_OPEN_PATH), "openPath")
2113
2166
  });
2114
2167
  };
2115
- //#endregion
2116
2168
  export { WritesDisabledError as A, SimulatorNotBootedError as C, ToolchainError as D, TOOLCHAIN_REMEDY as E, wdaUnavailableRemedy as M, BUILD_INFO as N, WdaError as O, START_RUNNER_TOOL_REMEDY as S, TIMEOUT_REMEDY as T, CommandTimeoutError as _, createServer as a, RuntimeUnavailableError as b, assertNotBulkTarget as c, summarizeDevices as d, toListRow as f, CommandError as g, toDisplayInfo as h, SERVER_VERSION as i, startRunnerRemedy as j, WdaUnavailableError as k, parseSimctlError as l, readScreenProfile as m, loadConfig as n, SimulatorClient as o, pngDimensions as p, SERVER_NAME as r, Simctl as s, defaultConfigPath as t, summarizeApps as u, EMPTY_SCREENSHOT_REMEDY as v, SimulatorNotFoundError as w, START_RUNNER_SCRIPT_REMEDY as x, IosError$1 as y };
2117
2169
 
2118
- //# sourceMappingURL=config-D0JgmT_U.js.map
2170
+ //# sourceMappingURL=config-HmqayUrt.js.map