mobile-debug-mcp 0.10.0 → 0.12.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 (67) hide show
  1. package/README.md +20 -5
  2. package/dist/android/diagnostics.js +24 -0
  3. package/dist/android/interact.js +1 -145
  4. package/dist/android/manage.js +162 -0
  5. package/dist/android/observe.js +133 -88
  6. package/dist/android/run.js +187 -0
  7. package/dist/android/utils.js +137 -147
  8. package/dist/ios/interact.js +4 -175
  9. package/dist/ios/manage.js +169 -0
  10. package/dist/ios/observe.js +129 -13
  11. package/dist/ios/run.js +200 -0
  12. package/dist/ios/utils.js +138 -124
  13. package/dist/server.js +45 -17
  14. package/dist/tools/interact.js +21 -71
  15. package/dist/tools/manage.js +180 -0
  16. package/dist/tools/observe.js +23 -69
  17. package/dist/tools/run.js +180 -0
  18. package/dist/utils/diagnostics.js +25 -0
  19. package/docs/CHANGELOG.md +14 -0
  20. package/eslint.config.js +22 -1
  21. package/package.json +8 -5
  22. package/scripts/check-idb.js +83 -0
  23. package/scripts/check-idb.ts +73 -0
  24. package/scripts/idb-helper.ts +76 -0
  25. package/scripts/install-idb.js +88 -0
  26. package/scripts/install-idb.ts +90 -0
  27. package/scripts/run-ios-smoke.ts +34 -0
  28. package/scripts/run-ios-ui-tree-tap.ts +33 -0
  29. package/src/android/diagnostics.ts +23 -0
  30. package/src/android/interact.ts +2 -155
  31. package/src/android/manage.ts +157 -0
  32. package/src/android/observe.ts +129 -97
  33. package/src/android/utils.ts +147 -149
  34. package/src/ios/interact.ts +5 -181
  35. package/src/ios/manage.ts +164 -0
  36. package/src/ios/observe.ts +130 -14
  37. package/src/ios/utils.ts +127 -128
  38. package/src/server.ts +47 -17
  39. package/src/tools/interact.ts +23 -62
  40. package/src/tools/manage.ts +171 -0
  41. package/src/tools/observe.ts +24 -74
  42. package/src/types.ts +9 -0
  43. package/src/utils/diagnostics.ts +36 -0
  44. package/test/device/README.md +49 -0
  45. package/test/device/index.ts +27 -0
  46. package/test/device/manage/run-build-install-ios.ts +82 -0
  47. package/test/{integration → device/manage}/run-install-android.ts +4 -4
  48. package/test/{integration → device/manage}/run-install-ios.ts +4 -4
  49. package/test/{integration → device/observe}/logstream-real.ts +5 -4
  50. package/test/{integration → device/utils}/test-dist.ts +2 -2
  51. package/test/unit/index.ts +10 -6
  52. package/test/unit/manage/build.test.ts +83 -0
  53. package/test/unit/manage/build_and_install.test.ts +134 -0
  54. package/test/unit/manage/diagnostics.test.ts +85 -0
  55. package/test/unit/{install.test.ts → manage/install.test.ts} +27 -18
  56. package/test/unit/{logparse.test.ts → observe/logparse.test.ts} +1 -1
  57. package/test/unit/{logstream.test.ts → observe/logstream.test.ts} +9 -10
  58. package/test/unit/{wait_for_element_mock.ts → observe/wait_for_element_mock.ts} +3 -3
  59. package/test/unit/{detect-java.test.ts → utils/detect-java.test.ts} +5 -5
  60. package/tsconfig.json +2 -1
  61. package/test/integration/index.ts +0 -8
  62. package/test/integration/test-dist.mjs +0 -41
  63. /package/test/{integration → device/interact}/run-real-test.ts +0 -0
  64. /package/test/{integration → device/interact}/smoke-test.ts +0 -0
  65. /package/test/{integration → device/manage}/install.integration.ts +0 -0
  66. /package/test/{integration → device/observe}/test-ui-tree.ts +0 -0
  67. /package/test/{integration → device/observe}/wait_for_element_real.ts +0 -0
