mobile-debug-mcp 0.14.0 → 0.16.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.
Files changed (103) hide show
  1. package/dist/android/interact.js +2 -2
  2. package/dist/android/observe.js +13 -0
  3. package/dist/cli/ios/run-ios-smoke.js +2 -2
  4. package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
  5. package/dist/interact/android.js +91 -0
  6. package/dist/interact/index.js +76 -0
  7. package/dist/interact/ios.js +120 -0
  8. package/dist/interact/shared/fingerprint.js +1 -0
  9. package/dist/interact/shared/scroll_to_element.js +1 -0
  10. package/dist/ios/interact.js +2 -2
  11. package/dist/ios/observe.js +12 -0
  12. package/dist/manage/android.js +162 -0
  13. package/dist/manage/index.js +364 -0
  14. package/dist/manage/ios.js +353 -0
  15. package/dist/observe/android.js +351 -0
  16. package/dist/observe/fingerprint.js +1 -0
  17. package/dist/observe/index.js +85 -0
  18. package/dist/observe/ios.js +320 -0
  19. package/dist/observe/test/device/logstream-real.js +34 -0
  20. package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
  21. package/dist/observe/test/device/run-scroll-test-android.js +22 -0
  22. package/dist/observe/test/device/test-ui-tree.js +67 -0
  23. package/dist/observe/test/device/wait_for_element_real.js +69 -0
  24. package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
  25. package/dist/observe/test/unit/logparse.test.js +39 -0
  26. package/dist/observe/test/unit/logstream.test.js +41 -0
  27. package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
  28. package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
  29. package/dist/server.js +41 -5
  30. package/dist/shared/fingerprint.js +72 -0
  31. package/dist/shared/scroll_to_element.js +98 -0
  32. package/dist/tools/interact.js +2 -2
  33. package/dist/tools/manage.js +2 -2
  34. package/dist/tools/observe.js +45 -43
  35. package/dist/utils/android/utils.js +373 -0
  36. package/dist/utils/cli/idb/check-idb.js +84 -0
  37. package/dist/utils/cli/idb/idb-helper.js +91 -0
  38. package/dist/utils/cli/idb/install-idb.js +82 -0
  39. package/dist/utils/cli/ios/preflight-ios.js +155 -0
  40. package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
  41. package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
  42. package/dist/utils/diagnostics.js +1 -1
  43. package/dist/utils/exec.js +34 -0
  44. package/dist/utils/ios/utils.js +301 -0
  45. package/dist/utils/resolve-device.js +2 -2
  46. package/dist/utils/ui/index.js +169 -0
  47. package/docs/CHANGELOG.md +8 -0
  48. package/docs/tools/interact.md +29 -0
  49. package/docs/tools/observe.md +24 -0
  50. package/package.json +1 -1
  51. package/src/{android/interact.ts → interact/android.ts} +3 -3
  52. package/src/{tools/interact.ts → interact/index.ts} +47 -3
  53. package/src/{ios/interact.ts → interact/ios.ts} +3 -3
  54. package/src/{android/manage.ts → manage/android.ts} +2 -2
  55. package/src/{tools/manage.ts → manage/index.ts} +7 -4
  56. package/src/{ios/manage.ts → manage/ios.ts} +1 -1
  57. package/src/{android/observe.ts → observe/android.ts} +14 -26
  58. package/src/observe/index.ts +92 -0
  59. package/src/{ios/observe.ts → observe/ios.ts} +17 -35
  60. package/src/server.ts +45 -6
  61. package/src/types.ts +1 -0
  62. package/src/{android → utils/android}/utils.ts +12 -79
  63. package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
  64. package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
  65. package/src/utils/diagnostics.ts +1 -1
  66. package/src/utils/exec.ts +33 -0
  67. package/src/{ios → utils/ios}/utils.ts +2 -2
  68. package/src/utils/resolve-device.ts +2 -2
  69. package/src/{tools/scroll_to_element.ts → utils/ui/index.ts} +73 -2
  70. package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
  71. package/test/interact/unit/wait_for_screen_change.test.ts +32 -0
  72. package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
  73. package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
  74. package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
  75. package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
  76. package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
  77. package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
  78. package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
  79. package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
  80. package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
  81. package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
  82. package/test/observe/device/run-screen-fingerprint.ts +36 -0
  83. package/test/{device/observe → observe/device}/run-scroll-test-android.ts +2 -2
  84. package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
  85. package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
  86. package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
  87. package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
  88. package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
  89. package/test/{unit/observe → observe/unit}/scroll_to_element.test.ts +3 -3
  90. package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +2 -2
  91. package/test/unit/index.ts +13 -11
  92. package/src/tools/observe.ts +0 -82
  93. package/test/device/README.md +0 -49
  94. package/test/device/index.ts +0 -27
  95. package/test/device/utils/test-dist.ts +0 -41
  96. package/test/unit/utils/detect-java.test.ts +0 -22
  97. /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
  98. /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
  99. /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
  100. /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
  101. /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
  102. /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
  103. /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "./utils.js";
