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.
- package/README.md +20 -5
- package/dist/android/diagnostics.js +24 -0
- package/dist/android/interact.js +1 -145
- package/dist/android/manage.js +162 -0
- package/dist/android/observe.js +133 -88
- package/dist/android/run.js +187 -0
- package/dist/android/utils.js +137 -147
- package/dist/ios/interact.js +4 -175
- package/dist/ios/manage.js +169 -0
- package/dist/ios/observe.js +129 -13
- package/dist/ios/run.js +200 -0
- package/dist/ios/utils.js +138 -124
- package/dist/server.js +45 -17
- package/dist/tools/interact.js +21 -71
- package/dist/tools/manage.js +180 -0
- package/dist/tools/observe.js +23 -69
- package/dist/tools/run.js +180 -0
- package/dist/utils/diagnostics.js +25 -0
- package/docs/CHANGELOG.md +14 -0
- package/eslint.config.js +22 -1
- package/package.json +8 -5
- package/scripts/check-idb.js +83 -0
- package/scripts/check-idb.ts +73 -0
- package/scripts/idb-helper.ts +76 -0
- package/scripts/install-idb.js +88 -0
- package/scripts/install-idb.ts +90 -0
- package/scripts/run-ios-smoke.ts +34 -0
- package/scripts/run-ios-ui-tree-tap.ts +33 -0
- package/src/android/diagnostics.ts +23 -0
- package/src/android/interact.ts +2 -155
- package/src/android/manage.ts +157 -0
- package/src/android/observe.ts +129 -97
- package/src/android/utils.ts +147 -149
- package/src/ios/interact.ts +5 -181
- package/src/ios/manage.ts +164 -0
- package/src/ios/observe.ts +130 -14
- package/src/ios/utils.ts +127 -128
- package/src/server.ts +47 -17
- package/src/tools/interact.ts +23 -62
- package/src/tools/manage.ts +171 -0
- package/src/tools/observe.ts +24 -74
- package/src/types.ts +9 -0
- package/src/utils/diagnostics.ts +36 -0
- package/test/device/README.md +49 -0
- package/test/device/index.ts +27 -0
- package/test/device/manage/run-build-install-ios.ts +82 -0
- package/test/{integration → device/manage}/run-install-android.ts +4 -4
- package/test/{integration → device/manage}/run-install-ios.ts +4 -4
- package/test/{integration → device/observe}/logstream-real.ts +5 -4
- package/test/{integration → device/utils}/test-dist.ts +2 -2
- package/test/unit/index.ts +10 -6
- package/test/unit/manage/build.test.ts +83 -0
- package/test/unit/manage/build_and_install.test.ts +134 -0
- package/test/unit/manage/diagnostics.test.ts +85 -0
- package/test/unit/{install.test.ts → manage/install.test.ts} +27 -18
- package/test/unit/{logparse.test.ts → observe/logparse.test.ts} +1 -1
- package/test/unit/{logstream.test.ts → observe/logstream.test.ts} +9 -10
- package/test/unit/{wait_for_element_mock.ts → observe/wait_for_element_mock.ts} +3 -3
- package/test/unit/{detect-java.test.ts → utils/detect-java.test.ts} +5 -5
- package/tsconfig.json +2 -1
- package/test/integration/index.ts +0 -8
- package/test/integration/test-dist.mjs +0 -41
- /package/test/{integration → device/interact}/run-real-test.ts +0 -0
- /package/test/{integration → device/interact}/smoke-test.ts +0 -0
- /package/test/{integration → device/manage}/install.integration.ts +0 -0
- /package/test/{integration → device/observe}/test-ui-tree.ts +0 -0
- /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 {
|
|
20
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
540
|
+
const res = await ToolsInteract.pressBackHandler({ deviceId })
|
|
511
541
|
return wrapResponse(res)
|
|
512
542
|
}
|
|
513
543
|
|
package/src/tools/interact.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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',
|
|
51
|
-
return await new iOSInteract().
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|
package/src/tools/observe.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { resolveTargetDevice
|
|
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
|
-
//
|
|
30
|
-
|
|
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
|
-
//
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
return await readLogStreamLines(sid, limit ?? 100, since)
|
|
55
|
+
return await new AndroidObserve().readLogStream(sid, limit ?? 100, since)
|
|
46
56
|
} else {
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|