package/src/server.ts CHANGED
@@ -14,12 +14,11 @@ import {
14
14
  InstallAppResponse
15
15
  } from "./types.js"
16
16
 
17
+ import { ToolsManage } from './tools/manage.js'
17
18
  import { ToolsInteract } from './tools/interact.js'
18
19
  import { ToolsObserve } from './tools/observe.js'
19
- import { AndroidInteract } from './android/interact.js'
20
- import { iOSInteract } from './ios/interact.js'
21
- import { AndroidObserve } from './android/observe.js'
22
- import { iOSObserve } from './ios/observe.js'
20
+ import { AndroidManage } from './android/manage.js'
21
+ import { iOSManage } from './ios/manage.js'
23
22
 
24
23
 
25
24
  const server = new Server(
@@ -146,6 +145,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
146
145
  required: ["appPath"]
147
146
  }
148
147
  },
148
+ {
149
+ name: "build_app",
150
+ description: "Build a project for Android or iOS and return the built artifact path. Does not install.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ platform: { type: "string", enum: ["android", "ios"], description: "Optional. If omitted the server will attempt to detect platform from projectPath files." },
155
+ projectPath: { type: "string", description: "Path to project directory (contains gradlew or xcodeproj/xcworkspace)" },
156
+ variant: { type: "string", description: "Optional build variant (e.g., Debug/Release)" }
157
+ },
158
+ required: ["projectPath"]
159
+ }
160
+ },
149
161
  {
150
162
  name: "get_logs",
151
163
  description: "Get recent logs from Android or iOS simulator. Returns device metadata and the log output.",
@@ -397,7 +409,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
397
409
  try {
398
410
  if (name === "start_app") {
399
411
  const { platform, appId, deviceId } = args as any
400
- const res = await (platform === 'android' ? new AndroidInteract().startApp(appId, deviceId) : new iOSInteract().startApp(appId, deviceId))
412
+ const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
401
413
  const response: StartAppResponse = {
402
414
  device: res.device,
403
415
  appStarted: res.appStarted,
@@ -408,28 +420,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
408
420
 
409
421
  if (name === "terminate_app") {
410
422
  const { platform, appId, deviceId } = args as any
411
- const res = await (platform === 'android' ? new AndroidInteract().terminateApp(appId, deviceId) : new iOSInteract().terminateApp(appId, deviceId))
423
+ const res = await (platform === 'android' ? new AndroidManage().terminateApp(appId, deviceId) : new iOSManage().terminateApp(appId, deviceId))
412
424
  const response: TerminateAppResponse = { device: res.device, appTerminated: res.appTerminated }
413
425
  return wrapResponse(response)
414
426
  }
415
427
 
416
428
  if (name === "restart_app") {
417
429
  const { platform, appId, deviceId } = args as any
418
- const res = await (platform === 'android' ? new AndroidInteract().restartApp(appId, deviceId) : new iOSInteract().restartApp(appId, deviceId))
430
+ const res = await (platform === 'android' ? new AndroidManage().restartApp(appId, deviceId) : new iOSManage().restartApp(appId, deviceId))
419
431
  const response: RestartAppResponse = { device: res.device, appRestarted: res.appRestarted, launchTimeMs: res.launchTimeMs }
420
432
  return wrapResponse(response)
421
433
  }
422
434
 
423
435
  if (name === "reset_app_data") {
424
436
  const { platform, appId, deviceId } = args as any
425
- const res = await (platform === 'android' ? new AndroidInteract().resetAppData(appId, deviceId) : new iOSInteract().resetAppData(appId, deviceId))
437
+ const res = await (platform === 'android' ? new AndroidManage().resetAppData(appId, deviceId) : new iOSManage().resetAppData(appId, deviceId))
426
438
  const response: ResetAppDataResponse = { device: res.device, dataCleared: res.dataCleared }
427
439
  return wrapResponse(response)
428
440
  }
429
441
 
430
442
  if (name === "install_app") {
431
443
  const { platform, appPath, deviceId } = args as any
432
- const res = await ToolsInteract.installAppHandler({ platform, appPath, deviceId })
444
+ const res = await ToolsManage.installAppHandler({ platform, appPath, deviceId })
433
445
  const response: InstallAppResponse = {
434
446
  device: res.device,
435
447
  installed: res.installed,
@@ -439,6 +451,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
439
451
  return wrapResponse(response)
440
452
  }
441
453
 
454
+ if (name === "build_app") {
455
+ const { platform, projectPath, variant } = args as any
456
+ const res = await ToolsManage.buildAppHandler({ platform, projectPath, variant })
457
+ return wrapResponse(res)
458
+ }
459
+
460
+ if (name === 'build_and_install') {
461
+ const { platform, projectPath, deviceId, timeout } = args as any
462
+ const res = await ToolsManage.buildAndInstallHandler({ platform, projectPath, deviceId, timeout })
463
+ // res: { ndjson, result }
464
+ return {
465
+ content: [
466
+ { type: 'text', text: res.ndjson },
467
+ { type: 'text', text: JSON.stringify(res.result, null, 2) }
468
+ ]
469
+ }
470
+ }
471
+
442
472
 
443
473
  if (name === "get_logs") {
444
474
  const { platform, appId, deviceId, lines } = args as any
@@ -453,7 +483,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
453
483
 
454
484
  if (name === "list_devices") {
455
485
  const { platform, appId } = (args || {}) as any
456
- const res = await ToolsObserve.listDevicesHandler({ platform, appId })
486
+ const res = await ToolsManage.listDevicesHandler({ platform, appId })
457
487
  return wrapResponse(res)
458
488
  }
459
489
 
@@ -471,43 +501,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
471
501
 
472
502
  if (name === "get_ui_tree") {
473
503
  const { platform, deviceId } = args as any
474
- const res = await (platform === 'android' ? new AndroidObserve().getUITree(deviceId) : new iOSObserve().getUITree(deviceId))
504
+ const res = await ToolsObserve.getUITreeHandler({ platform, deviceId })
475
505
  return wrapResponse(res)
476
506
  }
477
507
 
478
508
  if (name === "get_current_screen") {
479
509
  const { deviceId } = (args || {}) as any
480
- const res = await new AndroidObserve().getCurrentScreen(deviceId)
510
+ const res = await ToolsObserve.getCurrentScreenHandler({ deviceId })
481
511
  return wrapResponse(res)
482
512
  }
483
513
 
484
514
  if (name === "wait_for_element") {
485
515
  const { platform, text, timeout, deviceId } = (args || {}) as any
486
- const res = await (platform === 'android' ? new AndroidInteract().waitForElement(text, timeout, deviceId) : new iOSInteract().waitForElement(text, timeout, deviceId))
516
+ const res = await ToolsInteract.waitForElementHandler({ platform, text, timeout, deviceId })
487
517
  return wrapResponse(res)
488
518
  }
489
519
 
490
520
  if (name === "tap") {
491
521
  const { platform, x, y, deviceId } = (args || {}) as any
492
- const res = await (platform === 'android' ? new AndroidInteract().tap(x, y, deviceId) : new iOSInteract().tap(x, y, deviceId))
522
+ const res = await ToolsInteract.tapHandler({ platform, x, y, deviceId })
493
523
  return wrapResponse(res)
494
524
  }
495
525
 
496
526
  if (name === "swipe") {
497
527
  const { x1, y1, x2, y2, duration, deviceId } = (args || {}) as any
498
- const res = await new AndroidInteract().swipe(x1, y1, x2, y2, duration, deviceId)
528
+ const res = await ToolsInteract.swipeHandler({ x1, y1, x2, y2, duration, deviceId })
499
529
  return wrapResponse(res)
500
530
  }
501
531
 
502
532
  if (name === "type_text") {
503
533
  const { text, deviceId } = (args || {}) as any
504
- const res = await new AndroidInteract().typeText(text, deviceId)
534
+ const res = await ToolsInteract.typeTextHandler({ text, deviceId })
505
535
  return wrapResponse(res)
506
536
  }
507
537
 
508
538
  if (name === "press_back") {
509
539
  const { deviceId } = (args || {}) as any
510
- const res = await new AndroidInteract().pressBack(deviceId)
540
+ const res = await ToolsInteract.pressBackHandler({ deviceId })
511
541
  return wrapResponse(res)
512
542
  }
513
543
 
@@ -1,84 +1,45 @@
1
- import { promises as fs } from 'fs'
2
- import path from 'path'
3
1
  import { resolveTargetDevice } from '../resolve-device.js'
4
2
  import { AndroidInteract } from '../android/interact.js'
5
3
  import { iOSInteract } from '../ios/interact.js'
6
4
 
7
5
  export class ToolsInteract {
8
- static async installAppHandler({ platform, appPath, deviceId }: { platform?: 'android' | 'ios', appPath: string, deviceId?: string }) {
9
- let chosenPlatform: 'android' | 'ios' | undefined = platform
10
6
 
11
- try {
12
- const stat = await fs.stat(appPath).catch(() => null)
13
- if (stat && stat.isDirectory()) {
14
- const files = (await fs.readdir(appPath).catch(() => [])) as string[]
15
- if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
16
- chosenPlatform = 'ios'
17
- } 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)))) {
18
- chosenPlatform = 'android'
19
- } else {
20
- chosenPlatform = 'android'
21
- }
22
- } else if (typeof appPath === 'string') {
23
- const ext = path.extname(appPath).toLowerCase()
24
- if (ext === '.apk') chosenPlatform = 'android'
25
- else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
26
- else chosenPlatform = 'android'
27
- }
28
- } catch {
29
- chosenPlatform = 'android'
30
- }
31
-
32
- if (chosenPlatform === 'android') {
7
+ static async waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
8
+ const effectiveTimeout = timeout ?? 10000
9
+ if (platform === 'android') {
33
10
  const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
34
- const androidInteract = new AndroidInteract()
35
- const result = await androidInteract.installApp(appPath, resolved.id)
36
- return result
11
+ return await new AndroidInteract().waitForElement(text, effectiveTimeout, resolved.id)
37
12
  } else {
38
13
  const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
39
- const iosInteract = new iOSInteract()
40
- const result = await iosInteract.installApp(appPath, resolved.id)
41
- return result
14
+ return await new iOSInteract().waitForElement(text, effectiveTimeout, resolved.id)
42
15
  }
43
16
  }
44
17
 
45
- static async startAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
46
- if (platform === 'android') {
47
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
48
- return await new AndroidInteract().startApp(appId, resolved.id)
18
+ static async tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
19
+ const effectivePlatform = platform || 'android'
20
+ if (effectivePlatform === 'android') {
21
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
22
+ return await new AndroidInteract().tap(x, y, resolved.id)
49
23
  } else {
50
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
51
- return await new iOSInteract().startApp(appId, resolved.id)
24
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
25
+ return await new iOSInteract().tap(x, y, resolved.id)
52
26
  }
53
27
  }
54
28
 
55
- static async terminateAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
56
- if (platform === 'android') {
57
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
58
- return await new AndroidInteract().terminateApp(appId, resolved.id)
59
- } else {
60
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
61
- return await new iOSInteract().terminateApp(appId, resolved.id)
62
- }
29
+ static async swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
30
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
31
+ return await new AndroidInteract().swipe(x1, y1, x2, y2, duration, resolved.id)
63
32
  }
64
33
 
65
- static async restartAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
66
- if (platform === 'android') {
67
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
68
- return await new AndroidInteract().restartApp(appId, resolved.id)
69
- } else {
70
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
71
- return await new iOSInteract().restartApp(appId, resolved.id)
72
- }
34
+ static async typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
35
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
36
+ return await new AndroidInteract().typeText(text, resolved.id)
73
37
  }
74
38
 
75
- static async resetAppDataHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
76
- if (platform === 'android') {
77
- const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
78
- return await new AndroidInteract().resetAppData(appId, resolved.id)
79
- } else {
80
- const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
81
- return await new iOSInteract().resetAppData(appId, resolved.id)
82
- }
39
+ static async pressBackHandler({ deviceId }: { deviceId?: string }) {
40
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
41
+ return await new AndroidInteract().pressBack(resolved.id)
83
42
  }
43
+
84
44
  }
