@alumnium/xcode 0.0.0-stage → 0.24.0-alpha.1

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/src/index.js ADDED
@@ -0,0 +1,960 @@
1
+ var __legacyDecorateClassTS = function (decorators, target, key, desc) {
2
+ var c = arguments.length,
3
+ r =
4
+ c < 3
5
+ ? target
6
+ : desc === null
7
+ ? (desc = Object.getOwnPropertyDescriptor(target, key))
8
+ : desc,
9
+ d;
10
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
11
+ r = Reflect.decorate(decorators, target, key, desc);
12
+ else
13
+ for (var i = decorators.length - 1; i >= 0; i--)
14
+ if ((d = decorators[i]))
15
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
16
+ return (c > 3 && r && Object.defineProperty(target, key, r), r);
17
+ };
18
+
19
+ // xcode/src/Alumni.ts
20
+ import { Alumni as BaseAlumni } from "@alumnium/core";
21
+ // xcode/src/drivers/XcodeDriver.ts
22
+ import {
23
+ AppId,
24
+ BaseDriver,
25
+ ClickTool,
26
+ DragAndDropTool,
27
+ DragSliderTool,
28
+ NavigateBackTool,
29
+ PressKeyTool,
30
+ ScrollTool,
31
+ sleep,
32
+ Telemetry,
33
+ TypeTool,
34
+ } from "@alumnium/core/internal";
35
+ var { tracer } = Telemetry.get(
36
+ "packages/typescript/xcode/src/drivers/XcodeDriver.ts",
37
+ );
38
+ var { span } = tracer.dec();
39
+ var stateful = BaseDriver.stateful;
40
+
41
+ class XcodeDriver extends BaseDriver {
42
+ kind = "xcode";
43
+ platform = "ios";
44
+ supportedTools = new Set([
45
+ ClickTool,
46
+ DragAndDropTool,
47
+ DragSliderTool,
48
+ NavigateBackTool,
49
+ PressKeyTool,
50
+ ScrollTool,
51
+ TypeTool,
52
+ ]);
53
+ delay = 0;
54
+ session;
55
+ constructor(session) {
56
+ super();
57
+ this.session = session;
58
+ }
59
+ async fetchAccessibilityTree() {
60
+ if (this.delay > 0) await sleep(this.delay * 1000);
61
+ return (await this.session.capture()).tree;
62
+ }
63
+ async click(id) {
64
+ const node = await this.#resolve(id);
65
+ const point = this.#pointOf(node);
66
+ await this.#perform(`t ${point.x} ${point.y}`, node.activationBundleId);
67
+ }
68
+ async type(id, text) {
69
+ const node = await this.#resolve(id);
70
+ const point = this.#pointOf(node);
71
+ await this.#perform(`t ${point.x} ${point.y}`, node.activationBundleId);
72
+ const focused = await this.#resolveNode(node);
73
+ const value =
74
+ focused.value === focused.attributes["placeholderValue"]
75
+ ? ""
76
+ : (focused.value ?? "");
77
+ const input = "\b".repeat([...value].length) + text;
78
+ if (input) {
79
+ await this.#perform(`sender keyboard kbd ${this.#encodeText(input)}`);
80
+ }
81
+ }
82
+ async pressKey(key) {
83
+ const keys = {
84
+ Backspace: "\b",
85
+ Enter: `
86
+ `,
87
+ Tab: "\t",
88
+ Escape: "\x1B",
89
+ Space: " ",
90
+ ArrowUp: "",
91
+ ArrowDown: "",
92
+ ArrowLeft: "",
93
+ ArrowRight: "",
94
+ };
95
+ await this.#perform(`sender keyboard kbd ${this.#encodeText(keys[key])}`);
96
+ }
97
+ async dragAndDrop(fromId, toId) {
98
+ const previous = await this.#tree();
99
+ const from = previous.elementById(fromId);
100
+ const to = previous.elementById(toId);
101
+ if (from.activationBundleId !== to.activationBundleId)
102
+ throw new Error("Xcode cannot drag between applications");
103
+ const fresh = (
104
+ await this.session.capture(undefined, from.activationBundleId)
105
+ ).tree;
106
+ const start = fresh.resolve(from);
107
+ const end = fresh.resolve(to);
108
+ if (start.activationBundleId !== end.activationBundleId)
109
+ throw new Error("Xcode cannot drag between applications");
110
+ const a = this.#pointOf(start);
111
+ const b = this.#pointOf(end);
112
+ await this.#perform(
113
+ `drag ${a.x} ${a.y} ${b.x} ${b.y}`,
114
+ start.activationBundleId,
115
+ );
116
+ }
117
+ async dragSlider(id, value) {
118
+ if (value < 0 || value > 100)
119
+ throw new Error("Slider value must be between 0 and 100");
120
+ const node = await this.#resolve(id);
121
+ if (node.type !== "Slider" || !node.frame)
122
+ throw new Error("Xcode target must be a slider with a frame");
123
+ const start = this.#pointOf(node);
124
+ const end = node.frame.x + (node.frame.width * value) / 100;
125
+ await this.#perform(
126
+ `drag ${start.x} ${start.y} ${end} ${start.y}`,
127
+ node.activationBundleId,
128
+ );
129
+ }
130
+ async scrollTo(id) {
131
+ const target = (await this.#tree()).elementById(id);
132
+ for (let attempt = 0; attempt < 10; attempt++) {
133
+ const tree = (await this.session.capture()).tree;
134
+ const node = tree.resolve(target);
135
+ if (node.hitPoint) return;
136
+ let container = node.parent;
137
+ while (
138
+ container &&
139
+ !["ScrollView", "Table", "CollectionView", "Window"].includes(
140
+ container.type,
141
+ )
142
+ )
143
+ container = container.parent;
144
+ if (!container?.frame || !node.frame)
145
+ throw new Error(
146
+ "Xcode cannot determine a scroll container for this element",
147
+ );
148
+ const frame = container.frame;
149
+ const x = frame.x + frame.width / 2;
150
+ const top = frame.y + frame.height * 0.25;
151
+ const bottom = frame.y + frame.height * 0.75;
152
+ const up = node.frame.y > frame.y + frame.height / 2;
153
+ await this.#perform(
154
+ `t ${x} ${up ? bottom : top} f ${x} ${up ? top : bottom} 0.3`,
155
+ node.activationBundleId,
156
+ );
157
+ }
158
+ throw new Error("Xcode element did not become hittable after scrolling");
159
+ }
160
+ async back() {
161
+ const tree = (await this.session.capture()).tree;
162
+ const bar = tree.find((node) => node.type === "NavigationBar");
163
+ const back = bar?.children.find(
164
+ (node) =>
165
+ node.type === "Button" &&
166
+ node.hitPoint &&
167
+ (/^back$/i.test(node.label ?? "") ||
168
+ /^(back|back-button|backButton)$/i.test(node.identifier ?? "")),
169
+ );
170
+ if (!back) {
171
+ const window = tree.find((node) => node.type === "Window");
172
+ if (!bar || !window?.frame)
173
+ throw new Error("Xcode could not find a navigation back button");
174
+ const frame = window.frame;
175
+ const y = frame.y + frame.height / 2;
176
+ await this.#perform(
177
+ `t ${frame.x + 1} ${y} f ${frame.x + frame.width * 0.75} ${y} 0.3`,
178
+ window.activationBundleId,
179
+ );
180
+ return;
181
+ }
182
+ const point = this.#pointOf(back);
183
+ await this.#perform(`t ${point.x} ${point.y}`, back.activationBundleId);
184
+ }
185
+ async wait(seconds) {
186
+ await this.#perform(`w ${Math.max(1, Math.min(30, seconds))}`);
187
+ }
188
+ screenshot() {
189
+ return this.session.screenshot();
190
+ }
191
+ quit() {
192
+ return this.session.close();
193
+ }
194
+ app() {
195
+ return AppId.parse(this.session.appId);
196
+ }
197
+ title() {
198
+ return "";
199
+ }
200
+ url() {
201
+ return "";
202
+ }
203
+ checkNavigationPolicy() {}
204
+ findElement() {
205
+ throw new Error("Element handles are not supported on Xcode");
206
+ }
207
+ visit() {
208
+ throw new Error("Opening URLs is not supported by Xcode MCP");
209
+ }
210
+ executeScript() {
211
+ throw new Error("Executing JavaScript is not supported on Xcode");
212
+ }
213
+ switchToNextTab() {
214
+ throw new Error("Tab switching is not supported on Xcode");
215
+ }
216
+ switchToPreviousTab() {
217
+ throw new Error("Tab switching is not supported on Xcode");
218
+ }
219
+ waitForSelector() {
220
+ throw new Error("CSS selectors are not supported on Xcode");
221
+ }
222
+ printToPdf() {
223
+ throw new Error("Printing to PDF is not supported on Xcode");
224
+ }
225
+ async #tree() {
226
+ return await this.getAccessibilityTree();
227
+ }
228
+ async #resolve(id) {
229
+ return this.#resolveNode((await this.#tree()).elementById(id));
230
+ }
231
+ async #resolveNode(target) {
232
+ return (
233
+ await this.session.capture(undefined, target.activationBundleId)
234
+ ).tree.resolve(target);
235
+ }
236
+ async #perform(command, bundleId) {
237
+ try {
238
+ await this.session.capture(command, bundleId);
239
+ } finally {
240
+ this.resetAccessibilityTree();
241
+ }
242
+ }
243
+ #pointOf(node) {
244
+ if (!node.hitPoint || node.attributes["enabled"] === "false") {
245
+ throw new Error(
246
+ `Xcode element ${node.identifier || node.label || node.type} is not hittable`,
247
+ );
248
+ }
249
+ return node.hitPoint;
250
+ }
251
+ #encodeText(text) {
252
+ return [...text]
253
+ .map((character) => `\\u{${character.codePointAt(0).toString(16)}}`)
254
+ .join("");
255
+ }
256
+ }
257
+ __legacyDecorateClassTS(
258
+ [span("driver.fetch_accessibility_tree", BaseDriver.spanAttrs)],
259
+ XcodeDriver.prototype,
260
+ "fetchAccessibilityTree",
261
+ null,
262
+ );
263
+ __legacyDecorateClassTS(
264
+ [span("driver.click", BaseDriver.spanAttrs), stateful],
265
+ XcodeDriver.prototype,
266
+ "click",
267
+ null,
268
+ );
269
+ __legacyDecorateClassTS(
270
+ [span("driver.type", BaseDriver.spanAttrs), stateful],
271
+ XcodeDriver.prototype,
272
+ "type",
273
+ null,
274
+ );
275
+ __legacyDecorateClassTS(
276
+ [span("driver.press_key", BaseDriver.spanAttrs), stateful],
277
+ XcodeDriver.prototype,
278
+ "pressKey",
279
+ null,
280
+ );
281
+ __legacyDecorateClassTS(
282
+ [span("driver.drag_and_drop", BaseDriver.spanAttrs), stateful],
283
+ XcodeDriver.prototype,
284
+ "dragAndDrop",
285
+ null,
286
+ );
287
+ __legacyDecorateClassTS(
288
+ [span("driver.drag_slider", BaseDriver.spanAttrs), stateful],
289
+ XcodeDriver.prototype,
290
+ "dragSlider",
291
+ null,
292
+ );
293
+ __legacyDecorateClassTS(
294
+ [span("driver.scroll_to", BaseDriver.spanAttrs), stateful],
295
+ XcodeDriver.prototype,
296
+ "scrollTo",
297
+ null,
298
+ );
299
+ __legacyDecorateClassTS(
300
+ [span("driver.back", BaseDriver.spanAttrs), stateful],
301
+ XcodeDriver.prototype,
302
+ "back",
303
+ null,
304
+ );
305
+ __legacyDecorateClassTS(
306
+ [span("driver.wait", BaseDriver.spanAttrs), stateful],
307
+ XcodeDriver.prototype,
308
+ "wait",
309
+ null,
310
+ );
311
+ __legacyDecorateClassTS(
312
+ [span("driver.screenshot", BaseDriver.spanAttrs)],
313
+ XcodeDriver.prototype,
314
+ "screenshot",
315
+ null,
316
+ );
317
+ __legacyDecorateClassTS(
318
+ [span("driver.quit", BaseDriver.spanAttrs)],
319
+ XcodeDriver.prototype,
320
+ "quit",
321
+ null,
322
+ );
323
+
324
+ // xcode/src/drivers/XcodeSession.ts
325
+ import { randomUUID } from "node:crypto";
326
+ import { cp, mkdtemp, readFile, rm } from "node:fs/promises";
327
+ import os from "node:os";
328
+ import path from "node:path";
329
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
330
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
331
+ // xcode/src/accessibility/XcodeAccessibilityTree.ts
332
+ import { Element } from "domhandler";
333
+ import { z as z2 } from "zod";
334
+ import { z } from "zod";
335
+ import {
336
+ ALUMNIUM_VERSION,
337
+ Env,
338
+ exec,
339
+ Logger,
340
+ SimulatorWindow,
341
+ sleep as sleep2,
342
+ } from "@alumnium/core/internal";
343
+ import { BaseAccessibilityTree, XmlRenderer } from "@alumnium/core/internal";
344
+
345
+ class XcodeAccessibilityTree extends BaseAccessibilityTree {
346
+ #roots;
347
+ #nodes;
348
+ get kind() {
349
+ return "xcode";
350
+ }
351
+ constructor(source, roots) {
352
+ super(source);
353
+ this.#roots = roots ?? parseHierarchy(source);
354
+ this.#nodes = this.#roots.flatMap(flatten);
355
+ }
356
+ toStr() {
357
+ return (this.xml ??= XmlRenderer.render(this.#roots.map(toXml), {
358
+ compactAttrs: false,
359
+ }));
360
+ }
361
+ elementById(id) {
362
+ const node = this.#nodes.find((candidate) => candidate.id === id);
363
+ if (!node) throw new Error(`No element with raw_id=${id} found`);
364
+ return node;
365
+ }
366
+ scopeToArea(id) {
367
+ const node = this.#nodes.find((candidate) => candidate.id === id);
368
+ return node ? new XcodeAccessibilityTree(this.toStr(), [node]) : this;
369
+ }
370
+ resolve(target) {
371
+ let matches = this.#nodes.filter((candidate) =>
372
+ sameIdentity(candidate, target),
373
+ );
374
+ if (!target.identifier || matches.length > 1) {
375
+ const context = ancestorIdentity(target);
376
+ matches = matches.filter(
377
+ (candidate) => ancestorIdentity(candidate) === context,
378
+ );
379
+ }
380
+ const first = matches[0];
381
+ if (
382
+ first?.hitPoint &&
383
+ matches.every(
384
+ (candidate) => actionIdentity(candidate) === actionIdentity(first),
385
+ )
386
+ ) {
387
+ return first;
388
+ }
389
+ if (matches.length !== 1) {
390
+ throw new Error(
391
+ `Xcode element ${target.identifier || target.label || target.type} is ${matches.length ? "ambiguous" : "no longer present"}; fetch a fresh accessibility tree`,
392
+ );
393
+ }
394
+ return matches[0];
395
+ }
396
+ find(predicate) {
397
+ return this.#nodes.find(predicate);
398
+ }
399
+ }
400
+ var NUMBER = "(-?\\d+(?:\\.\\d+)?)";
401
+ var FRAME = new RegExp(
402
+ `\\{\\{${NUMBER},\\s*${NUMBER}\\},\\s*\\{${NUMBER},\\s*${NUMBER}\\}\\}`,
403
+ );
404
+ var HIT_POINT = new RegExp(`hitPoint:\\s*\\{${NUMBER},\\s*${NUMBER}\\}`);
405
+ var QUOTED_ATTRIBUTE =
406
+ /(?:^|,\s*)(identifier|label|value|title|placeholderValue):\s*'((?:\\.|[^']|'(?!,\s|$))*)'(?=,\s|$)/g;
407
+ var BOOLEAN_ATTRIBUTE =
408
+ /(?:^|,\s*)(enabled|selected|focused|isEnabled|isSelected|isFocused|isRemoteLeafPlaceholder):\s*(true|false|YES|NO|0|1)(?=,\s|$)/g;
409
+ var POINT = z.tuple([z.coerce.number().finite(), z.coerce.number().finite()]);
410
+ var RECT = z.tuple([
411
+ z.coerce.number().finite(),
412
+ z.coerce.number().finite(),
413
+ z.coerce.number().finite(),
414
+ z.coerce.number().finite(),
415
+ ]);
416
+ function parseHierarchy(source) {
417
+ const roots = [];
418
+ const stack = [];
419
+ let bundleId = "";
420
+ let id = 0;
421
+ for (const line of source.split(/\r?\n/)) {
422
+ const application = /^Application bundle identifier:\s*(.+)$/.exec(line);
423
+ if (application) {
424
+ bundleId = application[1].trim();
425
+ stack.length = 0;
426
+ continue;
427
+ }
428
+ const match = /^(\s*)([A-Z][A-Za-z0-9]*)(?=,|\s|$)(.*)$/.exec(line);
429
+ if (
430
+ !match ||
431
+ /^(Device|Application UI|Application pid|Application is)/.test(line)
432
+ )
433
+ continue;
434
+ const [, whitespace, type, description] = match;
435
+ if (type !== "Application" && !stack.length) continue;
436
+ const attributes = {};
437
+ for (const attr of description.matchAll(QUOTED_ATTRIBUTE)) {
438
+ attributes[attr[1]] = attr[2].replace(/\\(['\\])/g, "$1");
439
+ }
440
+ const value =
441
+ /(?:^|,\s*)value:\s*(.*?)(?=,\s*(?:(?:hitPoint|activationBundleId|enabled|selected|focused|is\w+):|(?:Disabled|Selected|Focused|Keyboard Focused)(?:,|$))|$)/.exec(
442
+ description,
443
+ );
444
+ if (value && attributes["value"] === undefined)
445
+ attributes["value"] = value[1].trim();
446
+ for (const attr of description.matchAll(BOOLEAN_ATTRIBUTE)) {
447
+ const name = attr[1].replace(/^is(?=[A-Z])/, "");
448
+ const normalized = name[0].toLowerCase() + name.slice(1);
449
+ attributes[normalized] = ["true", "YES", "1"].includes(attr[2])
450
+ ? "true"
451
+ : "false";
452
+ }
453
+ for (const [flag, attr] of [
454
+ ["Disabled", "enabled"],
455
+ ["Selected", "selected"],
456
+ ["Focused", "focused"],
457
+ ["Keyboard Focused", "focused"],
458
+ ]) {
459
+ if (new RegExp(`(?:^|,\\s*)${flag}(?=,|$)`).test(description))
460
+ attributes[attr] = flag === "Disabled" ? "false" : "true";
461
+ }
462
+ const frameMatch = FRAME.exec(description);
463
+ const hitMatch = HIT_POINT.exec(description);
464
+ const frame = frameMatch
465
+ ? RECT.parse(frameMatch.slice(1).map(Number))
466
+ : undefined;
467
+ const hitPoint = hitMatch
468
+ ? POINT.parse(hitMatch.slice(1).map(Number))
469
+ : undefined;
470
+ const indent = whitespace.length;
471
+ while (stack.length && stack.at(-1).indent >= indent) stack.pop();
472
+ const parent = stack.at(-1)?.node;
473
+ const node = {
474
+ id: ++id,
475
+ type,
476
+ identifier: attributes["identifier"],
477
+ name: attributes["identifier"] || attributes["label"],
478
+ label: attributes["label"],
479
+ value: attributes["value"],
480
+ frame: frame && {
481
+ x: frame[0],
482
+ y: frame[1],
483
+ width: frame[2],
484
+ height: frame[3],
485
+ },
486
+ hitPoint: hitPoint && { x: hitPoint[0], y: hitPoint[1] },
487
+ activationBundleId:
488
+ /activationBundleId:\s*([^,\s]+)/.exec(description)?.[1] ?? bundleId,
489
+ attributes,
490
+ children: [],
491
+ parent,
492
+ };
493
+ (parent?.children ?? roots).push(node);
494
+ stack.push({ indent, node });
495
+ }
496
+ if (!roots.length)
497
+ throw new Error(
498
+ "Xcode returned no application accessibility hierarchy; capture the device again",
499
+ );
500
+ return roots;
501
+ }
502
+ function flatten(node) {
503
+ return [node, ...node.children.flatMap(flatten)];
504
+ }
505
+ function toXml(node) {
506
+ const attributes = {
507
+ raw_id: String(node.id),
508
+ name: node.name ?? "",
509
+ ...node.attributes,
510
+ };
511
+ const element = new Element(node.type, attributes);
512
+ element.children = node.children.map(toXml);
513
+ for (const child of element.children) child.parent = element;
514
+ return element;
515
+ }
516
+ function sameIdentity(candidate, target) {
517
+ return (
518
+ candidate.type === target.type &&
519
+ candidate.activationBundleId === target.activationBundleId &&
520
+ (target.identifier
521
+ ? candidate.identifier === target.identifier
522
+ : candidate.label === target.label)
523
+ );
524
+ }
525
+ function ancestorIdentity(node) {
526
+ const identities = [];
527
+ for (let parent = node.parent; parent; parent = parent.parent) {
528
+ identities.push(
529
+ JSON.stringify([
530
+ parent.type,
531
+ parent.identifier || parent.label || "",
532
+ parent.type === "Cell"
533
+ ? flatten(parent)
534
+ .filter((child) => child.type === "StaticText")
535
+ .map((child) => child.label ?? "")
536
+ .sort()
537
+ : [],
538
+ ]),
539
+ );
540
+ }
541
+ return identities.join("/");
542
+ }
543
+ function actionIdentity(node) {
544
+ return JSON.stringify([node.attributes, node.frame, node.hitPoint]);
545
+ }
546
+
547
+ // xcode/src/drivers/XcodeSession.ts
548
+ var OpenWorkspace = z2.object({
549
+ workspaceIdentifier: z2.string(),
550
+ activeScheme: z2.string().nullable().optional(),
551
+ });
552
+ var Schemes = z2.object({
553
+ activeSchemeName: z2.string().nullable().optional(),
554
+ });
555
+ var BuildSettings = z2.array(
556
+ z2.object({
557
+ buildSettings: z2.object({
558
+ PRODUCT_TYPE: z2.string().optional(),
559
+ PRODUCT_BUNDLE_IDENTIFIER: z2.string().optional(),
560
+ }),
561
+ }),
562
+ );
563
+ var Session = z2.object({
564
+ interactionSessionKey: z2.string(),
565
+ deviceUUID: z2.string(),
566
+ deviceIsSimulator: z2.boolean(),
567
+ });
568
+ var PermissionStatus = z2.object({
569
+ permission: z2.object({
570
+ projectAccessAttempts: z2
571
+ .array(
572
+ z2.object({
573
+ agent: z2.object({ name: z2.string() }),
574
+ endpoints: z2.array(z2.string()),
575
+ }),
576
+ )
577
+ .default([]),
578
+ }),
579
+ });
580
+ var Capture = z2.object({
581
+ hierarchyPath: z2.string().optional(),
582
+ screenshotPath: z2.string(),
583
+ });
584
+ var logger = Logger.get(
585
+ "packages/typescript/xcode/src/drivers/XcodeSession.ts",
586
+ );
587
+
588
+ class XcodeSession {
589
+ static TOOL_TIMEOUT_MS = 5 * 60000;
590
+ static APPROVE_INTERVAL_MS = 1000;
591
+ #client = new Client({
592
+ name: "alumnium",
593
+ version: ALUMNIUM_VERSION,
594
+ });
595
+ #props;
596
+ #key;
597
+ #appId;
598
+ #simulatorWindow = new SimulatorWindow();
599
+ #approving = false;
600
+ constructor(props) {
601
+ this.#props = props;
602
+ this.#appId = props.appId ?? "";
603
+ }
604
+ get appId() {
605
+ return this.#appId;
606
+ }
607
+ static async start(props) {
608
+ const session = new XcodeSession(props);
609
+ try {
610
+ await session.#start();
611
+ return session;
612
+ } catch (error) {
613
+ await session.close().catch(() => {});
614
+ throw error;
615
+ }
616
+ }
617
+ async #start() {
618
+ const { appPath, appReset, device, launchArgs, launchEnv } = this.#props;
619
+ const workspace =
620
+ appPath && /\.(xcodeproj|xcworkspace)[\\/]?$/i.test(appPath)
621
+ ? appPath
622
+ : undefined;
623
+ if (
624
+ !appPath &&
625
+ !this.#appId &&
626
+ (appReset || launchArgs?.length || Object.keys(launchEnv ?? {}).length)
627
+ )
628
+ throw new Error(
629
+ "An app is required to reset or launch with arguments or environment variables",
630
+ );
631
+ this.#startAutoApprove();
632
+ await this.#connect();
633
+ const sessionIdentifier = `Alumnium ${randomUUID()}`;
634
+ let workspaceIdentifier;
635
+ let activeScheme;
636
+ if (workspace) {
637
+ const opened = OpenWorkspace.parse(
638
+ await this.#call("XcodeOpenWorkspace", { path: workspace }),
639
+ );
640
+ workspaceIdentifier = opened.workspaceIdentifier;
641
+ activeScheme = opened.activeScheme;
642
+ }
643
+ const result = Session.parse(
644
+ await this.#call(
645
+ workspace
646
+ ? "DeviceInteractionStartWorkspaceSession"
647
+ : "DeviceInteractionStartSession",
648
+ {
649
+ sessionIdentifier,
650
+ ...(workspace ? { workspaceIdentifier } : {}),
651
+ ...(workspace
652
+ ? device && { deviceIdentifier: device }
653
+ : { deviceIdentifier: device ?? "iPhone Simulator" }),
654
+ },
655
+ ),
656
+ );
657
+ this.#key = result.interactionSessionKey;
658
+ if (!result.deviceIsSimulator)
659
+ throw new Error(
660
+ "XcodeDriver supports iOS simulators only; select a simulator using the device option",
661
+ );
662
+ await this.#simulatorWindow.open(result.deviceUUID, this.#props.headless);
663
+ if (workspace) {
664
+ await this.#call("DeviceInteractionInstallAndRun", {
665
+ interactionSessionKey: this.#key,
666
+ workspaceIdentifier,
667
+ ...(launchArgs && { commandLineArguments: launchArgs }),
668
+ ...(launchEnv && { environmentVariables: launchEnv }),
669
+ });
670
+ await this.#resolveWorkspaceApp(
671
+ workspace,
672
+ workspaceIdentifier,
673
+ result.deviceUUID,
674
+ activeScheme,
675
+ );
676
+ } else {
677
+ await this.#installApp(result.deviceUUID);
678
+ }
679
+ if (appReset) await this.#resetApp(result.deviceUUID);
680
+ if (!workspace || appReset) await this.#launchApp(result.deviceUUID);
681
+ await this.capture(undefined, this.#appId || undefined);
682
+ }
683
+ #startAutoApprove() {
684
+ if (!Env.ALUMNIUM_XCODE_AUTO_APPROVE || this.#approving) return;
685
+ this.#approving = true;
686
+ this.#autoApprove();
687
+ }
688
+ async #autoApprove() {
689
+ let lastError;
690
+ while (this.#approving) {
691
+ try {
692
+ if (await this.#approvePending()) break;
693
+ } catch (error) {
694
+ const message = error instanceof Error ? error.message : String(error);
695
+ if (message !== lastError)
696
+ logger.warn(`Xcode auto approve failed: ${message}`);
697
+ lastError = message;
698
+ }
699
+ await sleep2(XcodeSession.APPROVE_INTERVAL_MS);
700
+ }
701
+ this.#approving = false;
702
+ }
703
+ async #approvePending() {
704
+ const { stdout } = await exec("xcrun", [
705
+ "mcp-server",
706
+ "status",
707
+ "--format",
708
+ "json",
709
+ ]);
710
+ const { permission } = PermissionStatus.parse(JSON.parse(stdout));
711
+ const ids = permission.projectAccessAttempts
712
+ .filter(({ agent }) => agent.name === "alumnium")
713
+ .flatMap(({ endpoints }) => endpoints);
714
+ if (!ids.length) return false;
715
+ logger.info(`Approving Xcode access requests: ${ids.join(", ")}`);
716
+ await exec("sudo", [
717
+ "xcrun",
718
+ "mcp-server",
719
+ "approve",
720
+ ...ids,
721
+ "--for-24-hours",
722
+ ]);
723
+ return true;
724
+ }
725
+ async #connect() {
726
+ try {
727
+ await this.#client.connect(
728
+ new StdioClientTransport({
729
+ command: "xcrun",
730
+ args: ["mcpbridge"],
731
+ env: Object.fromEntries(
732
+ Object.entries(process.env).filter(
733
+ (entry) => entry[1] !== undefined,
734
+ ),
735
+ ),
736
+ stderr: "pipe",
737
+ }),
738
+ );
739
+ } catch (error) {
740
+ throw (await this.#bridgeFailure(error)) ?? error;
741
+ }
742
+ }
743
+ async #bridgeFailure(cause) {
744
+ try {
745
+ await exec("xcrun", ["mcpbridge"], { timeout: 1e4 });
746
+ } catch (error) {
747
+ const output = (error instanceof Error ? error.message : "")
748
+ .replace(/^.*? exited with status \S+:\s*/s, "")
749
+ .trim();
750
+ let message = output;
751
+ try {
752
+ const parsed = z2
753
+ .object({ error: z2.object({ message: z2.string() }) })
754
+ .parse(JSON.parse(output));
755
+ message = parsed.error.message;
756
+ } catch {}
757
+ if (message)
758
+ return new Error(`Xcode mcpbridge failed: ${message}`, { cause });
759
+ }
760
+ }
761
+ async #resolveWorkspaceApp(
762
+ workspace,
763
+ workspaceIdentifier,
764
+ device,
765
+ activeScheme,
766
+ ) {
767
+ const scheme =
768
+ activeScheme ||
769
+ Schemes.parse(
770
+ await this.#call("XcodeListSchemes", { workspaceIdentifier }),
771
+ ).activeSchemeName;
772
+ if (!scheme) throw new Error("Xcode workspace has no active scheme");
773
+ const { stdout } = await exec(
774
+ "xcrun",
775
+ [
776
+ "xcodebuild",
777
+ /\.xcodeproj[\\/]?$/i.test(workspace) ? "-project" : "-workspace",
778
+ workspace,
779
+ "-scheme",
780
+ scheme,
781
+ "-destination",
782
+ `id=${device}`,
783
+ "-showBuildSettings",
784
+ "-json",
785
+ ],
786
+ { timeout: XcodeSession.TOOL_TIMEOUT_MS },
787
+ );
788
+ const apps = BuildSettings.parse(JSON.parse(stdout)).filter(
789
+ ({ buildSettings }) =>
790
+ buildSettings.PRODUCT_TYPE === "com.apple.product-type.application" &&
791
+ (!this.#appId ||
792
+ buildSettings.PRODUCT_BUNDLE_IDENTIFIER === this.#appId),
793
+ );
794
+ if (apps.length !== 1)
795
+ throw new Error(
796
+ "Xcode could not uniquely identify the active scheme's app",
797
+ );
798
+ this.#appId = z2
799
+ .string()
800
+ .trim()
801
+ .min(1)
802
+ .parse(apps[0].buildSettings.PRODUCT_BUNDLE_IDENTIFIER);
803
+ }
804
+ async #installApp(device) {
805
+ const { appPath } = this.#props;
806
+ if (appPath) {
807
+ const { stdout } = await exec("/usr/libexec/PlistBuddy", [
808
+ "-c",
809
+ "Print :CFBundleIdentifier",
810
+ path.join(appPath, "Info.plist"),
811
+ ]);
812
+ this.#appId = z2.string().trim().min(1).parse(stdout);
813
+ await exec("xcrun", ["simctl", "install", device, appPath], {
814
+ timeout: XcodeSession.TOOL_TIMEOUT_MS,
815
+ });
816
+ }
817
+ }
818
+ async #resetApp(device) {
819
+ if (!this.#appId)
820
+ throw new Error(
821
+ "Xcode could not determine the app bundle id for appReset",
822
+ );
823
+ if (this.#appId.toLowerCase() === "com.apple.springboard")
824
+ throw new Error("Xcode cannot reset SpringBoard");
825
+ const { stdout } = await exec("xcrun", [
826
+ "simctl",
827
+ "get_app_container",
828
+ device,
829
+ this.#appId,
830
+ "app",
831
+ ]);
832
+ const appPath = z2.string().trim().min(1).parse(stdout);
833
+ const directory = await mkdtemp(
834
+ path.join(os.tmpdir(), "alumnium-xcode-reset-"),
835
+ );
836
+ const savedApp = path.join(directory, path.basename(appPath));
837
+ try {
838
+ await cp(appPath, savedApp, { recursive: true });
839
+ await exec("xcrun", ["simctl", "uninstall", device, this.#appId]);
840
+ await exec("xcrun", ["simctl", "install", device, savedApp], {
841
+ timeout: XcodeSession.TOOL_TIMEOUT_MS,
842
+ });
843
+ } finally {
844
+ await rm(directory, { recursive: true, force: true });
845
+ }
846
+ }
847
+ async #launchApp(device) {
848
+ const { launchArgs = [], launchEnv = {} } = this.#props;
849
+ if (!this.#appId) return;
850
+ await exec(
851
+ "xcrun",
852
+ [
853
+ "simctl",
854
+ "launch",
855
+ "--terminate-running-process",
856
+ device,
857
+ this.#appId,
858
+ ...launchArgs,
859
+ ],
860
+ {
861
+ env: {
862
+ ...process.env,
863
+ ...Object.fromEntries(
864
+ Object.entries(launchEnv).map(([key, value]) => [
865
+ `SIMCTL_CHILD_${key}`,
866
+ value,
867
+ ]),
868
+ ),
869
+ },
870
+ },
871
+ );
872
+ }
873
+ async capture(command, activationBundleId) {
874
+ if (!this.#key) throw new Error("Xcode session is not started");
875
+ const result = Capture.parse(
876
+ await this.#call("DeviceInteractionSynthesize", {
877
+ interactSessionKey: this.#key,
878
+ ...(command !== undefined && { interactionCommand: command }),
879
+ ...(activationBundleId && { activationBundleId }),
880
+ }),
881
+ );
882
+ if (!result.hierarchyPath) return this.#recapture();
883
+ return this.#readCapture(result);
884
+ }
885
+ async #recapture() {
886
+ const result = Capture.parse(
887
+ await this.#call("DeviceInteractionSynthesize", {
888
+ interactSessionKey: this.#key,
889
+ }),
890
+ );
891
+ if (!result.hierarchyPath)
892
+ throw new Error("Xcode could not capture the accessibility hierarchy");
893
+ return this.#readCapture(result);
894
+ }
895
+ async #readCapture(result) {
896
+ if (!result.hierarchyPath)
897
+ throw new Error("Xcode could not capture the accessibility hierarchy");
898
+ const source = await readFile(result.hierarchyPath, "utf-8");
899
+ const tree = new XcodeAccessibilityTree(source);
900
+ if (!this.#props.appId && !this.#props.appPath)
901
+ this.#appId ||=
902
+ tree.find((node) => node.type === "Application")?.activationBundleId ??
903
+ "";
904
+ return { tree, screenshotPath: result.screenshotPath };
905
+ }
906
+ async screenshot() {
907
+ const { screenshotPath } = await this.capture();
908
+ return (await readFile(screenshotPath)).toString("base64");
909
+ }
910
+ async close() {
911
+ this.#approving = false;
912
+ try {
913
+ if (this.#key) {
914
+ const key = this.#key;
915
+ this.#key = undefined;
916
+ await this.#call("DeviceInteractionEndSession", {
917
+ interactionSessionKey: key,
918
+ });
919
+ }
920
+ } finally {
921
+ try {
922
+ await this.#client.close();
923
+ } finally {
924
+ await this.#simulatorWindow.close();
925
+ }
926
+ }
927
+ }
928
+ async #call(name, args) {
929
+ const result = await this.#client.callTool(
930
+ { name, arguments: args },
931
+ undefined,
932
+ {
933
+ timeout: XcodeSession.TOOL_TIMEOUT_MS,
934
+ },
935
+ );
936
+ const content = z2
937
+ .array(z2.object({ type: z2.string(), text: z2.string().optional() }))
938
+ .parse(result.content);
939
+ const text = content
940
+ .filter((item) => item.type === "text")
941
+ .map((item) => item.text ?? "").join(`
942
+ `);
943
+ if (result.isError) throw new Error(`Xcode ${name} failed: ${text}`);
944
+ return result.structuredContent ?? JSON.parse(text);
945
+ }
946
+ }
947
+
948
+ // xcode/src/Alumni.ts
949
+ class Alumni extends BaseAlumni {
950
+ constructor(driver, options = {}) {
951
+ super(new XcodeDriver(driver), options);
952
+ }
953
+ static accepts(driver) {
954
+ return driver instanceof XcodeSession;
955
+ }
956
+ }
957
+ export { Alumni, XcodeAccessibilityTree, XcodeDriver, XcodeSession };
958
+
959
+ //# debugId=EFBBB991D7E2833564756E2164756E21
960
+ //# sourceMappingURL=index.js.map