mobile-debug-mcp 0.9.0 → 0.11.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 (61) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.github/workflows/.gitkeep +0 -0
  4. package/.github/workflows/ci.yml +63 -0
  5. package/README.md +5 -16
  6. package/dist/android/interact.js +1 -123
  7. package/dist/android/manage.js +137 -0
  8. package/dist/android/observe.js +133 -88
  9. package/dist/android/run.js +187 -0
  10. package/dist/android/utils.js +181 -236
  11. package/dist/ios/interact.js +1 -168
  12. package/dist/ios/manage.js +145 -0
  13. package/dist/ios/observe.js +112 -5
  14. package/dist/ios/run.js +200 -0
  15. package/dist/ios/utils.js +19 -118
  16. package/dist/server.js +44 -42
  17. package/dist/tools/install.js +1 -1
  18. package/dist/tools/interact.js +39 -0
  19. package/dist/tools/logs.js +2 -2
  20. package/dist/tools/manage.js +180 -0
  21. package/dist/tools/observe.js +80 -0
  22. package/dist/tools/run.js +180 -0
  23. package/dist/utils/index.js +1 -0
  24. package/dist/utils/java.js +76 -0
  25. package/docs/CHANGELOG.md +25 -6
  26. package/eslint.config.cjs +36 -0
  27. package/eslint.config.js +60 -0
  28. package/package.json +8 -2
  29. package/src/android/interact.ts +2 -136
  30. package/src/android/manage.ts +135 -0
  31. package/src/android/observe.ts +129 -97
  32. package/src/android/utils.ts +199 -229
  33. package/src/ios/interact.ts +2 -175
  34. package/src/ios/manage.ts +143 -0
  35. package/src/ios/observe.ts +113 -5
  36. package/src/ios/utils.ts +20 -122
  37. package/src/server.ts +48 -58
  38. package/src/tools/interact.ts +45 -0
  39. package/src/tools/manage.ts +171 -0
  40. package/src/tools/observe.ts +82 -0
  41. package/src/utils/index.ts +1 -0
  42. package/src/utils/java.ts +69 -0
  43. package/test/integration/install.integration.ts +3 -3
  44. package/test/integration/logstream-real.ts +5 -4
  45. package/test/integration/run-install-android.ts +1 -1
  46. package/test/integration/run-install-ios.ts +1 -1
  47. package/test/integration/smoke-test.ts +1 -1
  48. package/test/integration/test-dist.ts +1 -1
  49. package/test/integration/test-ui-tree.ts +1 -1
  50. package/test/integration/wait_for_element_real.ts +1 -1
  51. package/test/unit/build.test.ts +84 -0
  52. package/test/unit/build_and_install.test.ts +132 -0
  53. package/test/unit/detect-java.test.ts +22 -0
  54. package/test/unit/install.test.ts +2 -8
  55. package/test/unit/logstream.test.ts +8 -9
  56. package/src/tools/app.ts +0 -46
  57. package/src/tools/devices.ts +0 -6
  58. package/src/tools/install.ts +0 -43
  59. package/src/tools/logs.ts +0 -62
  60. package/src/tools/screenshot.ts +0 -18
  61. package/src/tools/ui.ts +0 -62
@@ -12,7 +12,7 @@ async function main() {
12
12
  try {
13
13
  const res = await inter.installApp(appPath, deviceId || 'booted')
14
14
  console.log(JSON.stringify(res, null, 2))
15
- } catch (err) {
15
+ } catch {
16
16
  console.error('Install failed:', err instanceof Error ? err.message : String(err))
17
17
  process.exit(2)
18
18
  }
@@ -105,7 +105,7 @@ async function main() {
105
105
 
106
106
  console.log(`\n✨ Smoke test COMPLETED SUCCESSFULLY! ✨\n`);
107
107
 
108
- } catch (error) {
108
+ } catch {
109
109
  console.error(`\n❌ Smoke test FAILED:`, error);
110
110
  process.exit(1);
111
111
  }
@@ -32,7 +32,7 @@ async function main() {
32
32
  } else {
33
33
  console.log('No screenshot returned')
34
34
  }
35
- } catch (err) {
35
+ } catch {
36
36
  console.error('Smoke test script failed:', err)
37
37
  process.exit(1)
38
38
  }