45
+
@@ -0,0 +1,171 @@
1
+ import { promises as fs } from 'fs'
2
+ import path from 'path'
3
+ import { resolveTargetDevice, listDevices } from '../resolve-device.js'
4
+ import { AndroidManage } from '../android/manage.js'
5
+ import { iOSManage } from '../ios/manage.js'
6
+ import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
7
+
8
+ export class ToolsManage {
9
+ static async buildAppHandler({ platform, projectPath, variant }: { platform?: 'android' | 'ios', projectPath: string, variant?: string }) {
10
+ // delegate to platform-specific build implementations
11
+ const chosen = platform || 'android'
12
+ if (chosen === 'android') {
13
+ const android = new AndroidManage()
14
+ const artifact = await (android as any).build(projectPath, variant)
15
+ return artifact
16
+ } else {
17
+ const ios = new iOSManage()
18
+ const artifact = await (ios as any).build(projectPath, variant)
19
+ return artifact
20
+ }
21
+ }
22
+
23
+ static async installAppHandler({ platform, appPath, deviceId }: { platform?: 'android' | 'ios', appPath: string, deviceId?: string }): Promise<InstallAppResponse> {
24
+ let chosenPlatform: 'android' | 'ios' | undefined = platform
25
+
26
+ try {
27
+ const stat = await fs.stat(appPath).catch(() => null)
28
+ if (stat && stat.isDirectory()) {
29
+ // If the directory itself looks like an .app bundle, treat as iOS
30
+ if (appPath.endsWith('.app')) {
31
+ chosenPlatform = 'ios'
32
+ } else {
33
+ const files = (await fs.readdir(appPath).catch(() => [])) as string[]
34
+ if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
35
+ chosenPlatform = 'ios'
36
+ } 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)))) {
37
+ chosenPlatform = 'android'
38
+ } else {
39
+ chosenPlatform = 'android'
40
+ }
41
+ }
42
+ } else if (typeof appPath === 'string') {
43
+ const ext = path.extname(appPath).toLowerCase()
44
+ if (ext === '.apk') chosenPlatform = 'android'
45
+ else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
46
+ else chosenPlatform = 'android'
47
+ }
48
+ } catch {
49
+ chosenPlatform = 'android'
50
+ }
51
+
52
+ if (chosenPlatform === 'android') {
53
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
54
+ const androidRun = new AndroidManage()
55
+ const result = await androidRun.installApp(appPath, resolved.id)
56
+ return result
57
+ } else {
58
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
59
+ const iosRun = new iOSManage()
60
+ const result = await iosRun.installApp(appPath, resolved.id)
61
+ return result
62
+ }
63
+ }
64
+
65
+ static async startAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }): Promise<StartAppResponse> {
66
+ if (platform === 'android') {
67
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
68
+ return await new AndroidManage().startApp(appId, resolved.id)
69
+ } else {
70
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
71
+ return await new iOSManage().startApp(appId, resolved.id)
72
+ }
73
+ }
74
+
75
+ static async terminateAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }): Promise<TerminateAppResponse> {
76
+ if (platform === 'android') {
77
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
78
+ return await new AndroidManage().terminateApp(appId, resolved.id)
79
+ } else {
80
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
81
+ return await new iOSManage().terminateApp(appId, resolved.id)
82
+ }
83
+ }
84
+
85
+ static async restartAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }): Promise<RestartAppResponse> {
86
+ if (platform === 'android') {
87
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
88
+ return await new AndroidManage().restartApp(appId, resolved.id)
89
+ } else {
90
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
91
+ return await new iOSManage().restartApp(appId, resolved.id)
92
+ }
93
+ }
94
+
95
+ static async resetAppDataHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }): Promise<ResetAppDataResponse> {
96
+ if (platform === 'android') {
97
+ const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
98
+ return await new AndroidManage().resetAppData(appId, resolved.id)
99
+ } else {
100
+ const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
101
+ return await new iOSManage().resetAppData(appId, resolved.id)
102
+ }
103
+ }
104
+
105
+ static async buildAndInstallHandler({ platform, projectPath, deviceId, timeout }: { platform?: 'android' | 'ios', projectPath: string, deviceId?: string, timeout?: number }) {
106
+ const events: string[] = []
107
+ const pushEvent = (obj: any) => events.push(JSON.stringify(obj))
108
+ const effectiveTimeout = timeout ?? 180000 // reserved for future streaming/timeouts
109
+ void effectiveTimeout
110
+
111
+ // determine platform if not provided by inspecting path
112
+ let chosenPlatform = platform
113
+ try {
114
+ const stat = await fs.stat(projectPath).catch(() => null)
115
+ if (!chosenPlatform) {
116
+ if (stat && stat.isDirectory()) {
117
+ const files = (await fs.readdir(projectPath).catch(() => [])) as string[]
118
+ if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) chosenPlatform = 'ios'
119
+ else chosenPlatform = 'android'
120
+ } else {
121
+ const ext = path.extname(projectPath).toLowerCase()
122
+ if (ext === '.apk') chosenPlatform = 'android'
123
+ else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
124
+ else chosenPlatform = 'android'
125
+ }
126
+ }
127
+ } catch {
128
+ chosenPlatform = chosenPlatform || 'android'
129
+ }
130
+
131
+ pushEvent({ type: 'build', status: 'started', platform: chosenPlatform })
132
+
133
+ let buildRes: any
134
+ try {
135
+ buildRes = await ToolsManage.buildAppHandler({ platform: chosenPlatform as any, projectPath })
136
+ if (buildRes && (buildRes as any).error) {
137
+ pushEvent({ type: 'build', status: 'failed', error: (buildRes as any).error })
138
+ return { ndjson: events.join('\n') + '\n', result: { success: false, error: (buildRes as any).error } }
139
+ }
140
+ pushEvent({ type: 'build', status: 'finished', artifactPath: (buildRes as any).artifactPath })
141
+ } catch (e) {
142
+ const msg = e instanceof Error ? e.message : String(e)
143
+ pushEvent({ type: 'build', status: 'failed', error: msg })
144
+ return { ndjson: events.join('\n') + '\n', result: { success: false, error: msg } }
145
+ }
146
+
147
+ // Install phase
148
+ const artifact = (buildRes as any).artifactPath || projectPath
149
+ pushEvent({ type: 'install', status: 'started', artifactPath: artifact, deviceId })
150
+ let installRes: any
151
+ try {
152
+ installRes = await ToolsManage.installAppHandler({ platform: chosenPlatform as any, appPath: artifact, deviceId })
153
+ if (installRes && installRes.installed === true) {
154
+ pushEvent({ type: 'install', status: 'finished', artifactPath: artifact, device: installRes.device })
155
+ return { ndjson: events.join('\n') + '\n', result: { success: true, artifactPath: artifact, device: installRes.device, output: installRes.output } }
156
+ } else {
157
+ pushEvent({ type: 'install', status: 'failed', error: installRes.error || 'unknown' })
158
+ return { ndjson: events.join('\n') + '\n', result: { success: false, error: installRes.error || 'install failed' } }
159
+ }
160
+ } catch (e) {
161
+ const msg = e instanceof Error ? e.message : String(e)
162
+ pushEvent({ type: 'install', status: 'failed', error: msg })
163
+ return { ndjson: events.join('\n') + '\n', result: { success: false, error: msg } }
164
+ }
165
+ }
166
+
167
+ static async listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
168
+ const devices = await listDevices(platform as any, appId)
169
+ return { devices }
170
+ }
171
+ }
@@ -1,10 +1,23 @@
1
- import { resolveTargetDevice, listDevices } from '../resolve-device.js'
1
+ import { resolveTargetDevice } from '../resolve-device.js'
2
2
  import { AndroidObserve } from '../android/observe.js'