2
- import { AndroidObserve } from "./observe.js";
3
- import { scrollToElementShared } from "../tools/scroll_to_element.js";
2
+ import { AndroidObserve } from "../observe/index.js";
3
+ import { scrollToElementShared } from "../intera../interact/shared/scroll_to_element.js";
4
4
  export class AndroidInteract {
5
5
  observe = new AndroidObserve();
6
6
  async waitForElement(text, timeout, deviceId) {
@@ -4,6 +4,7 @@ import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, get
4
4
  import { createWriteStream } from "fs";
5
5
  import { promises as fsPromises } from "fs";
6
6
  import path from "path";
7
+ import { computeScreenFingerprint } from "../intera../interact/shared/fingerprint.js";
7
8
  const activeLogStreams = new Map();
8
9
  export class AndroidObserve {
9
10
  async getDeviceMetadata(appId, deviceId) {
@@ -229,6 +230,18 @@ export class AndroidObserve {
229
230
  };
230
231
  }
231
232
  }
233
+ async getScreenFingerprint(deviceId) {
234
+ try {
235
+ const tree = await this.getUITree(deviceId);
236
+ if (!tree || tree.error)
237
+ return { fingerprint: null, error: tree.error };
238
+ const current = await this.getCurrentScreen(deviceId).catch(() => null);
239
+ return computeScreenFingerprint(tree, current, 'android', 50);
240
+ }
241
+ catch (e) {
242
+ return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
243
+ }
244
+ }
232
245
  async startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') {
233
246
  try {
234
247
  const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '');
@@ -1,5 +1,5 @@
1
- import { iOSObserve } from '../../ios/observe.js';
2
- import { iOSManage } from '../../ios/manage.js';
1
+ import { iOSObserve } from '../../observe/index.js';
2
+ import { iOSManage } from '../../manage/index.js';
3
3
  async function main() {
4
4
  const appId = process.argv[2] || 'com.apple.springboard';
5
5
  const deviceId = 'booted';
@@ -1,5 +1,5 @@
1
- import { iOSObserve } from '../../ios/observe.js';
2
- import { iOSInteract } from '../../ios/interact.js';
1
+ import { iOSObserve } from '../../observe/index.js';
2
+ import { iOSInteract } from '../../interact/index.js';
3
3
  async function main() {
4
4
  const deviceId = 'booted';
5
5
  const obs = new iOSObserve();
@@ -0,0 +1,91 @@
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo } from "../utils/android/utils.js";
2
+ import { AndroidObserve } from "../observe/index.js";
3
+ import { scrollToElementShared } from "../utils/ui/index.js";
4
+ export class AndroidInteract {
5
+ observe = new AndroidObserve();
6
+ async waitForElement(text, timeout, deviceId) {
7
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
8
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
9
+ const startTime = Date.now();
10
+ while (Date.now() - startTime < timeout) {
11
+ try {
12
+ const tree = await this.observe.getUITree(deviceId);
13
+ if (tree.error) {
14
+ return { device: deviceInfo, found: false, error: tree.error };
15
+ }
16
+ const element = tree.elements.find(e => e.text === text);
17
+ if (element) {
18
+ return { device: deviceInfo, found: true, element };
19
+ }
20
+ }
21
+ catch (e) {
22
+ // Ignore errors during polling and retry
23
+ console.error("Error polling UI tree:", e);
24
+ }
25
+ const elapsed = Date.now() - startTime;
26
+ const remaining = timeout - elapsed;
27
+ if (remaining <= 0)
28
+ break;
29
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
30
+ }
31
+ return { device: deviceInfo, found: false };
32
+ }
33
+ async tap(x, y, deviceId) {
34
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
35
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
36
+ try {
37
+ await execAdb(['shell', 'input', 'tap', x.toString(), y.toString()], deviceId);
38
+ return { device: deviceInfo, success: true, x, y };
39
+ }
40
+ catch (e) {
41
+ return { device: deviceInfo, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
42
+ }
43
+ }
44
+ async swipe(x1, y1, x2, y2, duration, deviceId) {
45
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
46
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
47
+ try {
48
+ await execAdb(['shell', 'input', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString(), duration.toString()], deviceId);
49
+ return { device: deviceInfo, success: true, start: [x1, y1], end: [x2, y2], duration };
50
+ }
51
+ catch (e) {
52
+ return { device: deviceInfo, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
53
+ }
54
+ }
55
+ async typeText(text, deviceId) {
56
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
57
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
58
+ try {
59
+ // Encode spaces as %s to ensure proper input handling by adb shell input text
60
+ const encodedText = text.replace(/\s/g, '%s');
61
+ // Note: 'input text' might fail with some characters or if keyboard isn't ready, but it's the standard ADB way.
62
+ await execAdb(['shell', 'input', 'text', encodedText], deviceId);
63
+ return { device: deviceInfo, success: true, text };
64
+ }
65
+ catch (e) {
66
+ return { device: deviceInfo, success: false, text, error: e instanceof Error ? e.message : String(e) };
67
+ }
68
+ }
69
+ async pressBack(deviceId) {
70
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
71
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
72
+ try {
73
+ await execAdb(['shell', 'input', 'keyevent', '4'], deviceId);
74
+ return { device: deviceInfo, success: true };
75
+ }
76
+ catch (e) {
77
+ return { device: deviceInfo, success: false, error: e instanceof Error ? e.message : String(e) };
78
+ }
79
+ }
80
+ async scrollToElement(selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId) {
81
+ return await scrollToElementShared({
82
+ selector,
83
+ direction,
84
+ maxScrolls,
85
+ scrollAmount,
86
+ deviceId,
87
+ fetchTree: async () => await this.observe.getUITree(deviceId),
88
+ swipe: async (x1, y1, x2, y2, duration, devId) => await this.swipe(x1, y1, x2, y2, duration, devId)
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,76 @@
1
+ import { AndroidInteract } from './android.js';
2
+ import { iOSInteract } from './ios.js';
3
+ export { AndroidInteract, iOSInteract };
4
+ import { resolveTargetDevice } from '../utils/resolve-device.js';
5
+ import { ToolsObserve } from '../observe/index.js';
6
+ export class ToolsInteract {
7
+ static async getInteractionService(platform, deviceId) {
8
+ const effectivePlatform = platform || 'android';
9
+ const resolved = await resolveTargetDevice({ platform: effectivePlatform, deviceId });
10
+ const interact = effectivePlatform === 'android' ? new AndroidInteract() : new iOSInteract();
11
+ return { interact: interact, resolved, platform: effectivePlatform };
12
+ }
13
+ static async waitForElementHandler({ platform, text, timeout, deviceId }) {
14
+ const effectiveTimeout = timeout ?? 10000;
15
+ const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
16
+ return await interact.waitForElement(text, effectiveTimeout, resolved.id);
17
+ }
18
+ static async tapHandler({ platform, x, y, deviceId }) {
19
+ const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
20
+ return await interact.tap(x, y, resolved.id);
21
+ }
22
+ static async swipeHandler({ platform = 'android', x1, y1, x2, y2, duration, deviceId }) {
23
+ const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
24
+ return await interact.swipe(x1, y1, x2, y2, duration, resolved.id);
25
+ }
26
+ static async typeTextHandler({ text, deviceId }) {
27
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
28
+ return await new AndroidInteract().typeText(text, resolved.id);
29
+ }
30
+ static async pressBackHandler({ deviceId }) {
31
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId });
32
+ return await new AndroidInteract().pressBack(resolved.id);
33
+ }
34
+ static async scrollToElementHandler({ platform, selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId }) {
35
+ const { interact, resolved } = await ToolsInteract.getInteractionService(platform, deviceId);
36
+ return await interact.scrollToElement(selector, direction, maxScrolls, scrollAmount, resolved.id);
37
+ }
38
+ static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }) {
39
+ const start = Date.now();
40
+ let lastFingerprint = null;
41
+ while (Date.now() - start < timeoutMs) {
42
+ try {
43
+ const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
44
+ const fp = res?.fingerprint ?? null;
45
+ if (fp === null || fp === undefined) {
46
+ lastFingerprint = null;
47
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
48
+ continue;
49
+ }
50
+ lastFingerprint = fp;
51
+ if (fp !== previousFingerprint) {
52
+ // Stability confirmation
53
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
54
+ try {
55
+ const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
56
+ const confirmFp = confirmRes?.fingerprint ?? null;
57
+ if (confirmFp === fp) {
58
+ return { success: true, newFingerprint: fp, elapsedMs: Date.now() - start };
59
+ }
60
+ lastFingerprint = confirmFp;
61
+ continue;
62
+ }
63
+ catch {
64
+ // ignore and continue polling
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // ignore transient errors
71
+ }
72
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
73
+ }
74
+ return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
75
+ }
76
+ }
@@ -0,0 +1,120 @@
1
+ import { spawn } from "child_process";
2
+ import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "../utils/ios/utils.js";
3
+ import { iOSObserve } from "../observe/index.js";
4
+ import { scrollToElementShared } from "../utils/ui/index.js";
5
+ export class iOSInteract {
6
+ observe = new iOSObserve();
7
+ async waitForElement(text, timeout, deviceId = "booted") {
8
+ const device = await getIOSDeviceMetadata(deviceId);
9
+ const startTime = Date.now();
10
+ while (Date.now() - startTime < timeout) {
11
+ try {
12
+ const tree = await this.observe.getUITree(deviceId);
13
+ if (tree.error) {
14
+ return { device, found: false, error: tree.error };
15
+ }
16
+ const element = tree.elements.find(e => e.text === text);
17
+ if (element) {
18
+ return { device, found: true, element };
19
+ }
20
+ }
21
+ catch (e) {
22
+ // Ignore errors during polling and retry
23
+ console.error("Error polling UI tree:", e);
24
+ }
25
+ const elapsed = Date.now() - startTime;
26
+ const remaining = timeout - elapsed;
27
+ if (remaining <= 0)
28
+ break;
29
+ await new Promise(resolve => setTimeout(resolve, Math.min(500, remaining)));
30
+ }
31
+ return { device, found: false };
32
+ }
33
+ async tap(x, y, deviceId = "booted") {
34
+ const device = await getIOSDeviceMetadata(deviceId);
35
+ // Use shared helper to detect idb
36
+ const idbExists = await isIDBInstalled();
37
+ if (!idbExists) {
38
+ return {
39
+ device,
40
+ success: false,
41
+ x,
42
+ y,
43
+ error: "iOS tap requires 'idb' (iOS Device Bridge)."
44
+ };
45
+ }
46
+ try {
47
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
48
+ const args = ['ui', 'tap', x.toString(), y.toString()];
49
+ if (targetUdid) {
50
+ args.push('--udid', targetUdid);
51
+ }
52
+ await new Promise((resolve, reject) => {
53
+ const proc = spawn(getIdbCmd(), args);
54
+ let stderr = '';
55
+ proc.stderr.on('data', d => stderr += d.toString());
56
+ proc.on('close', code => {
57
+ if (code === 0)
58
+ resolve();
59
+ else
60
+ reject(new Error(`idb ui tap failed: ${stderr}`));
61
+ });
62
+ proc.on('error', err => reject(err));
63
+ });
64
+ return { device, success: true, x, y };
65
+ }
66
+ catch (e) {
67
+ return { device, success: false, x, y, error: e instanceof Error ? e.message : String(e) };
68
+ }
69
+ }
70
+ async swipe(x1, y1, x2, y2, duration, deviceId = "booted") {
71
+ const device = await getIOSDeviceMetadata(deviceId);
72
+ // Use shared helper to detect idb
73
+ const idbExists = await isIDBInstalled();
74
+ if (!idbExists) {
75
+ return {
76
+ device,
77
+ success: false,
78
+ start: [x1, y1],
79
+ end: [x2, y2],
80
+ duration,
81
+ error: "iOS swipe requires 'idb' (iOS Device Bridge)."
82
+ };
83
+ }
84
+ try {
85
+ const targetUdid = (device.id && device.id !== 'booted') ? device.id : undefined;
86
+ // idb 'ui swipe' does not accept a duration parameter; use coordinates only
87
+ const args = ['ui', 'swipe', x1.toString(), y1.toString(), x2.toString(), y2.toString()];
88
+ if (targetUdid) {
89
+ args.push('--udid', targetUdid);
90
+ }
91
+ await new Promise((resolve, reject) => {
92
+ const proc = spawn(getIdbCmd(), args);
93
+ let stderr = '';
94
+ proc.stderr.on('data', d => stderr += d.toString());
95
+ proc.on('close', code => {
96
+ if (code === 0)
97
+ resolve();
98
+ else
99
+ reject(new Error(`idb ui swipe failed: ${stderr}`));
100
+ });
101
+ proc.on('error', err => reject(err));
102
+ });
103
+ return { device, success: true, start: [x1, y1], end: [x2, y2], duration };
104
+ }
105
+ catch (e) {
106
+ return { device, success: false, start: [x1, y1], end: [x2, y2], duration, error: e instanceof Error ? e.message : String(e) };
107
+ }
108
+ }
109
+ async scrollToElement(selector, direction = 'down', maxScrolls = 10, scrollAmount = 0.7, deviceId = 'booted') {
110
+ return await scrollToElementShared({
111
+ selector,
112
+ direction,
113
+ maxScrolls,
114
+ scrollAmount,
115
+ deviceId,
116
+ fetchTree: async () => await this.observe.getUITree(deviceId),
117
+ swipe: async (x1, y1, x2, y2, duration, devId) => await this.swipe(x1, y1, x2, y2, duration, devId)
118
+ });
119
+ }
120
+ }
@@ -0,0 +1 @@
1
+ export { computeScreenFingerprint } from '../../utils/ui/index.js';
@@ -0,0 +1 @@
1
+ export { scrollToElementShared } from '../../utils/ui/index.js';
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import { getIOSDeviceMetadata, getIdbCmd, isIDBInstalled } from "./utils.js";
3
- import { iOSObserve } from "./observe.js";
4
- import { scrollToElementShared } from "../tools/scroll_to_element.js";
3
+ import { iOSObserve } from "../observe/index.js";
4
+ import { scrollToElementShared } from "../intera../interact/shared/scroll_to_element.js";
5
5
  export class iOSInteract {
6
6
  observe = new iOSObserve();
7
7
  async waitForElement(text, timeout, deviceId = "booted") {
@@ -4,6 +4,7 @@ import { execCommand, getIOSDeviceMetadata, validateBundleId, getIdbCmd, getXcru
4
4
  import { createWriteStream, promises as fsPromises } from 'fs';
5
5
  import path from 'path';
6
6
  import { parseLogLine } from '../android/utils.js';
7
+ import { computeScreenFingerprint } from '../intera../interact/shared/fingerprint.js';
7
8
  // --- Helper Functions Specific to Observe ---
8
9
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
9
10
  function parseIDBFrame(frame) {
@@ -237,6 +238,17 @@ export class iOSObserve {
237
238
  };
238
239
  }
239
240
  }
241
+ async getScreenFingerprint(deviceId = 'booted') {
242
+ try {
243
+ const tree = await this.getUITree(deviceId);
244
+ if (!tree || tree.error)
245
+ return { fingerprint: null, error: tree && tree.error };
246
+ return computeScreenFingerprint(tree, null, 'ios', 50);
247
+ }
248
+ catch (e) {
249
+ return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
250
+ }
251
+ }
240
252
  // --- Log stream methods ---
241
253
  async startLogStream(bundleId, deviceId = 'booted', sessionId = 'default') {
242
254
  try {
@@ -0,0 +1,162 @@
1
+ import { promises as fs } from 'fs';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { existsSync } from 'fs';
5
+ import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js';
6
+ import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
7
+ import { detectJavaHome } from '../utils/java.js';
8
+ export class AndroidManage {
9
+ async build(projectPath, _variant) {
10
+ void _variant;
11
+ try {
12
+ // Always use the shared prepareGradle utility for consistent env/setup
13
+ const { execCmd, gradleArgs, spawnOpts } = await (await import('../utils/android/utils.js')).prepareGradle(projectPath);
14
+ await new Promise((resolve, reject) => {
15
+ const proc = spawn(execCmd, gradleArgs, spawnOpts);
16
+ let stderr = '';
17
+ proc.stderr?.on('data', d => stderr += d.toString());
18
+ proc.on('close', code => {
19
+ if (code === 0)
20
+ resolve();
21
+ else
22
+ reject(new Error(stderr || `Gradle failed with code ${code}`));
23
+ });
24
+ proc.on('error', err => reject(err));
25
+ });
26
+ const apk = await findApk(projectPath);
27
+ if (!apk)
28
+ return { error: 'Could not find APK after build' };
29
+ return { artifactPath: apk };
30
+ }
31
+ catch (e) {
32
+ return { error: e instanceof Error ? e.message : String(e) };
33
+ }
34
+ }
35
+ async installApp(apkPath, deviceId) {
36
+ const metadata = await getAndroidDeviceMetadata('', deviceId);
37
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
38
+ let apkToInstall = apkPath;
39
+ try {
40
+ const stat = await fs.stat(apkPath).catch(() => null);
41
+ if (stat && stat.isDirectory()) {
42
+ const detectedJavaHome = await detectJavaHome().catch(() => undefined);
43
+ const env = Object.assign({}, process.env);
44
+ if (detectedJavaHome) {
45
+ if (env.JAVA_HOME !== detectedJavaHome) {
46
+ env.JAVA_HOME = detectedJavaHome;
47
+ env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
48
+ console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome);
49
+ }
50
+ }
51
+ try {
52
+ delete env.SHELL;
53
+ }
54
+ catch { }
55
+ const gradleArgs = ['assembleDebug'];
56
+ if (detectedJavaHome) {
57
+ gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
58
+ gradleArgs.push('--no-daemon');
59
+ env.GRADLE_JAVA_HOME = detectedJavaHome;
60
+ }
61
+ const wrapperPath = path.join(apkPath, 'gradlew');
62
+ const useWrapper = existsSync(wrapperPath);
63
+ const execCmd = useWrapper ? wrapperPath : 'gradle';
64
+ const spawnOpts = { cwd: apkPath, env };
65
+ if (useWrapper) {
66
+ await fs.chmod(wrapperPath, 0o755).catch(() => { });
67
+ spawnOpts.shell = false;
68
+ }
69
+ else
70
+ spawnOpts.shell = true;
71
+ const proc = spawn(execCmd, gradleArgs, spawnOpts);
72
+ let stderr = '';
73
+ await new Promise((resolve, reject) => {
74
+ proc.stderr?.on('data', d => stderr += d.toString());
75
+ proc.on('close', code => {
76
+ if (code === 0)
77
+ resolve();
78
+ else
79
+ reject(new Error(stderr || `Gradle build failed with code ${code}`));
80
+ });
81
+ proc.on('error', err => reject(err));
82
+ });
83
+ const built = await findApk(apkPath);
84
+ if (!built)
85
+ throw new Error('Could not locate built APK after running Gradle');
86
+ apkToInstall = built;
87
+ }
88
+ try {
89
+ const res = await spawnAdb(['install', '-r', apkToInstall], deviceId);
90
+ if (res.code === 0) {
91
+ return { device: deviceInfo, installed: true, output: res.stdout };
92
+ }
93
+ }
94
+ catch (e) {
95
+ console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
96
+ }
97
+ const basename = path.basename(apkToInstall);
98
+ const remotePath = `/data/local/tmp/${basename}`;
99
+ await execAdb(['push', apkToInstall, remotePath], deviceId);
100
+ const pmOut = await execAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
101
+ try {
102
+ await execAdb(['shell', 'rm', remotePath], deviceId);
103
+ }
104
+ catch { }
105
+ return { device: deviceInfo, installed: true, output: pmOut };
106
+ }
107
+ catch (e) {
108
+ // gather diagnostics for attempted adb operations
109
+ const basename = path.basename(apkToInstall);
110
+ const remotePath = `/data/local/tmp/${basename}`;
111
+ const installDiag = execAdbWithDiagnostics(['install', '-r', apkToInstall], deviceId);
112
+ const pushDiag = execAdbWithDiagnostics(['push', apkToInstall, remotePath], deviceId);
113
+ const pmDiag = execAdbWithDiagnostics(['shell', 'pm', 'install', '-r', remotePath], deviceId);
114
+ return { device: deviceInfo, installed: false, error: e instanceof Error ? e.message : String(e), diagnostics: { installDiag, pushDiag, pmDiag } };
115
+ }
116
+ }
117
+ async startApp(appId, deviceId) {
118
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
119
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
120
+ try {
121
+ await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
122
+ return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
123
+ }
124
+ catch (e) {
125
+ const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
126
+ return { device: deviceInfo, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
127
+ }
128
+ }
129
+ async terminateApp(appId, deviceId) {
130
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
131
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
132
+ try {
133
+ await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
134
+ return { device: deviceInfo, appTerminated: true };
135
+ }
136
+ catch (e) {
137
+ const diag = execAdbWithDiagnostics(['shell', 'am', 'force-stop', appId], deviceId);
138
+ return { device: deviceInfo, appTerminated: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
139
+ }
140
+ }
141
+ async restartApp(appId, deviceId) {
142
+ await this.terminateApp(appId, deviceId);
143
+ const startResult = await this.startApp(appId, deviceId);
144
+ return {
145
+ device: startResult.device,
146
+ appRestarted: startResult.appStarted,
147
+ launchTimeMs: startResult.launchTimeMs
148
+ };
149
+ }
150
+ async resetAppData(appId, deviceId) {
151
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
152
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
153
+ try {
154
+ const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
155
+ return { device: deviceInfo, dataCleared: output === 'Success' };
156
+ }
157
+ catch (e) {
158
+ const diag = execAdbWithDiagnostics(['shell', 'pm', 'clear', appId], deviceId);
159
+ return { device: deviceInfo, dataCleared: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
160
+ }
161
+ }
162
+ }