@@ -67,7 +67,7 @@ async function main() {
67
67
  console.log(`- Elements with text: ${withText}`);
68
68
  }
69
69
 
70
- } catch (error) {
70
+ } catch {
71
71
  console.error("\n❌ Test Failed:", error);
72
72
  process.exit(1);
73
73
  }
@@ -72,7 +72,7 @@ async function runRealTest() {
72
72
  console.log(`Calls: ${calls} ${calls === 3 ? "PASS" : "FAIL"}`);
73
73
  console.log(`Elapsed time (should be >= 1000ms): ${elapsed3} ${elapsed3 >= 1000 ? "PASS" : "FAIL"}`);
74
74
 
75
- } catch (error) {
75
+ } catch {
76
76
  console.error("Test failed with error:", error);
77
77
  }
78
78
  }
@@ -0,0 +1,84 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ // Test build_app handler by creating fake gradlew and xcodebuild behaviours
7
+
8
+ async function makeTempProject(platform: 'android' | 'ios') {
9
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), `mcp-build-${platform}-`))
10
+ if (platform === 'android') {
11
+ const gradlew = path.join(dir, 'gradlew')
12
+ const script = `#!/usr/bin/env node
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const apk = path.join(process.cwd(),'app','build','outputs','apk','debug','app-debug.apk')
16
+ fs.mkdirSync(path.dirname(apk), { recursive: true })
17
+ fs.writeFileSync(apk, 'fake-apk')
18
+ console.log('BUILD SUCCESS')
19
+ process.exit(0)
20
+ `
21
+ await fs.writeFile(gradlew, script, { mode: 0o755 })
22
+ } else {
23
+ // create minimal Xcode workspace structure
24
+ const ws = path.join(dir, 'Example.xcworkspace')
25
+ await fs.writeFile(ws, '')
26
+ const script = `#!/usr/bin/env node
27
+ const fs = require('fs')
28
+ const path = require('path')
29
+ const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
30
+ fs.mkdirSync(app, { recursive: true })
31
+ fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
32
+ console.log('BUILD SUCCESS')
33
+ process.exit(0)
34
+ `
35
+ const xbuild = path.join(dir, 'xcodebuild')
36
+ await fs.writeFile(xbuild, script, { mode: 0o755 })
37
+ }
38
+ return dir
39
+ }
40
+
41
+ export async function run() {
42
+ const androidProject = await makeTempProject('android')
43
+ const iosProject = await makeTempProject('ios')
44
+
45
+ // Create a fake xcodebuild in PATH for the iOS build step
46
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-xcode-bin-'))
47
+ const xcodeScript = `#!/usr/bin/env node
48
+ const fs = require('fs')
49
+ const path = require('path')
50
+ // emulate xcodebuild by creating an Example.app inside Build/Products/Debug-iphonesimulator
51
+ const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
52
+ fs.mkdirSync(app, { recursive: true })
53
+ fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
54
+ console.log('xcodebuild simulated')
55
+ process.exit(0)
56
+ `
57
+ const xcodePath = path.join(binDir, 'xcodebuild')
58
+ await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
59
+
60
+ const origPath = process.env.PATH || ''
61
+ process.env.PATH = `${binDir}:${origPath}`
62
+
63
+ const { ToolsManage } = await import('../../src/tools/manage.js')
64
+
65
+ try {
66
+ const ares = await ToolsManage.buildAppHandler({ platform: 'android', projectPath: androidProject })
67
+ console.log('android build', ares)
68
+ assert.ok((ares as any).artifactPath && (ares as any).artifactPath.endsWith('.apk'))
69
+
70
+ const ires = await ToolsManage.buildAppHandler({ platform: 'ios', projectPath: iosProject })
71
+ console.log('ios build', ires)
72
+ assert.ok((ires as any).artifactPath && (ires as any).artifactPath.endsWith('.app'))
73
+
74
+ console.log('build tests passed')
75
+ } finally {
76
+ // cleanup
77
+ await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
78
+ await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
79
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
80
+ process.env.PATH = origPath
81
+ }
82
+ }
83
+
84
+ run().catch(e => { console.error(e); process.exit(1) })
@@ -0,0 +1,132 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ async function makeAndroidProject() {
7
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-android-'))
8
+ const gradlew = path.join(dir, 'gradlew')
9
+ const script = `#!/usr/bin/env node
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+ const apk = path.join(process.cwd(),'app','build','outputs','apk','debug','app-debug.apk')
13
+ fs.mkdirSync(path.dirname(apk), { recursive: true })
14
+ fs.writeFileSync(apk, 'fake-apk')
15
+ console.log('BUILD SUCCESS')
16
+ process.exit(0)
17
+ `
18
+ await fs.writeFile(gradlew, script, { mode: 0o755 })
19
+ return dir
20
+ }
21
+
22
+ async function makeIOSProject() {
23
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-ios-'))
24
+ const ws = path.join(dir, 'Example.xcworkspace')
25
+ await fs.writeFile(ws, '')
26
+ return dir
27
+ }
28
+
29
+ export async function run() {
30
+ const androidProject = await makeAndroidProject()
31
+ const iosProject = await makeIOSProject()
32
+
33
+ // Create fake bin dir with adb and simctl
34
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-bin-'))
35
+ const adbPath = path.join(binDir, 'adb')
36
+ const adbScript = `#!/usr/bin/env node
37
+ const args = process.argv.slice(2)
38
+ if (args.includes('devices')) {
39
+ console.log('List of devices attached')
40
+ console.log('emulator-5554\tdevice product:sdk_gphone64_arm64')
41
+ process.exit(0)
42
+ }
43
+ if (args.includes('install')) {
44
+ console.log('Performing Streamed Install')
45
+ console.log('Success')
46
+ process.exit(0)
47
+ }
48
+ if (args.includes('shell')) {
49
+ const idx = args.indexOf('shell')
50
+ const shellArgs = args.slice(idx+1)
51
+ if (shellArgs[0] === 'getprop') {
52
+ console.log('')
53
+ process.exit(0)
54
+ }
55
+ console.log('Success')
56
+ process.exit(0)
57
+ }
58
+ console.log('OK')
59
+ process.exit(0)
60
+ `
61
+ await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
62
+
63
+ const simctlPath = path.join(binDir, 'simctl')
64
+ const simctlScript = `#!/usr/bin/env node
65
+ const args = process.argv.slice(2)
66
+ if (args.includes('install')) {
67
+ console.log('simctl install simulated')
68
+ process.exit(0)
69
+ }
70
+ // Respond to 'list devices --json'
71
+ if (args.includes('list') && args.includes('devices') && args.includes('--json')) {
72
+ const out = { devices: { 'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [ { udid: 'booted', name: 'iPhone 14', state: 'Booted' } ] } }
73
+ console.log(JSON.stringify(out))
74
+ process.exit(0)
75
+ }
76
+ console.log('simctl ok')
77
+ process.exit(0)
78
+ `
79
+ await fs.writeFile(simctlPath, simctlScript, { mode: 0o755 })
80
+
81
+ // fake xcodebuild for iOS build step
82
+ const xcodeScript = `#!/usr/bin/env node
83
+ const fs = require('fs')
84
+ const path = require('path')
85
+ const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
86
+ fs.mkdirSync(app, { recursive: true })
87
+ fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
88
+ console.log('xcodebuild simulated')
89
+ process.exit(0)
90
+ `
91
+ const xcodePath = path.join(binDir, 'xcodebuild')
92
+ await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
93
+
94
+ const origPath = process.env.PATH || ''
95
+ const origXcrun = process.env.XCRUN_PATH
96
+ process.env.PATH = `${binDir}:${origPath}`
97
+ process.env.XCRUN_PATH = simctlPath
98
+
99
+ const { ToolsManage } = await import('../../src/tools/manage.js')
100
+
101
+ try {
102
+ // Android build_and_install
103
+ const ares = await ToolsManage.buildAndInstallHandler({ platform: 'android', projectPath: androidProject, deviceId: 'emulator-5554' })
104
+ console.log('android ndjson:\n', ares.ndjson)
105
+ console.log('android result:', ares.result)
106
+ assert.ok(ares.result.success === true, 'android build_and_install should succeed')
107
+ assert.ok(ares.result.artifactPath && ares.result.artifactPath.endsWith('.apk'))
108
+ assert.ok(ares.ndjson.includes('"type":"build"'))
109
+ assert.ok(ares.ndjson.includes('"type":"install"'))
110
+
111
+ // iOS build_and_install
112
+ const ires = await ToolsManage.buildAndInstallHandler({ platform: 'ios', projectPath: iosProject, deviceId: 'booted' })
113
+ console.log('ios ndjson:\n', ires.ndjson)
114
+ console.log('ios result:', ires.result)
115
+ assert.ok(ires.result.success === true, 'ios build_and_install should succeed')
116
+ assert.ok(ires.result.artifactPath && ires.result.artifactPath.endsWith('.app'))
117
+ assert.ok(ires.ndjson.includes('"type":"build"'))
118
+ assert.ok(ires.ndjson.includes('"type":"install"'))
119
+
120
+ console.log('build_and_install tests passed')
121
+ } finally {
122
+ process.env.PATH = origPath
123
+ if (origXcrun === undefined) delete process.env.XCRUN_PATH
124
+ else process.env.XCRUN_PATH = origXcrun
125
+ await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
126
+ await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
127
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
128
+ }
129
+ }
130
+
131
+
132
+ run().catch(e => { console.error(e); process.exit(1) })
@@ -0,0 +1,22 @@
1
+ import assert from 'assert'
2
+ import { detectJavaHome } from '../../src/utils/java.js'
3
+
4
+ // These tests are lightweight smoke tests; they don't rely on actual JDK17 installs,
5
+ // but exercise the failure modes and ensure the function returns undefined or a string.
6
+
7
+ export async function run() {
8
+ const res = await detectJavaHome()
9
+ // It's acceptable for local dev env to not have JDK17; just ensure call returns (string|undefined)
10
+ assert.ok(typeof res === 'string' || typeof res === 'undefined')
11
+
12
+ // Basic mocking: if JAVA_HOME points to a fake path, detection should return undefined
13
+ const orig = process.env.JAVA_HOME
14
+ process.env.JAVA_HOME = '/non/existent/java/home'
15
+ const res2 = await detectJavaHome()
16
+ assert.ok(typeof res2 === 'undefined')
17
+ process.env.JAVA_HOME = orig
18
+
19
+ console.log('detectJavaHome tests passed')
20
+ }
21
+
22
+ run().catch((e) => { console.error(e); process.exit(1) })
@@ -2,7 +2,6 @@ import assert from 'assert'
2
2
  import fs from 'fs/promises'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