3
3
  import { iOSObserve } from '../ios/observe.js'
4
- import { AndroidInteract } from '../android/interact.js'
5
- import { iOSInteract } from '../ios/interact.js'
6
4
 
7
5
  export class ToolsObserve {
6
+ static async getUITreeHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
7
+ if (platform === 'android') {
8
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
9
+ return await new AndroidObserve().getUITree(resolved.id)
10
+ } else {
11
+ const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
12
+ return await new iOSObserve().getUITree(resolved.id)
13
+ }
14
+ }
15
+
16
+ static async getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
17
+ const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
18
+ return await new AndroidObserve().getCurrentScreen(resolved.id)
19
+ }
20
+
8
21
  static async getLogsHandler({ platform, appId, deviceId, lines }: { platform: 'android' | 'ios', appId?: string, deviceId?: string, lines?: number }) {
9
22
  if (platform === 'android') {
10
23
  const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
@@ -26,14 +39,12 @@ export class ToolsObserve {
26
39
  const sid = sessionId || 'default'
27
40
  if (effectivePlatform === 'android') {
28
41
  const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId })
29
- // AndroidObserve uses utils for log stream control; delegate to android/utils functions where appropriate
30
- const { startAndroidLogStream } = await import('../android/utils.js')
31
- return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid)
42
+ // Delegate to AndroidObserve's log stream methods
43
+ return await new AndroidObserve().startLogStream(packageName, level || 'error', resolved.id, sid)
32
44
  } else {
33
45
  const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId })
