mobile-debug-mcp 0.20.0 → 0.21.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 +7 -5
- package/dist/interact/index.js +154 -118
- package/dist/manage/android.js +5 -2
- package/dist/scripts/capture_ui_after_tap.mjs +43 -0
- package/dist/scripts/check_play_observe.mjs +18 -0
- package/dist/scripts/check_play_substring.mjs +38 -0
- package/dist/scripts/dump_ui_tree.mjs +20 -0
- package/dist/scripts/observe-test.mjs +32 -0
- package/dist/scripts/press_and_observe.mjs +90 -0
- package/dist/scripts/press_and_wait_ui.mjs +85 -0
- package/dist/scripts/test_generate_and_wait.mjs +123 -0
- package/dist/server.js +18 -0
- package/dist/system/gradle.js +101 -0
- package/dist/system/index.js +8 -2
- package/dist/utils/android/utils.js +33 -1
- package/docs/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/interact/index.ts +96 -68
- package/src/manage/android.ts +5 -1
- package/src/server.ts +20 -0
- package/src/system/gradle.ts +98 -0
- package/src/system/index.ts +8 -2
- package/src/utils/android/utils.ts +27 -1
- package/test/observe/unit/observe_until_edge_cases.test.ts +41 -0
- package/test/observe/unit/observe_until_stability.test.ts +30 -0
- package/test/interact/device/observe_until_device.ts +0 -24
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const bundle = 'com.ideamechanics.modul8'
|
|
6
|
+
const sessionId = 'press-ui-test'
|
|
7
|
+
console.log('Starting log stream for', bundle)
|
|
8
|
+
try {
|
|
9
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
10
|
+
const pkg = bundle
|
|
11
|
+
const sessionOpts = { platform, packageName: pkg, sessionId }
|
|
12
|
+
const start = await ToolsObserve.startLogStreamHandler(sessionOpts)
|
|
13
|
+
console.log('startLogStream result:', JSON.stringify(start))
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('\nAttempting to find and tap Generate Session...')
|
|
19
|
+
let el = null
|
|
20
|
+
try {
|
|
21
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
22
|
+
const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: platform === 'android', timeoutMs: 3000, platform })
|
|
23
|
+
const foundFlag = found && typeof (found).found !== 'undefined' ? found.found : false
|
|
24
|
+
console.log('findElementHandler:', foundFlag ? 'found' : 'not found')
|
|
25
|
+
if (foundFlag) {
|
|
26
|
+
el = found.element
|
|
27
|
+
console.log('Matched element:', JSON.stringify(el))
|
|
28
|
+
}
|
|
29
|
+
} catch (e) { console.error('findElementHandler err:', e) }
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
33
|
+
const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
|
|
34
|
+
|
|
35
|
+
// Strict foreground check: ensure the package is in foreground before tapping
|
|
36
|
+
try {
|
|
37
|
+
const fg = await ToolsInteract.get_current_activity ? await ToolsInteract.get_current_activity({ platform, deviceId: device }) : null
|
|
38
|
+
console.log('Foreground activity info:', fg)
|
|
39
|
+
if (fg && fg.activity && !String(fg.activity).includes('modul8')) {
|
|
40
|
+
console.error('App is not foreground. Aborting tap.')
|
|
41
|
+
process.exit(2)
|
|
42
|
+
}
|
|
43
|
+
} catch (e) { console.warn('Foreground check failed:', e) }
|
|
44
|
+
|
|
45
|
+
if (el && el.tapCoordinates) {
|
|
46
|
+
console.log('Tapping matched element at', JSON.stringify(el.tapCoordinates))
|
|
47
|
+
await ToolsInteract.tapHandler({ platform, x: el.tapCoordinates.x, y: el.tapCoordinates.y, deviceId: device })
|
|
48
|
+
} else {
|
|
49
|
+
console.log('Element not found, tapping center fallback')
|
|
50
|
+
await ToolsInteract.tapHandler({ platform, x: 200, y: 400, deviceId: device })
|
|
51
|
+
}
|
|
52
|
+
} catch (e) { console.error('Tap failed:', e) }
|
|
53
|
+
|
|
54
|
+
console.log('\nNow waiting for "Generating session" to disappear (timeout 30s)')
|
|
55
|
+
const start = Date.now()
|
|
56
|
+
const deadline = start + (parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '30000'))
|
|
57
|
+
let poll = 0
|
|
58
|
+
let disappeared = false
|
|
59
|
+
const platformArg = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
60
|
+
const deviceArg = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
|
|
61
|
+
while (Date.now() <= deadline) {
|
|
62
|
+
poll++
|
|
63
|
+
try {
|
|
64
|
+
const f = await ToolsInteract.findElementHandler({ query: 'Generating session', exact: false, timeoutMs: 1000, platform: platformArg, deviceId: deviceArg })
|
|
65
|
+
const exists = f && typeof f.found !== 'undefined' ? f.found : false
|
|
66
|
+
console.log(`poll ${poll}: generating exists=${exists}`)
|
|
67
|
+
if (!exists) { disappeared = true; break }
|
|
68
|
+
} catch (e) { console.error('findElement err:', e) }
|
|
69
|
+
await new Promise(r => setTimeout(r, 500))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const elapsed = Date.now()-start
|
|
73
|
+
console.log('Result: disappeared=', disappeared, 'polls=', poll, 'elapsedMs=', elapsed)
|
|
74
|
+
|
|
75
|
+
// Platform-specific post-conditions
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
console.log('\nFinal Result: success=', disappeared)
|
|
79
|
+
|
|
80
|
+
console.log('\nStopping log stream')
|
|
81
|
+
try { const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId }); console.log('stopLogStream', stop) } catch (e) { console.error('stop failed', e) }
|
|
82
|
+
process.exit(disappeared ? 0 : 2)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch(e=>{ console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
4
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
5
|
+
|
|
6
|
+
const argv = process.argv.slice(2)
|
|
7
|
+
const opts = { platform: 'android', deviceId: undefined, timeoutMs: 180000, stabilityMs: 1000, pollIntervalMs: 500, saveDir: '/tmp' }
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
if (argv[i] === '--platform') opts.platform = argv[++i]
|
|
10
|
+
else if (argv[i] === '--deviceId') opts.deviceId = argv[++i]
|
|
11
|
+
else if (argv[i] === '--timeout') opts.timeoutMs = Number(argv[++i])
|
|
12
|
+
else if (argv[i] === '--stabilityMs') opts.stabilityMs = Number(argv[++i])
|
|
13
|
+
else if (argv[i] === '--poll') opts.pollIntervalMs = Number(argv[++i])
|
|
14
|
+
else if (argv[i] === '--saveDir') opts.saveDir = argv[++i]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const platform = opts.platform
|
|
18
|
+
const deviceId = opts.deviceId
|
|
19
|
+
const timeoutMs = opts.timeoutMs
|
|
20
|
+
const stabilityMs = opts.stabilityMs
|
|
21
|
+
const poll = opts.pollIntervalMs
|
|
22
|
+
const saveDir = opts.saveDir
|
|
23
|
+
|
|
24
|
+
const ts = () => Date.now()
|
|
25
|
+
|
|
26
|
+
async function findAndTapGenerate() {
|
|
27
|
+
try {
|
|
28
|
+
const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: false, timeoutMs: 3000, platform, deviceId })
|
|
29
|
+
const foundFlag = found && typeof found.found !== 'undefined' ? found.found : false
|
|
30
|
+
console.log('find Generate Session:', foundFlag)
|
|
31
|
+
if (foundFlag) {
|
|
32
|
+
const el = found.element
|
|
33
|
+
if (el && el.tapCoordinates) {
|
|
34
|
+
console.log('Tapping at', JSON.stringify(el.tapCoordinates))
|
|
35
|
+
await ToolsInteract.tapHandler({ platform, x: el.tapCoordinates.x, y: el.tapCoordinates.y, deviceId })
|
|
36
|
+
return true
|
|
37
|
+
} else {
|
|
38
|
+
// tap center of parent bounds if available
|
|
39
|
+
if (el && el.bounds) {
|
|
40
|
+
const b = el.bounds
|
|
41
|
+
const cx = Math.floor((b.left + b.right)/2)
|
|
42
|
+
const cy = Math.floor((b.top + b.bottom)/2)
|
|
43
|
+
console.log('Tapping parent center', cx, cy)
|
|
44
|
+
await ToolsInteract.tapHandler({ platform, x: cx, y: cy, deviceId })
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (e) { console.error('error finding/tapping generate:', e) }
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function captureUITree(savePrefix) {
|
|
54
|
+
try {
|
|
55
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId })
|
|
56
|
+
const p = path.join(saveDir, `${savePrefix}_ui_${Date.now()}.json`)
|
|
57
|
+
fs.writeFileSync(p, JSON.stringify(tree, null, 2))
|
|
58
|
+
console.log('Wrote UI tree', p)
|
|
59
|
+
return tree
|
|
60
|
+
} catch (e) { console.error('failed getUITree:', e); return null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function tryCaptureScreenshot(savePrefix) {
|
|
64
|
+
try {
|
|
65
|
+
const res = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
|
|
66
|
+
// handler may return { image: 'data:image/png;base64,...' } or blocks; try to extract base64
|
|
67
|
+
let b64 = null
|
|
68
|
+
if (res && res.image && typeof res.image === 'string') b64 = res.image.replace(/^data:image\/png;base64,/, '')
|
|
69
|
+
else if (Array.isArray(res) && res.length) {
|
|
70
|
+
for (const blk of res) { if (blk && blk.mime === 'image/png' && blk.data) { b64 = blk.data; break } }
|
|
71
|
+
} else if (res && res.blocks) {
|
|
72
|
+
for (const blk of res.blocks) { if (blk && blk.mime === 'image/png' && blk.data) { b64 = blk.data; break } }
|
|
73
|
+
}
|
|
74
|
+
if (b64) {
|
|
75
|
+
const p = path.join(saveDir, `${savePrefix}_shot_${Date.now()}.png`)
|
|
76
|
+
fs.writeFileSync(p, Buffer.from(b64, 'base64'))
|
|
77
|
+
console.log('Wrote screenshot', p)
|
|
78
|
+
return p
|
|
79
|
+
}
|
|
80
|
+
console.log('No image returned from captureScreenshotHandler')
|
|
81
|
+
} catch (e) { console.error('screenshot error:', e) }
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
console.log('Starting test: tap Generate Session and wait for Play session to appear stably')
|
|
87
|
+
const tapped = await findAndTapGenerate()
|
|
88
|
+
if (!tapped) console.log('Warning: generate tap not performed')
|
|
89
|
+
|
|
90
|
+
const start = ts()
|
|
91
|
+
const deadline = start + timeoutMs
|
|
92
|
+
let matchedAt = null
|
|
93
|
+
let pollCount = 0
|
|
94
|
+
|
|
95
|
+
while (Date.now() <= deadline) {
|
|
96
|
+
pollCount++
|
|
97
|
+
const savePrefix = `gen_wait_p${pollCount}`
|
|
98
|
+
const tree = await captureUITree(savePrefix)
|
|
99
|
+
try {
|
|
100
|
+
const f = await ToolsInteract.findElementHandler({ query: 'Play session', exact: false, timeoutMs: 500, platform, deviceId })
|
|
101
|
+
const exists = f && typeof f.found !== 'undefined' ? f.found : false
|
|
102
|
+
console.log(`poll ${pollCount}: play exists=${exists}`)
|
|
103
|
+
await tryCaptureScreenshot(savePrefix)
|
|
104
|
+
if (exists) {
|
|
105
|
+
if (matchedAt === null) matchedAt = Date.now()
|
|
106
|
+
const stable = Date.now() - matchedAt
|
|
107
|
+
if (stable >= stabilityMs) {
|
|
108
|
+
console.log('SUCCESS: Play session present and stable for', stable, 'ms after', Date.now()-start, 'ms and', pollCount, 'polls')
|
|
109
|
+
process.exit(0)
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
matchedAt = null
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { console.error('error during poll:', e) }
|
|
115
|
+
|
|
116
|
+
await new Promise(r => setTimeout(r, poll))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.error('FAIL: timeout waiting for Play session (polls=', pollCount, ')')
|
|
120
|
+
process.exit(2)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch(e=>{ console.error('fatal', e); process.exit(1) })
|
package/dist/server.js
CHANGED
|
@@ -314,6 +314,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
314
314
|
required: ["previousFingerprint"]
|
|
315
315
|
}
|
|
316
316
|
},
|
|
317
|
+
{
|
|
318
|
+
name: "observe_until",
|
|
319
|
+
description: "Wait for a UI condition (element present/absent) and require a stability window before returning success. Network-based waiting is not required; UI-only synchronization is the default and primary mode.",
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
properties: {
|
|
323
|
+
type: { type: "string", enum: ["ui", "log", "screen", "idle"], description: "Condition type to observe", default: "ui" },
|
|
324
|
+
query: { type: "string", description: "Optional query string for ui/log/screen types" },
|
|
325
|
+
timeoutMs: { type: "number", description: "Timeout in ms to wait for condition (default 30000)", default: 30000 },
|
|
326
|
+
pollIntervalMs: { type: "number", description: "Polling interval in ms (default 300, clamped to 250-500)", default: 300 },
|
|
327
|
+
match: { type: "string", enum: ["present", "absent"], description: "Match mode for UI checks: 'present' or 'absent' (default 'present')", default: "present" },
|
|
328
|
+
stability_ms: { type: "number", description: "Stability window in ms that the condition must hold before returning success (default 700)", default: 700 },
|
|
329
|
+
includeSnapshotOnFailure: { type: "boolean", description: "Whether to include a debug snapshot on timeout (default true)", default: true },
|
|
330
|
+
platform: { type: "string", enum: ["android", "ios"], description: "Optional platform override" },
|
|
331
|
+
deviceId: { type: "string", description: "Optional device serial/udid" }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
317
335
|
{
|
|
318
336
|
name: "wait_for_element",
|
|
319
337
|
description: "Wait until a UI element with matching text appears on screen or timeout is reached.",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
function readPropertiesFile(p) {
|
|
5
|
+
try {
|
|
6
|
+
const txt = readFileSync(p, { encoding: 'utf8' });
|
|
7
|
+
const lines = String(txt).split(/\r?\n/);
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const l of lines) {
|
|
10
|
+
const trimmed = l.trim();
|
|
11
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
12
|
+
continue;
|
|
13
|
+
const idx = trimmed.indexOf('=');
|
|
14
|
+
if (idx === -1)
|
|
15
|
+
continue;
|
|
16
|
+
const k = trimmed.substring(0, idx).trim();
|
|
17
|
+
const v = trimmed.substring(idx + 1).trim();
|
|
18
|
+
out[k] = v;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function javaBinExists(p) {
|
|
27
|
+
if (!p)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
const javaPath = path.join(p, 'bin', 'java');
|
|
31
|
+
if (existsSync(javaPath))
|
|
32
|
+
return true;
|
|
33
|
+
const alt = path.join(p, 'Contents', 'Home', 'bin', 'java');
|
|
34
|
+
if (existsSync(alt))
|
|
35
|
+
return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function checkGradle() {
|
|
43
|
+
const issues = [];
|
|
44
|
+
const filesChecked = [];
|
|
45
|
+
const suggestedFixes = [];
|
|
46
|
+
let gradleJavaHome;
|
|
47
|
+
// 1) explicit env
|
|
48
|
+
if (process.env.GRADLE_JAVA_HOME) {
|
|
49
|
+
gradleJavaHome = process.env.GRADLE_JAVA_HOME;
|
|
50
|
+
if (!javaBinExists(gradleJavaHome)) {
|
|
51
|
+
issues.push(`GRADLE_JAVA_HOME is set to '${gradleJavaHome}' but no java binary was found there`);
|
|
52
|
+
suggestedFixes.push('Unset GRADLE_JAVA_HOME or point it to a valid JDK (e.g., /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home)');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2) user gradle.properties
|
|
56
|
+
const gradleUserHome = process.env.GRADLE_USER_HOME || path.join(os.homedir(), '.gradle');
|
|
57
|
+
const userProps = path.join(gradleUserHome, 'gradle.properties');
|
|
58
|
+
filesChecked.push(userProps);
|
|
59
|
+
try {
|
|
60
|
+
const props = readPropertiesFile(userProps);
|
|
61
|
+
if (props['org.gradle.java.home']) {
|
|
62
|
+
const p = props['org.gradle.java.home'];
|
|
63
|
+
gradleJavaHome = gradleJavaHome || p;
|
|
64
|
+
if (!javaBinExists(p)) {
|
|
65
|
+
issues.push(`org.gradle.java.home in ${userProps} points to '${p}' which does not look like a valid JDK`);
|
|
66
|
+
suggestedFixes.push(`Edit ${userProps} to remove or correct org.gradle.java.home`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
// 3) system gradle.properties
|
|
72
|
+
const systemProps = '/etc/gradle/gradle.properties';
|
|
73
|
+
filesChecked.push(systemProps);
|
|
74
|
+
try {
|
|
75
|
+
const props = readPropertiesFile(systemProps);
|
|
76
|
+
if (props['org.gradle.java.home']) {
|
|
77
|
+
const p = props['org.gradle.java.home'];
|
|
78
|
+
gradleJavaHome = gradleJavaHome || p;
|
|
79
|
+
if (!javaBinExists(p)) {
|
|
80
|
+
issues.push(`org.gradle.java.home in ${systemProps} points to '${p}' which does not look like a valid JDK`);
|
|
81
|
+
suggestedFixes.push(`Edit ${systemProps} to remove or correct org.gradle.java.home`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
// 4) GRADLE_HOME fallback
|
|
87
|
+
if (!gradleJavaHome && process.env.GRADLE_HOME) {
|
|
88
|
+
filesChecked.push(process.env.GRADLE_HOME);
|
|
89
|
+
if (javaBinExists(process.env.GRADLE_HOME)) {
|
|
90
|
+
gradleJavaHome = process.env.GRADLE_HOME;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const gradleValid = !!gradleJavaHome && javaBinExists(gradleJavaHome);
|
|
94
|
+
if (!gradleJavaHome) {
|
|
95
|
+
// no explicit gradle java home detected — not an issue
|
|
96
|
+
}
|
|
97
|
+
else if (!gradleValid) {
|
|
98
|
+
issues.push(`Detected org.gradle.java.home = '${gradleJavaHome}' but it is invalid`);
|
|
99
|
+
}
|
|
100
|
+
return { gradleJavaHome, gradleValid, filesChecked, issues, suggestedFixes };
|
|
101
|
+
}
|
package/dist/system/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { checkAndroid } from './android.js';
|
|
2
2
|
import { checkIOS } from './ios.js';
|
|
3
|
+
import { checkGradle } from './gradle.js';
|
|
3
4
|
export async function getSystemStatus() {
|
|
4
5
|
try {
|
|
5
6
|
const android = await checkAndroid();
|
|
6
7
|
const ios = await checkIOS();
|
|
7
|
-
const
|
|
8
|
+
const gradle = await checkGradle();
|
|
9
|
+
const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
|
|
8
10
|
const success = issues.length === 0;
|
|
9
11
|
return {
|
|
10
12
|
success,
|
|
@@ -17,7 +19,11 @@ export async function getSystemStatus() {
|
|
|
17
19
|
issues,
|
|
18
20
|
appInstalled: android.appInstalled,
|
|
19
21
|
iosAvailable: ios.iosAvailable,
|
|
20
|
-
iosDevices: ios.iosDevices
|
|
22
|
+
iosDevices: ios.iosDevices,
|
|
23
|
+
gradleJavaHome: gradle.gradleJavaHome,
|
|
24
|
+
gradleValid: gradle.gradleValid,
|
|
25
|
+
gradleFilesChecked: gradle.filesChecked,
|
|
26
|
+
gradleSuggestedFixes: gradle.suggestedFixes
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
29
|
catch (e) {
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { detectJavaHome } from '../java.js';
|
|
4
4
|
import { execCmd } from '../exec.js';
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
|
+
import { checkGradle } from '../../system/gradle.js';
|
|
6
7
|
function findInPath(cmd) {
|
|
7
8
|
try {
|
|
8
9
|
// prefer command -v for POSIX
|
|
@@ -76,6 +77,14 @@ export async function prepareGradle(projectPath) {
|
|
|
76
77
|
gradleArgs.push('-Dorg.gradle.caching=false');
|
|
77
78
|
}
|
|
78
79
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
80
|
+
// Check for problematic org.gradle.java.home entries (env or properties) and avoid passing invalid values to Gradle
|
|
81
|
+
let gradleCheck;
|
|
82
|
+
try {
|
|
83
|
+
gradleCheck = await checkGradle();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] };
|
|
87
|
+
}
|
|
79
88
|
const env = Object.assign({}, process.env);
|
|
80
89
|
// Ensure child processes can find Android platform-tools (adb, etc.) by
|
|
81
90
|
// prepending the platform-tools directory to PATH for spawned processes.
|
|
@@ -90,6 +99,7 @@ export async function prepareGradle(projectPath) {
|
|
|
90
99
|
console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`);
|
|
91
100
|
}
|
|
92
101
|
const pathParts = [];
|
|
102
|
+
// Prefer a detected (validated) Java home from the system/IDE
|
|
93
103
|
if (detectedJavaHome) {
|
|
94
104
|
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
95
105
|
env.JAVA_HOME = detectedJavaHome;
|
|
@@ -100,6 +110,26 @@ export async function prepareGradle(projectPath) {
|
|
|
100
110
|
gradleArgs.push('--no-daemon');
|
|
101
111
|
env.GRADLE_JAVA_HOME = detectedJavaHome;
|
|
102
112
|
}
|
|
113
|
+
else if (gradleCheck && gradleCheck.gradleJavaHome) {
|
|
114
|
+
// There's an org.gradle.java.home configured somewhere (env or gradle.properties)
|
|
115
|
+
if (gradleCheck.gradleValid) {
|
|
116
|
+
const p = gradleCheck.gradleJavaHome;
|
|
117
|
+
const javaBin = path.join(p, 'bin');
|
|
118
|
+
if (!env.PATH || !env.PATH.includes(javaBin))
|
|
119
|
+
pathParts.push(javaBin);
|
|
120
|
+
gradleArgs.push(`-Dorg.gradle.java.home=${p}`);
|
|
121
|
+
gradleArgs.push('--no-daemon');
|
|
122
|
+
env.GRADLE_JAVA_HOME = p;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Invalid gradle java home detected: avoid passing it to Gradle and remove from spawn env
|
|
126
|
+
console.debug(`[prepareGradle] Invalid org.gradle.java.home detected (${gradleCheck.gradleJavaHome}); removing from spawn env to avoid Gradle error.`);
|
|
127
|
+
try {
|
|
128
|
+
delete env.GRADLE_JAVA_HOME;
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
103
133
|
if (platformToolsDir) {
|
|
104
134
|
// Prepend platform-tools so gradle and child tools find adb without modifying global env
|
|
105
135
|
if (!env.PATH || !env.PATH.includes(platformToolsDir)) {
|
|
@@ -138,10 +168,12 @@ export async function prepareGradle(projectPath) {
|
|
|
138
168
|
catch (e) {
|
|
139
169
|
console.debug('[prepareGradle] chmod failed for gradlew:', String(e));
|
|
140
170
|
}
|
|
171
|
+
// Execute the wrapper directly without a shell to avoid shell tokenization of args (spaces in paths)
|
|
141
172
|
spawnOpts.shell = false;
|
|
142
173
|
}
|
|
143
174
|
else {
|
|
144
|
-
|
|
175
|
+
// Prefer executing gradle directly without invoking a shell to preserve argument boundaries
|
|
176
|
+
spawnOpts.shell = false;
|
|
145
177
|
}
|
|
146
178
|
return { execCmd, gradleArgs, spawnOpts };
|
|
147
179
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.21.0]
|
|
6
|
+
- Added `observe_until` as a tool for agents to wait for things like API requests
|
|
7
|
+
|
|
8
|
+
## [0.20.1]
|
|
9
|
+
- Fixes gradle home issue for android
|
|
10
|
+
|
|
5
11
|
## [0.20.0]
|
|
6
12
|
- Added `get_system_status` tool and refactored system health checks into `src/system`.
|
|
7
13
|
- Provides a fast environment healthcheck (ADB availability/version, connected devices, log access, Android env vars, and basic iOS xcrun/simulator checks).
|