- import { createRequire } from 'module'
6
5
 
7
6
  // This test mocks child_process.spawn and simulates a Gradle build producing an APK
8
7
  // and an adb install. It does not patch AndroidInteract.installApp itself so the
@@ -15,11 +14,6 @@ async function makeTempFile(ext: string) {
15
14
  return { dir, file }
16
15
  }
17
16
 
18
- async function makeTempDirWith(name: string) {
19
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
20
- await fs.writeFile(path.join(dir, name), '')
21
- return dir
22
- }
23
17
 
24
18
  export async function run() {
25
19
  // Create a fake adb executable in a temporary bin dir and prepend to PATH so
@@ -37,12 +31,12 @@ process.exit(0)
37
31
  process.env.PATH = `${binDir}:${origPath}`
38
32
 
39
33
  // Import the module under test after PATH is adjusted
40
- const { AndroidInteract } = await import('../../src/android/interact.js')
34
+ const { AndroidManage } = await import('../../src/android/manage.js')
41
35
 
42
36
  try {
43
37
  // Test: install with .apk file should call adb install
44
38
  const { dir: d1, file: apk } = await makeTempFile('.apk')
45
- const ai = new AndroidInteract()
39
+ const ai = new AndroidManage()
46
40
  const res1 = await ai.installApp(apk)
47
41
  console.log('res1', res1)
48
42
  assert.ok(res1.installed === true, 'APK install should succeed')
@@ -1,11 +1,12 @@
1
1
  import { promises as fs } from 'fs'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
- import { readLogStreamLines, _setActiveLogStream, _clearActiveLogStream } from '../../src/android/utils.js'
4
+ import { AndroidObserve } from '../../src/android/observe.js'
5
5
 
6
6
  async function run() {
7
7
  const tmp = os.tmpdir()
8
- const file = path.join(tmp, `test-mobile-debug-log-${Date.now()}.ndjson`)
8
+ const sessionId = 'unit-test-logstream'
9
+ const file = path.join(tmp, `mobile-debug-log-${sessionId}.ndjson`)
9
10
 
10
11
  // Prepare NDJSON with one crash entry and one info entry
11
12
  const crashEntry = { timestamp: '2026-03-13T14:00:00.000Z', level: 'E', tag: 'AndroidRuntime', message: 'FATAL EXCEPTION: main\njava.lang.NullPointerException' }
@@ -13,12 +14,11 @@ async function run() {
13
14
 
14
15
  await fs.writeFile(file, JSON.stringify(crashEntry) + '\n' + JSON.stringify(infoEntry) + '\n')
15
16
 
16
- const sessionId = 'unit-test-logstream'
17
- _setActiveLogStream(sessionId, file)
18
17
 
19
18
  try {
20
- // Read all
21
- const { entries, crash_summary } = await readLogStreamLines(sessionId, 10)
19
+ // Read all via AndroidObserve (falls back to session NDJSON file)
20
+ const obs = new AndroidObserve()
21
+ const { entries, crash_summary } = await obs.readLogStream(sessionId, 10)
22
22
  if (!Array.isArray(entries) || entries.length !== 2) throw new Error('Expected 2 entries')
23
23
  if (!crash_summary || crash_summary.crash_detected !== true) throw new Error('Expected crash_detected true')
24
24
  if (!crash_summary.exception || !/NullPointerException/.test(crash_summary.exception)) throw new Error('Expected NullPointerException detected')
@@ -27,18 +27,17 @@ async function run() {
27
27
 
28
28
  // Test since filter (after first entry)
29
29
  const since = new Date('2026-03-13T14:00:30.000Z').toISOString()
30
- const r2 = await readLogStreamLines(sessionId, 10, since)
30
+ const r2 = await obs.readLogStream(sessionId, 10, since)
31
31
  if (r2.entries.length !== 1) throw new Error('Expected 1 entry after since filter')
32
32
  console.log('Test 2 PASS: since filter')
33
33
 
34
34
  // Test limit
35
- const r3 = await readLogStreamLines(sessionId, 1)
35
+ const r3 = await obs.readLogStream(sessionId, 1)
36
36
  if (r3.entries.length !== 1) throw new Error('Expected 1 entry with limit=1')
37
37
  console.log('Test 3 PASS: limit works')
38
38
 
39
39
  console.log('ALL logstream tests passed')
40
40
  } finally {
41
- _clearActiveLogStream(sessionId)
42
41
  await fs.unlink(file).catch(()=>{})
43
42
  }
44
43
  }
package/src/tools/app.ts DELETED
@@ -1,46 +0,0 @@
1
- import { resolveTargetDevice } from '../resolve-device.js'
2
- import { AndroidInteract } from '../android/interact.js'
3
- import { iOSInteract } from '../ios/interact.js'
4
-
5
- const androidInteract = new AndroidInteract()
6
- const iosInteract = new iOSInteract()
7
-
8
- export async function startAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
9
- if (platform === 'android') {
10
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
11
- return await androidInteract.startApp(appId, resolved.id)
12
- } else {
13
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
14
- return await iosInteract.startApp(appId, resolved.id)
15
- }
16
- }
17
-
18
- export async function terminateAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
19
- if (platform === 'android') {
20
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
21
- return await androidInteract.terminateApp(appId, resolved.id)
22
- } else {
23
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
24
- return await iosInteract.terminateApp(appId, resolved.id)
25
- }
26
- }
27
-
28
- export async function restartAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
29
- if (platform === 'android') {
30
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
31
- return await androidInteract.restartApp(appId, resolved.id)
32
- } else {
33
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
34
- return await iosInteract.restartApp(appId, resolved.id)
35
- }
36
- }
37
-
38
- export async function resetAppDataHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
39
- if (platform === 'android') {
40
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
41
- return await androidInteract.resetAppData(appId, resolved.id)
42
- } else {
43
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
44
- return await iosInteract.resetAppData(appId, resolved.id)
45
- }
46
- }
@@ -1,6 +0,0 @@
1
- import { listDevices } from '../resolve-device.js'
2
-
3
- export async function listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
4
- const devices = await listDevices(platform as any, appId)
5
- return { devices }
6
- }
@@ -1,43 +0,0 @@
1
- import { promises as fs } from 'fs'
2
- import path from 'path'
3
- import { resolveTargetDevice } from '../resolve-device.js'
4
- import { AndroidInteract } from '../android/interact.js'
5
- import { iOSInteract } from '../ios/interact.js'
6
-
7
- const androidInteract = new AndroidInteract()
8
- const iosInteract = new iOSInteract()
9
-
10
- export async function installAppHandler({ platform, appPath, deviceId }: { platform?: 'android' | 'ios', appPath: string, deviceId?: string }) {
11
- let chosenPlatform: 'android' | 'ios' | undefined = platform
12
-
13
- try {
14
- const stat = await fs.stat(appPath).catch(() => null)
15
- if (stat && stat.isDirectory()) {
16
- const files = (await fs.readdir(appPath).catch(() => [])) as string[]
17
- if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
18
- chosenPlatform = 'ios'
19
- } else if (files.includes('gradlew') || files.includes('build.gradle') || files.includes('settings.gradle') || (files.includes('app') && (await fs.stat(path.join(appPath, 'app')).catch(() => null)))) {
20
- chosenPlatform = 'android'
21
- } else {
22
- chosenPlatform = 'android'
23
- }
24
- } else if (typeof appPath === 'string') {
25
- const ext = path.extname(appPath).toLowerCase()
26
- if (ext === '.apk') chosenPlatform = 'android'
27
- else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
28
- else chosenPlatform = 'android'
29
- }
30
- } catch (e) {
31
- chosenPlatform = 'android'
32
- }
33
-
34
- if (chosenPlatform === 'android') {
35
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
36
- const result = await androidInteract.installApp(appPath, resolved.id)
37
- return result
38
- } else {
39
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
40
- const result = await iosInteract.installApp(appPath, resolved.id)
41
- return result
42
- }
43
- }
package/src/tools/logs.ts DELETED
@@ -1,62 +0,0 @@
1
- import { resolveTargetDevice } from '../resolve-device.js'
2
- import { AndroidObserve } from '../android/observe.js'
3
- import { iOSObserve } from '../ios/observe.js'
4
- import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from '../android/utils.js'
5
- import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from '../ios/utils.js'
6
-
7
- const androidObserve = new AndroidObserve()
8
-
9
- export async function getLogsHandler({ platform, appId, deviceId, lines }: { platform: 'android' | 'ios', appId?: string, deviceId?: string, lines?: number }) {
10
- if (platform === 'android') {
11
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
12
- const deviceInfo = resolved
13
- const response = await androidObserve.getLogs(appId, lines ?? 200, resolved.id)
14
- const logs = Array.isArray(response.logs) ? response.logs : []
15
- const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
16
- return { device: deviceInfo, logs, crashLines }
17
- } else {
18
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
19
- const deviceInfo = resolved
20
- try {
21
- const iosObs = new iOSObserve()
22
- const resp = await iosObs.getLogs(appId, resolved.id)
23
- const logs = Array.isArray(resp.logs) ? resp.logs : []
24
- const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'))
25
- return { device: deviceInfo, logs, crashLines }
26
- } catch (e) {
27
- return { device: deviceInfo, logs: [], crashLines: [] }
28
- }
29
- }
30
- }
31
-
32
- export async function startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }: { platform?: 'android' | 'ios', packageName: string, level?: 'error' | 'warn' | 'info' | 'debug', sessionId?: string, deviceId?: string }) {
33
- const effectivePlatform = platform || 'android'
34
- const sid = sessionId || 'default'
35
- if (effectivePlatform === 'android') {
36
- const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId })
37
- return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid)
38
- } else {
39
- const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId })
40
- return await startIOSLogStream(packageName, level || 'error', resolved.id, sid)
41
- }
42
- }
43
-
44
- export async function readLogStreamHandler({ platform, sessionId, limit, since }: { platform?: 'android' | 'ios', sessionId?: string, limit?: number, since?: string }) {
45
- const effectivePlatform = platform || 'android'
46
- const sid = sessionId || 'default'
47
- if (effectivePlatform === 'android') {
48
- return await readLogStreamLines(sid, limit ?? 100, since)
49
- } else {
50
- return await readIOSLogStreamLines(sid, limit ?? 100, since)
51
- }
52
- }
53
-
54
- export async function stopLogStreamHandler({ platform, sessionId }: { platform?: 'android' | 'ios', sessionId?: string }) {
55
- const effectivePlatform = platform || 'android'
56
- const sid = sessionId || 'default'
57
- if (effectivePlatform === 'android') {
58
- return await stopAndroidLogStream(sid)
59
- } else {
60
- return await stopIOSLogStream(sid)
61
- }
62
- }
@@ -1,18 +0,0 @@
1
- import { resolveTargetDevice } from '../resolve-device.js'
2
- import { AndroidObserve } from '../android/observe.js'
3
- import { iOSObserve } from '../ios/observe.js'
4
-
5
- const androidObserve = new AndroidObserve()
6
- const iosObserve = new iOSObserve()
7
-
8
- export async function captureScreenshotHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
9
- if (platform === 'android') {
10
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
11
- const result = await androidObserve.captureScreen(resolved.id)
12
- return { device: resolved, resolution: result.resolution, screenshot: result.screenshot }
13
- } else {
14
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
15
- const result = await iosObserve.captureScreenshot(resolved.id)
16
- return { device: resolved, resolution: result.resolution, screenshot: result.screenshot }
17
- }
18
- }
package/src/tools/ui.ts DELETED
@@ -1,62 +0,0 @@
1
- import { resolveTargetDevice } from '../resolve-device.js'
2
- import { AndroidObserve } from '../android/observe.js'
3
- import { iOSObserve } from '../ios/observe.js'
4
- import { AndroidInteract } from '../android/interact.js'
5
- import { iOSInteract } from '../ios/interact.js'
6
-
7
- const androidObserve = new AndroidObserve()
8
- const iosObserve = new iOSObserve()
9
- const androidInteract = new AndroidInteract()
10
- const iosInteract = new iOSInteract()
11
-
12
- export async function getUITreeHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
13
- if (platform === 'android') {
14
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
15
- return await androidObserve.getUITree(resolved.id)
16
- } else {
17
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
18
- return await iosObserve.getUITree(resolved.id)
19
- }
20
- }
21
-
22
- export async function getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
23
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
24
- return await androidObserve.getCurrentScreen(resolved.id)
25
- }
26
-
27
- export async function waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
28
- const effectiveTimeout = timeout ?? 10000
29
- if (platform === 'android') {
30
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
31
- return await androidInteract.waitForElement(text, effectiveTimeout, resolved.id)
32
- } else {
33
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
34
- return await iosInteract.waitForElement(text, effectiveTimeout, resolved.id)
35
- }
36
- }
37
-
38
- export async function tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
39
- const effectivePlatform = platform || 'android'
40
- if (effectivePlatform === 'android') {
41
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
42
- return await androidInteract.tap(x, y, resolved.id)
43
- } else {
44
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
45
- return await iosInteract.tap(x, y, resolved.id)
46
- }
47
- }
48
-
49
- export async function swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
50
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
51
- return await androidInteract.swipe(x1, y1, x2, y2, duration, resolved.id)
52
- }
53
-
54
- export async function typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
55
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
56
- return await androidInteract.typeText(text, resolved.id)
57
- }
58
-
59
- export async function pressBackHandler({ deviceId }: { deviceId?: string }) {
60
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
61
- return await androidInteract.pressBack(resolved.id)
62
- }