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