34
- // iOSObserve implements startIOSLogStream via ios/utils; use its helper
35
- const { startIOSLogStream } = await import('../ios/utils.js')
36
- return await startIOSLogStream(packageName, resolved.id, sid)
46
+ // Delegate to iOSObserve for starting log streams
47
+ return await new iOSObserve().startLogStream(packageName, resolved.id, sid)
37
48
  }
38
49
  }
39
50
 
@@ -41,11 +52,9 @@ export class ToolsObserve {
41
52
  const effectivePlatform = platform || 'android'
42
53
  const sid = sessionId || 'default'
43
54
  if (effectivePlatform === 'android') {
44
- const { readLogStreamLines } = await import('../android/utils.js')
45
- return await readLogStreamLines(sid, limit ?? 100, since)
55
+ return await new AndroidObserve().readLogStream(sid, limit ?? 100, since)
46
56
  } else {
47
- const { readIOSLogStreamLines } = await import('../ios/utils.js')
48
- return await readIOSLogStreamLines(sid, limit ?? 100, since)
57
+ return await new iOSObserve().readLogStream(sid, limit ?? 100, since)
49
58
  }
50
59
  }
51
60
 
@@ -53,71 +62,12 @@ export class ToolsObserve {
53
62
  const effectivePlatform = platform || 'android'
54
63
  const sid = sessionId || 'default'
55
64
  if (effectivePlatform === 'android') {
56
- const { stopAndroidLogStream } = await import('../android/utils.js')
57
- return await stopAndroidLogStream(sid)
58
- } else {
59
- const { stopIOSLogStream } = await import('../ios/utils.js')
60
- return await stopIOSLogStream(sid)
61
- }
62
- }
63
-
64
- static async getUITreeHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
65
- if (platform === 'android') {
66
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
67
- return await new AndroidObserve().getUITree(resolved.id)
68
- } else {
69
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
70
- return await new iOSObserve().getUITree(resolved.id)
71
- }
72
- }
73
-
74
- static async getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
75
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
76
- return await new AndroidObserve().getCurrentScreen(resolved.id)
77
- }
78
-
79
- static async waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
80
- const effectiveTimeout = timeout ?? 10000
81
- if (platform === 'android') {
82
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
83
- return await new AndroidInteract().waitForElement(text, effectiveTimeout, resolved.id)
84
- } else {
85
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
86
- return await new iOSInteract().waitForElement(text, effectiveTimeout, resolved.id)
87
- }
88
- }
89
-
90
- static async tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
91
- const effectivePlatform = platform || 'android'
92
- if (effectivePlatform === 'android') {
93
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
94
- return await new AndroidInteract().tap(x, y, resolved.id)
65
+ return await new AndroidObserve().stopLogStream(sid)
95
66
  } else {
96
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
97
- return await new iOSInteract().tap(x, y, resolved.id)
67
+ return await new iOSObserve().stopLogStream(sid)
98
68
  }
99
69
  }
100
70
 
101
- static async swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
102
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
103
- return await new AndroidInteract().swipe(x1, y1, x2, y2, duration, resolved.id)
104
- }
105
-
106
- static async typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
107
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
108
- return await new AndroidInteract().typeText(text, resolved.id)
109
- }
110
-
111
- static async pressBackHandler({ deviceId }: { deviceId?: string }) {
112
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
113
- return await new AndroidInteract().pressBack(resolved.id)
114
- }
115
-
116
- static async listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
117
- const devices = await listDevices(platform as any, appId)
118
- return { devices }
119
- }
120
-
121
71
  static async captureScreenshotHandler({ platform, deviceId }: { platform?: 'android' | 'ios', deviceId?: string }) {
122
72
  const effectivePlatform = platform || 'android'
123
73
  if (effectivePlatform === 'android') {
package/src/types.ts CHANGED
@@ -10,22 +10,30 @@ export interface StartAppResponse {
10
10
  device: DeviceInfo;
11
11
  appStarted: boolean;
12
12
  launchTimeMs: number;
13
+ error?: string;
14
+ diagnostics?: any;
13
15
  }
14
16
 
15
17
  export interface TerminateAppResponse {
16
18
  device: DeviceInfo;
17
19
  appTerminated: boolean;
20
+ error?: string;
21
+ diagnostics?: any;
18
22
  }
19
23
 
20
24
  export interface RestartAppResponse {
21
25
  device: DeviceInfo;
22
26
  appRestarted: boolean;
23
27
  launchTimeMs: number;
28
+ error?: string;
29
+ diagnostics?: any;
24
30
  }
25
31
 
26
32
  export interface ResetAppDataResponse {
27
33
  device: DeviceInfo;
28
34
  dataCleared: boolean;
35
+ error?: string;
36
+ diagnostics?: any;
29
37
  }
30
38
 
31
39
  export interface GetLogsResponse {
@@ -133,4 +141,5 @@ export interface InstallAppResponse {
133
141
  installed: boolean;
134
142
  output?: string;
135
143
  error?: string;
144
+ diagnostics?: any;
136
145
  }
@@ -0,0 +1,36 @@
1
+ export type RunResult = {
2
+ exitCode: number | null
3
+ stdout: string
4
+ stderr: string
5
+ envSnapshot: Record<string,string | undefined>
6
+ command: string
7
+ args: string[]
8
+ suggestedFixes?: string[]
9
+ }
10
+
11
+ export function makeEnvSnapshot(keys: string[]) {
12
+ const snap: Record<string,string|undefined> = {}
13
+ for (const k of keys) snap[k] = process.env[k]
14
+ return snap
15
+ }
16
+
17
+ export function wrapExecResult(command: string, args: string[], res: { status: number | null, stdout?: string | Buffer, stderr?: string | Buffer }) : RunResult {
18
+ return {
19
+ exitCode: res.status,
20
+ stdout: res.stdout ? (typeof res.stdout === 'string' ? res.stdout : res.stdout.toString()) : '',
21
+ stderr: res.stderr ? (typeof res.stderr === 'string' ? res.stderr : res.stderr.toString()) : '',
22
+ envSnapshot: makeEnvSnapshot(['PATH','IDB_PATH','JAVA_HOME','HOME']),
23
+ command,
24
+ args,
25
+ suggestedFixes: []
26
+ }
27
+ }
28
+
29
+ export class DiagnosticError extends Error {
30
+ runResult: RunResult
31
+ constructor(message: string, runResult: RunResult) {
32
+ super(message)
33
+ this.name = 'DiagnosticError'
34
+ this.runResult = runResult
35
+ }
36
+ }