mobile-debug-mcp 0.19.0 → 0.19.2
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/dist/server.js +48 -0
- package/dist/utils/android/utils.js +110 -12
- package/dist/utils/cli/ios/preflight-ios.js +1 -1
- package/dist/utils/ios/utils.js +3 -2
- package/dist/utils/java.js +91 -34
- package/docs/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/skills/mcp-builder/SKILL.md +68 -0
- package/skills/mcp-builder/references/build-flags.md +43 -0
- package/skills/mcp-builder/references/diagnostics-schema.md +48 -0
- package/skills/mcp-builder/references/toolchain-details.md +62 -0
- package/src/manage/android.ts +3 -3
- package/src/manage/ios.ts +3 -3
- package/src/server.ts +47 -3
- package/src/utils/android/utils.ts +90 -19
- package/src/utils/cli/ios/preflight-ios.ts +2 -2
- package/src/utils/ios/utils.ts +3 -2
- package/src/utils/java.ts +76 -30
- package/test/utils/detect_java.test.ts +25 -0
package/dist/server.js
CHANGED
|
@@ -7,6 +7,10 @@ import { ToolsInteract } from './interact/index.js';
|
|
|
7
7
|
import { ToolsObserve } from './observe/index.js';
|
|
8
8
|
import { AndroidManage } from './manage/index.js';
|
|
9
9
|
import { iOSManage } from './manage/index.js';
|
|
10
|
+
import { ensureAdbAvailable } from './utils/android/utils.js';
|
|
11
|
+
import { getIdbCmd, isIDBInstalled } from './utils/ios/utils.js';
|
|
12
|
+
import { getXcrunCmd } from './utils/ios/utils.js';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
10
14
|
const server = new Server({
|
|
11
15
|
name: "mobile-debug-mcp",
|
|
12
16
|
version: "0.7.0"
|
|
@@ -15,6 +19,50 @@ const server = new Server({
|
|
|
15
19
|
tools: {}
|
|
16
20
|
}
|
|
17
21
|
});
|
|
22
|
+
// Startup healthchecks (non-fatal) — verify adb availability and log chosen command
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const adbCheck = ensureAdbAvailable();
|
|
26
|
+
if (adbCheck.ok)
|
|
27
|
+
console.debug('[startup] adb available:', adbCheck.adbCmd, adbCheck.version);
|
|
28
|
+
else
|
|
29
|
+
console.warn('[startup] adb not available or failed to run:', adbCheck.adbCmd, adbCheck.error);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e instanceof Error) {
|
|
33
|
+
console.warn('[startup] error during adb healthcheck:', e.message);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.warn('[startup] error during adb healthcheck:', String(e));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Check idb availability (non-fatal)
|
|
40
|
+
try {
|
|
41
|
+
const idbInstalled = await isIDBInstalled();
|
|
42
|
+
const idbCmd = getIdbCmd();
|
|
43
|
+
if (idbInstalled)
|
|
44
|
+
console.debug('[startup] idb available:', idbCmd);
|
|
45
|
+
else
|
|
46
|
+
console.debug('[startup] idb not available or failed to run:', idbCmd);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.warn('[startup] error during idb healthcheck:', e instanceof Error ? e.message : String(e));
|
|
50
|
+
}
|
|
51
|
+
// Check xcrun availability (non-fatal)
|
|
52
|
+
try {
|
|
53
|
+
const xcrun = getXcrunCmd();
|
|
54
|
+
try {
|
|
55
|
+
const out = execSync(`${xcrun} --version`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
56
|
+
console.debug('[startup] xcrun available:', xcrun, out.split('\n')[0]);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.warn('[startup] xcrun not available or failed to run:', xcrun, err instanceof Error ? err.message : String(err));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.warn('[startup] error during xcrun healthcheck:', e instanceof Error ? e.message : String(e));
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
18
66
|
function wrapResponse(data) {
|
|
19
67
|
return {
|
|
20
68
|
content: [{
|
|
@@ -2,7 +2,60 @@ import { promises as fsPromises, existsSync } from 'fs';
|
|
|
2
2
|
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
|
+
function findInPath(cmd) {
|
|
7
|
+
try {
|
|
8
|
+
// prefer command -v for POSIX
|
|
9
|
+
const res = spawnSync('command', ['-v', cmd], { encoding: 'utf8' });
|
|
10
|
+
if (res.status === 0 && res.stdout)
|
|
11
|
+
return res.stdout.trim();
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
console.debug(`[findInPath] command -v ${cmd} failed: ${String(e)}`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const res = spawnSync('which', [cmd], { encoding: 'utf8' });
|
|
18
|
+
if (res.status === 0 && res.stdout)
|
|
19
|
+
return res.stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.debug(`[findInPath] which ${cmd} failed: ${String(e)}`);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function resolveAdbCmd() {
|
|
27
|
+
// Priority: explicit env ADB_PATH -> ANDROID_SDK_ROOT/platform-tools/adb -> ANDROID_HOME/platform-tools/adb -> ~/Library/Android/sdk/platform-tools/adb -> PATH discovery -> 'adb'
|
|
28
|
+
if (process.env.ADB_PATH && process.env.ADB_PATH.trim())
|
|
29
|
+
return process.env.ADB_PATH;
|
|
30
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;
|
|
31
|
+
if (sdkRoot) {
|
|
32
|
+
const candidate = path.join(sdkRoot, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
33
|
+
if (existsSync(candidate))
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
// common macOS user SDK path
|
|
37
|
+
const homeSdk = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
38
|
+
if (existsSync(homeSdk))
|
|
39
|
+
return homeSdk;
|
|
40
|
+
const found = findInPath('adb');
|
|
41
|
+
if (found)
|
|
42
|
+
return found;
|
|
43
|
+
return 'adb';
|
|
44
|
+
}
|
|
45
|
+
export function getAdbCmd() { return resolveAdbCmd(); }
|
|
46
|
+
export function ensureAdbAvailable() {
|
|
47
|
+
const adb = resolveAdbCmd();
|
|
48
|
+
try {
|
|
49
|
+
const res = spawnSync(adb, ['--version'], { encoding: 'utf8' });
|
|
50
|
+
if (res.status === 0) {
|
|
51
|
+
return { adbCmd: adb, ok: true, version: (res.stdout || res.stderr || '').trim() };
|
|
52
|
+
}
|
|
53
|
+
return { adbCmd: adb, ok: false, error: (res.stderr || res.stdout || '').trim() };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return { adbCmd: adb, ok: false, error: String(err) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
6
59
|
/**
|
|
7
60
|
* Prepare Gradle execution options for building an Android project.
|
|
8
61
|
* Returns execCmd (wrapper or gradle), base gradleArgs array, and spawn options including env.
|
|
@@ -24,26 +77,67 @@ export async function prepareGradle(projectPath) {
|
|
|
24
77
|
}
|
|
25
78
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
26
79
|
const env = Object.assign({}, process.env);
|
|
80
|
+
// Ensure child processes can find Android platform-tools (adb, etc.) by
|
|
81
|
+
// prepending the platform-tools directory to PATH for spawned processes.
|
|
82
|
+
const adbPath = resolveAdbCmd();
|
|
83
|
+
let platformToolsDir = undefined;
|
|
84
|
+
try {
|
|
85
|
+
if (adbPath && adbPath !== 'adb' && existsSync(adbPath)) {
|
|
86
|
+
platformToolsDir = path.dirname(adbPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`);
|
|
91
|
+
}
|
|
92
|
+
const pathParts = [];
|
|
27
93
|
if (detectedJavaHome) {
|
|
28
94
|
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
29
95
|
env.JAVA_HOME = detectedJavaHome;
|
|
30
|
-
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
|
|
31
96
|
}
|
|
97
|
+
const javaBin = path.join(detectedJavaHome, 'bin');
|
|
98
|
+
pathParts.push(javaBin);
|
|
32
99
|
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
|
|
33
100
|
gradleArgs.push('--no-daemon');
|
|
34
101
|
env.GRADLE_JAVA_HOME = detectedJavaHome;
|
|
35
102
|
}
|
|
103
|
+
if (platformToolsDir) {
|
|
104
|
+
// Prepend platform-tools so gradle and child tools find adb without modifying global env
|
|
105
|
+
if (!env.PATH || !env.PATH.includes(platformToolsDir)) {
|
|
106
|
+
pathParts.push(platformToolsDir);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME) {
|
|
110
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || '';
|
|
111
|
+
const candidate = path.join(sdkRoot, 'platform-tools');
|
|
112
|
+
if (existsSync(candidate) && (!env.PATH || !env.PATH.includes(candidate))) {
|
|
113
|
+
pathParts.push(candidate);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// also try common user sdk location
|
|
118
|
+
const homeSdkTools = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools');
|
|
119
|
+
if (existsSync(homeSdkTools) && (!env.PATH || !env.PATH.includes(homeSdkTools))) {
|
|
120
|
+
pathParts.push(homeSdkTools);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (pathParts.length > 0) {
|
|
124
|
+
env.PATH = `${pathParts.join(path.delimiter)}${path.delimiter}${env.PATH || ''}`;
|
|
125
|
+
}
|
|
36
126
|
try {
|
|
37
127
|
delete env.SHELL;
|
|
38
128
|
}
|
|
39
|
-
catch {
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.debug('[prepareGradle] failed to delete SHELL from env:', String(e));
|
|
131
|
+
}
|
|
40
132
|
const useWrapper = existsSync(gradlewPath);
|
|
41
133
|
const spawnOpts = { cwd: projectPath, env };
|
|
42
134
|
if (useWrapper) {
|
|
43
135
|
try {
|
|
44
136
|
await fsPromises.chmod(gradlewPath, 0o755);
|
|
45
137
|
}
|
|
46
|
-
catch {
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.debug('[prepareGradle] chmod failed for gradlew:', String(e));
|
|
140
|
+
}
|
|
47
141
|
spawnOpts.shell = false;
|
|
48
142
|
}
|
|
49
143
|
else {
|
|
@@ -117,8 +211,8 @@ export async function getAndroidDeviceMetadata(appId, deviceId) {
|
|
|
117
211
|
resolvedDeviceId = deviceLines[0];
|
|
118
212
|
}
|
|
119
213
|
}
|
|
120
|
-
catch {
|
|
121
|
-
|
|
214
|
+
catch (e) {
|
|
215
|
+
console.debug('[getAndroidDeviceMetadata] error detecting single device: ' + String(e));
|
|
122
216
|
}
|
|
123
217
|
}
|
|
124
218
|
// Run these in parallel to avoid sequential timeouts
|
|
@@ -130,7 +224,8 @@ export async function getAndroidDeviceMetadata(appId, deviceId) {
|
|
|
130
224
|
const simulator = simOutput === '1';
|
|
131
225
|
return { platform: 'android', id: resolvedDeviceId || 'default', osVersion, model, simulator };
|
|
132
226
|
}
|
|
133
|
-
catch {
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.debug('[getAndroidDeviceMetadata] failed to gather metadata: ' + String(e));
|
|
134
229
|
return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false };
|
|
135
230
|
}
|
|
136
231
|
}
|
|
@@ -170,19 +265,22 @@ export async function listAndroidDevices(appId) {
|
|
|
170
265
|
const pm = await execAdb(['shell', 'pm', 'path', appId], serial);
|
|
171
266
|
appInstalled = !!(pm && pm.includes('package:'));
|
|
172
267
|
}
|
|
173
|
-
catch {
|
|
268
|
+
catch (e) {
|
|
269
|
+
console.debug(`[listAndroidDevices] pm check failed for ${serial}: ${String(e)}`);
|
|
174
270
|
appInstalled = false;
|
|
175
271
|
}
|
|
176
272
|
}
|
|
177
273
|
return { platform: 'android', id: serial, osVersion, model, simulator, appInstalled };
|
|
178
274
|
}
|
|
179
|
-
catch {
|
|
275
|
+
catch (e) {
|
|
276
|
+
console.debug(`[listAndroidDevices] failed gathering metadata for ${serial}: ${String(e)}`);
|
|
180
277
|
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false, appInstalled: false };
|
|
181
278
|
}
|
|
182
279
|
}));
|
|
183
280
|
return infos;
|
|
184
281
|
}
|
|
185
|
-
catch {
|
|
282
|
+
catch (e) {
|
|
283
|
+
console.debug('[listAndroidDevices] failed to list devices: ' + String(e));
|
|
186
284
|
return [];
|
|
187
285
|
}
|
|
188
286
|
}
|
|
@@ -207,8 +305,8 @@ export async function getScreenResolution(deviceId) {
|
|
|
207
305
|
return { width: parseInt(match[1]), height: parseInt(match[2]) };
|
|
208
306
|
}
|
|
209
307
|
}
|
|
210
|
-
catch {
|
|
211
|
-
|
|
308
|
+
catch (e) {
|
|
309
|
+
console.debug('[getScreenResolution] failed to detect screen resolution: ' + String(e));
|
|
212
310
|
}
|
|
213
311
|
return { width: 0, height: 0 };
|
|
214
312
|
}
|
|
@@ -48,7 +48,7 @@ function startCompanionIfNeeded(companionPath, udid) {
|
|
|
48
48
|
return { started: true };
|
|
49
49
|
}
|
|
50
50
|
catch (e) {
|
|
51
|
-
return { started: false, error: e.message };
|
|
51
|
+
return { started: false, error: e instanceof Error ? e.message : String(e) };
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
async function main() {
|
package/dist/utils/ios/utils.js
CHANGED
|
@@ -75,12 +75,13 @@ export async function isIDBInstalled() {
|
|
|
75
75
|
execSync(`command -v ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
-
catch {
|
|
78
|
+
catch (e) {
|
|
79
79
|
try {
|
|
80
80
|
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
-
catch {
|
|
83
|
+
catch (e2) {
|
|
84
|
+
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`);
|
|
84
85
|
return false;
|
|
85
86
|
}
|
|
86
87
|
}
|
package/dist/utils/java.js
CHANGED
|
@@ -1,76 +1,133 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
function isJavaVersionAcceptable(output) {
|
|
5
|
+
if (!output)
|
|
6
|
+
return false;
|
|
7
|
+
const s = String(output);
|
|
8
|
+
// Accept Java 17 or 21 (common supported LTS for Android builds)
|
|
9
|
+
if (/\b17\b/.test(s) || /17\./.test(s))
|
|
10
|
+
return true;
|
|
11
|
+
if (/\b21\b/.test(s) || /21\./.test(s))
|
|
12
|
+
return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
function javaVersionOf(javaBin) {
|
|
17
|
+
try {
|
|
18
|
+
const res = spawnSync(javaBin, ['-version'], { encoding: 'utf8' });
|
|
19
|
+
// Java prints version to stderr traditionally
|
|
20
|
+
const out = (res.stdout || '') + (res.stderr || '');
|
|
21
|
+
return out || undefined;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
console.debug('[javaVersionOf] java -version failed: ' + String(e));
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
export async function detectJavaHome() {
|
|
5
29
|
try {
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
console.debug('[java.detect] Existing JAVA_HOME does not appear to be Java 17, will search for JDK17');
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK17');
|
|
30
|
+
// 1) Honor explicit ANDROID_STUDIO_JDK env (highest priority)
|
|
31
|
+
const envStudio = process.env.ANDROID_STUDIO_JDK || process.env.ANDROID_STUDIO_JBR;
|
|
32
|
+
if (envStudio && existsSync(path.join(envStudio, 'bin', 'java'))) {
|
|
33
|
+
const v = javaVersionOf(path.join(envStudio, 'bin', 'java'));
|
|
34
|
+
if (isJavaVersionAcceptable(v)) {
|
|
35
|
+
console.debug('[java.detect] Using ANDROID_STUDIO_JDK from env:', envStudio);
|
|
36
|
+
return envStudio;
|
|
17
37
|
}
|
|
38
|
+
console.debug('[java.detect] ANDROID_STUDIO_JDK present but java -version did not match expected versions');
|
|
18
39
|
}
|
|
19
|
-
//
|
|
20
|
-
const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home';
|
|
21
|
-
if (existsSync(explicit))
|
|
22
|
-
return explicit;
|
|
23
|
-
// Android Studio JBR candidates
|
|
40
|
+
// 2) Android Studio JBR candidates (prefer these over JAVA_HOME)
|
|
24
41
|
const jbrCandidates = [
|
|
25
42
|
'/Applications/Android Studio.app/Contents/jbr',
|
|
43
|
+
'/Applications/Android Studio.app/Contents/jbr/Contents/Home',
|
|
26
44
|
'/Applications/Android Studio Preview.app/Contents/jbr',
|
|
45
|
+
'/Applications/Android Studio Preview.app/Contents/jbr/Contents/Home',
|
|
27
46
|
'/Applications/Android Studio Preview 2022.3.app/Contents/jbr',
|
|
28
|
-
'/Applications/Android Studio Preview
|
|
47
|
+
'/Applications/Android Studio Preview 2022.3.app/Contents/jbr/Contents/Home',
|
|
48
|
+
'/Applications/Android Studio Preview 2023.1.app/Contents/jbr',
|
|
49
|
+
'/Applications/Android Studio Preview 2023.1.app/Contents/jbr/Contents/Home'
|
|
29
50
|
];
|
|
30
51
|
for (const p of jbrCandidates) {
|
|
31
52
|
const javaBin = path.join(p, 'bin', 'java');
|
|
32
53
|
if (existsSync(javaBin)) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
const v = javaVersionOf(javaBin);
|
|
55
|
+
if (isJavaVersionAcceptable(v)) {
|
|
56
|
+
console.debug('[java.detect] Found Android Studio JBR at:', p);
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 3) If JAVA_HOME set, validate it (accept 17 or 21)
|
|
62
|
+
if (process.env.JAVA_HOME) {
|
|
63
|
+
try {
|
|
64
|
+
const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java');
|
|
65
|
+
const v = javaVersionOf(javaBin);
|
|
66
|
+
if (isJavaVersionAcceptable(v)) {
|
|
67
|
+
console.debug('[java.detect] Using JAVA_HOME from env:', process.env.JAVA_HOME);
|
|
68
|
+
return process.env.JAVA_HOME;
|
|
37
69
|
}
|
|
38
|
-
|
|
70
|
+
console.debug('[java.detect] Existing JAVA_HOME does not appear to be acceptable Java (17/21), will search');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK');
|
|
39
74
|
}
|
|
40
75
|
}
|
|
41
|
-
// macOS
|
|
76
|
+
// 4) macOS explicit path for JDK 17
|
|
77
|
+
const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home';
|
|
78
|
+
if (existsSync(explicit))
|
|
79
|
+
return explicit;
|
|
80
|
+
// 5) macOS /usr/libexec/java_home try supported versions
|
|
81
|
+
try {
|
|
82
|
+
const out17 = execSync('/usr/libexec/java_home -v 17', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
83
|
+
if (out17)
|
|
84
|
+
return out17;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.debug('[java.detect] /usr/libexec/java_home -v 17 failed: ' + String(e));
|
|
88
|
+
}
|
|
42
89
|
try {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
90
|
+
const out21 = execSync('/usr/libexec/java_home -v 21', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
91
|
+
if (out21)
|
|
92
|
+
return out21;
|
|
46
93
|
}
|
|
47
|
-
catch {
|
|
48
|
-
|
|
94
|
+
catch (e) {
|
|
95
|
+
console.debug('[java.detect] /usr/libexec/java_home -v 21 failed: ' + String(e));
|
|
96
|
+
}
|
|
97
|
+
// 6) macOS common JDK locations
|
|
49
98
|
try {
|
|
50
99
|
const homes = execSync('ls -1 /Library/Java/JavaVirtualMachines || true', { stdio: ['ignore', 'pipe', 'inherit'] }).toString().split(/\r?\n/).filter(Boolean);
|
|
51
100
|
for (const h of homes) {
|
|
52
|
-
if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17')) {
|
|
101
|
+
if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17') || h.toLowerCase().includes('21') || h.toLowerCase().includes('jdk-21')) {
|
|
53
102
|
const candidate = `/Library/Java/JavaVirtualMachines/${h}/Contents/Home`;
|
|
54
103
|
return candidate;
|
|
55
104
|
}
|
|
56
105
|
}
|
|
57
106
|
}
|
|
58
|
-
catch {
|
|
59
|
-
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.debug('[java.detect] listing /Library/Java/JavaVirtualMachines failed: ' + String(e));
|
|
109
|
+
}
|
|
110
|
+
// 7) Linux locations
|
|
60
111
|
const linuxCandidates = [
|
|
61
112
|
'/usr/lib/jvm/java-17-openjdk-amd64',
|
|
62
113
|
'/usr/lib/jvm/java-17-openjdk',
|
|
63
114
|
'/usr/lib/jvm/zulu17',
|
|
64
|
-
'/usr/lib/jvm/temurin-17-jdk'
|
|
115
|
+
'/usr/lib/jvm/temurin-17-jdk',
|
|
116
|
+
'/usr/lib/jvm/temurin-21-jdk',
|
|
117
|
+
'/usr/lib/jvm/java-21-openjdk-amd64'
|
|
65
118
|
];
|
|
66
119
|
for (const p of linuxCandidates) {
|
|
67
120
|
try {
|
|
68
121
|
if (existsSync(p))
|
|
69
122
|
return p;
|
|
70
123
|
}
|
|
71
|
-
catch {
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.debug(`[java.detect] checking linux candidate ${p} failed: ${String(e)}`);
|
|
126
|
+
}
|
|
72
127
|
}
|
|
73
128
|
}
|
|
74
|
-
catch {
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.debug('[java.detect] error detecting java home:', e instanceof Error ? e.message : String(e));
|
|
131
|
+
}
|
|
75
132
|
return undefined;
|
|
76
133
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.19.2]
|
|
6
|
+
- Added healthcheck improvments
|
|
7
|
+
- Added skills
|
|
8
|
+
|
|
9
|
+
## [0.19.1]
|
|
10
|
+
|
|
11
|
+
- Fixed Android install issues
|
|
12
|
+
|
|
5
13
|
## [0.19.0]
|
|
6
14
|
|
|
7
15
|
- Added `observe_until` interaction tool: waits for UI, log, screen fingerprint or idle conditions with configurable polling and timeout. Returns rich details on match (element info, log line, new fingerprint).
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# MCP Builder skill
|
|
2
|
+
|
|
3
|
+
name: mcp-builder
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
summary: Reusable procedures for building, validating and installing Android/iOS apps in this repo. Designed for agents to act autonomously with project-specific guidance.
|
|
6
|
+
|
|
7
|
+
# Purpose
|
|
8
|
+
Provide concise, actionable procedures and schemas that encode the successful sequences used in mobile-debug-mcp: toolchain detection, build orchestration, install fallbacks, diagnostics collection, and verification (lint/tests). Keep core guidance short; link to references for details.
|
|
9
|
+
|
|
10
|
+
# Activation conditions
|
|
11
|
+
Activate when an agent needs to:
|
|
12
|
+
- build or install an app from this repository
|
|
13
|
+
- diagnose failing CI/dev machine builds
|
|
14
|
+
- run lint/tests and collect reproducible diagnostics
|
|
15
|
+
|
|
16
|
+
# Surface area (actions)
|
|
17
|
+
- detect-toolchain
|
|
18
|
+
- build-android
|
|
19
|
+
- install-android
|
|
20
|
+
- build-ios
|
|
21
|
+
- install-ios
|
|
22
|
+
- run-lint
|
|
23
|
+
- run-tests
|
|
24
|
+
- collect-diagnostics
|
|
25
|
+
|
|
26
|
+
# Core guidance (what agent must do)
|
|
27
|
+
1. Prefer project conventions and existing helpers in src/utils and src/manage.
|
|
28
|
+
2. Use detect-toolchain to decide JAVA_HOME/JBR preference and whether adb/idb/xcrun are available.
|
|
29
|
+
3. For Android builds, call prepareGradle() to prepare child PATH and env, then run ./gradlew (wrapper) if present.
|
|
30
|
+
4. For installs, parse adb output defensively (ignore streamed-install noise) and on failure fall back to push+pm path.
|
|
31
|
+
5. For iOS installs, prefer simctl for simulators; if simctl fails check idb and attempt idb install with diagnostics.
|
|
32
|
+
6. On any error, call collect-diagnostics and attach env snapshot, invoked commands, stdout/stderr, and suggested fixes.
|
|
33
|
+
|
|
34
|
+
# Inputs & outputs (short schemas)
|
|
35
|
+
- detect-toolchain(input: { platform: 'android'|'ios'|'both', preferJBR?: boolean }) -> { tools: [{name, cmd, ok, version, suggestion}] }
|
|
36
|
+
- build-android(input: { projectPath, variant?, clean?, envOverrides? }) -> { success, artifactPath?, logs?, diagnostics? }
|
|
37
|
+
- install-android(input: { appPath, projectPath?, deviceId?, allowBuild? }) -> { installed:boolean, device, output?, diagnostics? }
|
|
38
|
+
- build-ios/install-ios: mirror Android schema but use scheme/workspace/project and resultBundlePath
|
|
39
|
+
- run-lint/run-tests -> { exitCode, stdout, stderr, artifacts[] }
|
|
40
|
+
- collect-diagnostics(input: { reason, platform? }) -> { artifacts: [{ name, contentBase64?, path? }], envSnapshot }
|
|
41
|
+
|
|
42
|
+
# Failure handling & suggestions
|
|
43
|
+
- Always return structured diagnostics instead of throwing when possible.
|
|
44
|
+
- Provide a short human-friendly suggestion in diagnostics (e.g., "Install Android Platform Tools or set ADB_PATH").
|
|
45
|
+
- Redact sensitive env values (tokens, credentials) when including envSnapshot.
|
|
46
|
+
|
|
47
|
+
# Progressive disclosure
|
|
48
|
+
- Keep SKILL.md compact (this file). Place heavy references in skills/mcp-builder/references/*.md and instruct agents to load them only when needed.
|
|
49
|
+
|
|
50
|
+
# References (implement as separate files)
|
|
51
|
+
- references/toolchain-details.md — exact paths/heuristics used for JBR, JAVA_HOME, ANDROID_SDK locations.
|
|
52
|
+
- references/build-flags.md — gradle/xcodebuild flags used, timeouts, and retry rationale.
|
|
53
|
+
- references/diagnostics-schema.md — JSON schema for runResult and collected artifacts.
|
|
54
|
+
|
|
55
|
+
# Implementation notes for maintainers
|
|
56
|
+
- Implement a thin adapter in src/skills/mcp-builder/index.ts that maps skill actions to existing functions in src/manage and src/utils.
|
|
57
|
+
- Provide unit tests under test/skills/mcp-builder that mock child_process to validate happy/failure paths.
|
|
58
|
+
- Add env toggles: MCP_PREFERS_JBR, MCP_GRADLE_RETRIES, MCP_XCODEBUILD_RETRIES.
|
|
59
|
+
|
|
60
|
+
# Example agent flow
|
|
61
|
+
1. call detect-toolchain({platform:'android', preferJBR:true})
|
|
62
|
+
2. if ok -> call build-android({projectPath:'/path', variant:'Debug'})
|
|
63
|
+
3. call install-android({appPath:'/path/to.apk', deviceId:'emulator-5554'})
|
|
64
|
+
4. if install fails -> call collect-diagnostics({reason:'install failure', platform:'android'})
|
|
65
|
+
|
|
66
|
+
# License
|
|
67
|
+
Same as repository (MIT).
|
|
68
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Build flags and runtime configuration
|
|
2
|
+
|
|
3
|
+
Purpose: describe the flags, environment variables and spawn-environment handling that agents should use when orchestrating Android and iOS builds for this repo.
|
|
4
|
+
|
|
5
|
+
Common env variables
|
|
6
|
+
- MCP_GRADLE_RETRIES (default: 1) — number of retry attempts on watchdog kills for Gradle
|
|
7
|
+
- MCP_GRADLE_TIMEOUT_MS (default: 300000) — gradle watch/duration timeout
|
|
8
|
+
- MCP_XCODEBUILD_RETRIES, MCP_XCODEBUILD_TIMEOUT_MS — similar for xcodebuild
|
|
9
|
+
- MCP_PREFERS_JBR — prefer Android Studio JBR for JAVA_HOME
|
|
10
|
+
- MCP_DERIVED_DATA, MCP_XCODE_RESULTBUNDLE_PATH — override DerivedData/result bundle locations
|
|
11
|
+
|
|
12
|
+
Android (Gradle)
|
|
13
|
+
- Use the Gradle wrapper if present: `./gradlew assemble<Variant>` (e.g., assembleDebug). Prefer wrapper to ensure consistent Gradle version.
|
|
14
|
+
- Prepare spawn env by prepending javaBin and platform-tools dir to PATH and set GRADLE_JAVA_HOME and `-Dorg.gradle.java.home` when necessary.
|
|
15
|
+
- If wrapper exists, ensure `chmod +x ./gradlew` before spawn.
|
|
16
|
+
- Recommended flags:
|
|
17
|
+
- `--no-daemon` sometimes helpful in CI, but wrap with existing project conventions.
|
|
18
|
+
- Set `org.gradle.jvmargs` via env or gradle.properties only if needed.
|
|
19
|
+
- Timeouts & watchdog:
|
|
20
|
+
- Use a watchdog timeout (MCP_GRADLE_TIMEOUT_MS) and retry (MCP_GRADLE_RETRIES) if killed by watchdog. Record stdout/stderr for each attempt.
|
|
21
|
+
|
|
22
|
+
iOS (xcodebuild)
|
|
23
|
+
- Use `xcodebuild -workspace <ws> -scheme <scheme> -configuration Debug -sdk iphonesimulator build` for simulator builds.
|
|
24
|
+
- Provide `-derivedDataPath` and `-resultBundlePath` to isolate builds and produce diagnostics. Default result bundle path should be unique per run to avoid collisions.
|
|
25
|
+
- Recommended flags: `-parallelizeTargets -jobs <N>` where N is from MCP_XCODE_JOBS or sensible default (4).
|
|
26
|
+
- When a destination UDID is available, always pass `-destination "platform=iOS Simulator,id=<UDID>"` to avoid ambiguous device selection.
|
|
27
|
+
- Timeouts & retries: respect MCP_XCODEBUILD_TIMEOUT_MS and MCP_XCODEBUILD_RETRIES; capture stdout/stderr and save logs under build-results.
|
|
28
|
+
|
|
29
|
+
Install-time behavior
|
|
30
|
+
- Android: prefer `adb install -r <apk>` for fast installs. If output contains spurious lines (e.g., "Performing Streamed Install" followed by "Success"), parse to extract final status. On failure try `adb push` to `/data/local/tmp` + `pm install -r <remote>` as a fallback collect push/install diagnostics.
|
|
31
|
+
- iOS: prefer `xcrun simctl install <device> <app>` for simulator. On failure attempt idb install if idb exists: `idb install <ipa|app> --udid <udid>` and record its stdout/stderr.
|
|
32
|
+
|
|
33
|
+
Diagnostics capture
|
|
34
|
+
- Save stdout/stderr, exitCode and environment snapshot for each command. Persist logs under workspace/build-results or a temp dir provided by the caller.
|
|
35
|
+
- For builds, also capture produced artifact paths to return to caller.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
- Gradle invocation (pseudo):
|
|
39
|
+
spawnOpts.env.PATH = `${javaBin}:${platformTools}:${process.env.PATH}`
|
|
40
|
+
spawn('./gradlew', ['assembleDebug'], { cwd: projectRoot, env: spawnOpts.env })
|
|
41
|
+
|
|
42
|
+
- xcodebuild invocation (pseudo):
|
|
43
|
+
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath /tmp/derived -resultBundlePath /tmp/Result-123.xcresult -parallelizeTargets -jobs 4 build
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Diagnostics schema
|
|
2
|
+
|
|
3
|
+
This document defines the JSON shapes that mcp-builder actions return when collecting diagnostics. Agents and tooling should rely on these keys.
|
|
4
|
+
|
|
5
|
+
Top-level diagnostic object
|
|
6
|
+
{
|
|
7
|
+
"error": "short human-friendly message",
|
|
8
|
+
"runResult": {
|
|
9
|
+
"command": "/usr/bin/adb",
|
|
10
|
+
"args": ["install", "app.apk"],
|
|
11
|
+
"exitCode": 1,
|
|
12
|
+
"stdout": "...",
|
|
13
|
+
"stderr": "...",
|
|
14
|
+
"startTimeMs": 1650000000000,
|
|
15
|
+
"endTimeMs": 1650000001000,
|
|
16
|
+
"envSnapshot": { "PATH": "...", "JAVA_HOME": "REDACTED" }
|
|
17
|
+
},
|
|
18
|
+
"artifacts": [ { name, path?, contentBase64?, type? } ],
|
|
19
|
+
"suggestedFixes": ["Install Android Platform Tools", "Set JAVA_HOME to JDK 17"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Fields
|
|
23
|
+
- error: short message summarising the failure.
|
|
24
|
+
- runResult: information captured from a single command invocation. Use this for programmatic retries or human debugging.
|
|
25
|
+
- command, args: the executed command
|
|
26
|
+
- exitCode: numeric exit code, null for killed/unknown
|
|
27
|
+
- stdout, stderr: captured text
|
|
28
|
+
- startTimeMs, endTimeMs: epoch ms for duration
|
|
29
|
+
- envSnapshot: a restricted set of env variables (PATH, JAVA_HOME, ADB_PATH, IDB_PATH, XCRUN_PATH). Any value matching sensitive patterns (e.g., /token|secret|key|passwd/i) must be redacted to "REDACTED".
|
|
30
|
+
- artifacts: array of captured files. Each artifact: { name: string, path?: string, contentBase64?: string, type?: "log"|"archive"|"image" }
|
|
31
|
+
- suggestedFixes: short actionable suggestions for human/operator.
|
|
32
|
+
|
|
33
|
+
Notes on size and transmission
|
|
34
|
+
- Prefer storing large artifacts on disk and returning paths rather than inlining large base64 blobs. If transmitting, limit base64 inlined artifacts to a configurable max (e.g., 5MB) and prefer compression/archiving.
|
|
35
|
+
|
|
36
|
+
Example: adb install failure
|
|
37
|
+
{
|
|
38
|
+
"error": "adb: device not found",
|
|
39
|
+
"runResult": {
|
|
40
|
+
"command": "adb",
|
|
41
|
+
"args": ["devices"],
|
|
42
|
+
"exitCode": 0,
|
|
43
|
+
"stdout": "List of devices attached\n\n",
|
|
44
|
+
"stderr": "",
|
|
45
|
+
"envSnapshot": { "PATH": "/usr/local/bin:/usr/bin", "JAVA_HOME": null }
|
|
46
|
+
},
|
|
47
|
+
"suggestedFixes": ["Connect an Android device or start an emulator", "Ensure adb is on PATH or set ADB_PATH"]
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Toolchain details
|
|
2
|
+
|
|
3
|
+
This reference documents the exact heuristics and commands used by the mcp-builder skill to detect and validate the native toolchain (Android & iOS) on contributor machines.
|
|
4
|
+
|
|
5
|
+
Key environment variables
|
|
6
|
+
- ADB_PATH: explicit path to adb binary
|
|
7
|
+
- ANDROID_SDK_ROOT / ANDROID_HOME: SDK root; platform-tools/adb under these
|
|
8
|
+
- ANDROID_STUDIO_JBR / ANDROID_STUDIO_JDK: explicit Android Studio JBR/JDK path
|
|
9
|
+
- JAVA_HOME: system JDK
|
|
10
|
+
- IDB_PATH / MCP_IDB_PATH: idb path for iOS
|
|
11
|
+
- XCRUN_PATH: explicit xcrun path
|
|
12
|
+
- MCP_PREFERS_JBR: boolean to prefer Android Studio JBR when present
|
|
13
|
+
|
|
14
|
+
Android Java detection precedence
|
|
15
|
+
1. ANDROID_STUDIO_JBR / ANDROID_STUDIO_JDK env vars (preferred when MCP_PREFERS_JBR set)
|
|
16
|
+
2. Known Android Studio JBR locations (macOS):
|
|
17
|
+
- /Applications/Android Studio.app/Contents/jbr/Contents/Home
|
|
18
|
+
- /Applications/Android Studio Preview.app/Contents/jbr/Contents/Home
|
|
19
|
+
3. Explicit JAVA_HOME (validate via `java -version`)
|
|
20
|
+
4. macOS `/usr/libexec/java_home -v 17` or `-v 21`
|
|
21
|
+
5. Common Linux JDK locations: `/usr/lib/jvm/*temurin*`, `/usr/lib/jvm/*zulu*`
|
|
22
|
+
|
|
23
|
+
Validation rules
|
|
24
|
+
- Accept only Java 17 or Java 21 for Gradle builds in this project (both tested/known working).
|
|
25
|
+
- Reject GraalVM / Java 23 that causes Gradle jlink errors. If detected, return suggestion: "Prefer an Apple/Temurin/JBR Java 17 or 21. Set ANDROID_STUDIO_JBR or JAVA_HOME to a supported JDK."
|
|
26
|
+
|
|
27
|
+
ADB resolution precedence
|
|
28
|
+
1. process.env.ADB_PATH (explicit)
|
|
29
|
+
2. ANDROID_SDK_ROOT or ANDROID_HOME -> platform-tools/adb
|
|
30
|
+
3. Common SDK locations (macOS/Linux): $HOME/Library/Android/sdk/platform-tools, /opt/android-sdk/platform-tools
|
|
31
|
+
4. `command -v adb` / `which adb` on PATH
|
|
32
|
+
5. fallback to `adb` (best-effort)
|
|
33
|
+
|
|
34
|
+
IDB / XCRUN detection (iOS)
|
|
35
|
+
- IDB: check MCP_IDB_PATH -> IDB_PATH -> common locations (/opt/homebrew/bin/idb, /usr/local/bin/idb, $HOME/Library/Python/*/bin/idb). Validate by running `idb --version` or `idb list-targets --json`.
|
|
36
|
+
- XCRUN: check XCRUN_PATH -> run `xcrun --version`.
|
|
37
|
+
|
|
38
|
+
How to probe (commands)
|
|
39
|
+
- adb: `adb --version` and `adb devices --print` (or `adb devices -l`) to list.
|
|
40
|
+
- java: `java -XshowSettings:properties -version` or `java -version` parse first line.
|
|
41
|
+
- xcrun: `xcrun --version`
|
|
42
|
+
- idb: `idb --version` or `idb list-targets --json`
|
|
43
|
+
|
|
44
|
+
Output format
|
|
45
|
+
- Each probe returns { name, cmd, ok, version?, suggestion? } so agents can reason programmatically.
|
|
46
|
+
- Include env snapshot (PATH, JAVA_HOME, ADB_PATH, ANDROID_SDK_ROOT, IDB_PATH, XCRUN_PATH) to aid diagnostics.
|
|
47
|
+
|
|
48
|
+
Notes & rationale
|
|
49
|
+
- Prefer Android Studio JBR (JBR is bundled & known-good) because developer machines often have mismatched system JDKs (e.g., Graal). Allow override via env for CI.
|
|
50
|
+
- For long-running server processes, prepend java/bin and platform-tools to spawn env PATH when running child builds so external PATH/JAVA_HOME changes don't require a server restart.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
- Detected result (JSON):
|
|
54
|
+
{
|
|
55
|
+
"name": "adb",
|
|
56
|
+
"cmd": "/Users/xxx/Library/Android/sdk/platform-tools/adb",
|
|
57
|
+
"ok": true,
|
|
58
|
+
"version": "Android Debug Bridge version 1.0.41"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- Java suggestion when incompatible:
|
|
62
|
+
{ "name": "java", "ok": false, "suggestion": "Found GraalVM (Java 23). Use Java 17 or 21: set ANDROID_STUDIO_JBR or JAVA_HOME." }
|
package/src/manage/android.ts
CHANGED
|
@@ -115,7 +115,7 @@ export class AndroidManage {
|
|
|
115
115
|
try {
|
|
116
116
|
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
117
117
|
return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 }
|
|
118
|
-
} catch (e:
|
|
118
|
+
} catch (e: unknown) {
|
|
119
119
|
const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
120
120
|
return { device: deviceInfo, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag }
|
|
121
121
|
}
|
|
@@ -127,7 +127,7 @@ export class AndroidManage {
|
|
|
127
127
|
try {
|
|
128
128
|
await execAdb(['shell', 'am', 'force-stop', appId], deviceId)
|
|
129
129
|
return { device: deviceInfo, appTerminated: true }
|
|
130
|
-
} catch (e:
|
|
130
|
+
} catch (e: unknown) {
|
|
131
131
|
const diag = execAdbWithDiagnostics(['shell', 'am', 'force-stop', appId], deviceId)
|
|
132
132
|
return { device: deviceInfo, appTerminated: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag }
|
|
133
133
|
}
|
|
@@ -149,7 +149,7 @@ export class AndroidManage {
|
|
|
149
149
|
try {
|
|
150
150
|
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId)
|
|
151
151
|
return { device: deviceInfo, dataCleared: output === 'Success' }
|
|
152
|
-
} catch (e:
|
|
152
|
+
} catch (e: unknown) {
|
|
153
153
|
const diag = execAdbWithDiagnostics(['shell', 'pm', 'clear', appId], deviceId)
|
|
154
154
|
return { device: deviceInfo, dataCleared: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag }
|
|
155
155
|
}
|
package/src/manage/ios.ts
CHANGED
|
@@ -302,7 +302,7 @@ export class iOSManage {
|
|
|
302
302
|
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
303
303
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
304
304
|
return { device, appStarted: !!result.output, launchTimeMs: 1000 }
|
|
305
|
-
} catch (e:
|
|
305
|
+
} catch (e: unknown) {
|
|
306
306
|
const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
307
307
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
308
308
|
return { device, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag } as any
|
|
@@ -315,7 +315,7 @@ export class iOSManage {
|
|
|
315
315
|
await execCommand(['simctl', 'terminate', deviceId, bundleId], deviceId)
|
|
316
316
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
317
317
|
return { device, appTerminated: true }
|
|
318
|
-
} catch (e:
|
|
318
|
+
} catch (e: unknown) {
|
|
319
319
|
const diag = execCommandWithDiagnostics(['simctl', 'terminate', deviceId, bundleId], deviceId)
|
|
320
320
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
321
321
|
return { device, appTerminated: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag } as any
|
|
@@ -351,7 +351,7 @@ export class iOSManage {
|
|
|
351
351
|
} catch (e) {
|
|
352
352
|
throw new Error(`Failed to clear data for ${bundleId}: ${e instanceof Error ? e.message : String(e)}`)
|
|
353
353
|
}
|
|
354
|
-
} catch (e:
|
|
354
|
+
} catch (e: unknown) {
|
|
355
355
|
const diag = execCommandWithDiagnostics(['simctl', 'get_app_container', deviceId, bundleId, 'data'], deviceId)
|
|
356
356
|
return { device, dataCleared: false, error: e instanceof Error ? e.message : String(e), diagnostics: diag } as any
|
|
357
357
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
4
|
+
import type { SchemaOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js"
|
|
4
5
|
import {
|
|
5
6
|
ListToolsRequestSchema,
|
|
6
7
|
CallToolRequestSchema
|
|
@@ -19,6 +20,10 @@ import { ToolsInteract } from './interact/index.js'
|
|
|
19
20
|
import { ToolsObserve } from './observe/index.js'
|
|
20
21
|
import { AndroidManage } from './manage/index.js'
|
|
21
22
|
import { iOSManage } from './manage/index.js'
|
|
23
|
+
import { ensureAdbAvailable } from './utils/android/utils.js'
|
|
24
|
+
import { getIdbCmd, isIDBInstalled } from './utils/ios/utils.js'
|
|
25
|
+
import { getXcrunCmd } from './utils/ios/utils.js'
|
|
26
|
+
import { execSync } from 'child_process'
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
const server = new Server(
|
|
@@ -31,7 +36,46 @@ const server = new Server(
|
|
|
31
36
|
tools: {}
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Startup healthchecks (non-fatal) — verify adb availability and log chosen command
|
|
42
|
+
(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const adbCheck = ensureAdbAvailable()
|
|
45
|
+
if (adbCheck.ok) console.debug('[startup] adb available:', adbCheck.adbCmd, adbCheck.version)
|
|
46
|
+
else console.warn('[startup] adb not available or failed to run:', adbCheck.adbCmd, adbCheck.error)
|
|
47
|
+
} catch (e: unknown) {
|
|
48
|
+
if (e instanceof Error) {
|
|
49
|
+
console.warn('[startup] error during adb healthcheck:', e.message)
|
|
50
|
+
} else {
|
|
51
|
+
console.warn('[startup] error during adb healthcheck:', String(e))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check idb availability (non-fatal)
|
|
56
|
+
try {
|
|
57
|
+
const idbInstalled = await isIDBInstalled()
|
|
58
|
+
const idbCmd = getIdbCmd()
|
|
59
|
+
if (idbInstalled) console.debug('[startup] idb available:', idbCmd)
|
|
60
|
+
else console.debug('[startup] idb not available or failed to run:', idbCmd)
|
|
61
|
+
} catch (e: unknown) {
|
|
62
|
+
console.warn('[startup] error during idb healthcheck:', e instanceof Error ? e.message : String(e))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check xcrun availability (non-fatal)
|
|
66
|
+
try {
|
|
67
|
+
const xcrun = getXcrunCmd()
|
|
68
|
+
try {
|
|
69
|
+
const out = execSync(`${xcrun} --version`, { stdio: ['ignore','pipe','ignore'] }).toString().trim()
|
|
70
|
+
console.debug('[startup] xcrun available:', xcrun, out.split('\n')[0])
|
|
71
|
+
} catch (err: unknown) {
|
|
72
|
+
console.warn('[startup] xcrun not available or failed to run:', xcrun, err instanceof Error ? err.message : String(err))
|
|
73
|
+
}
|
|
74
|
+
} catch (e: unknown) {
|
|
75
|
+
console.warn('[startup] error during xcrun healthcheck:', e instanceof Error ? e.message : String(e))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
})()
|
|
35
79
|
|
|
36
80
|
function wrapResponse<T>(data: T) {
|
|
37
81
|
return {
|
|
@@ -487,9 +531,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
487
531
|
}
|
|
488
532
|
}
|
|
489
533
|
]
|
|
490
|
-
}))
|
|
534
|
+
}));
|
|
491
535
|
|
|
492
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
536
|
+
server.setRequestHandler(CallToolRequestSchema, async (request: SchemaOutput<typeof CallToolRequestSchema>) => {
|
|
493
537
|
const { name, arguments: args } = request.params
|
|
494
538
|
|
|
495
539
|
try {
|
|
@@ -3,8 +3,51 @@ import { promises as fsPromises, existsSync } from 'fs'
|
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { detectJavaHome } from '../java.js'
|
|
5
5
|
import { execCmd } from '../exec.js'
|
|
6
|
+
import { spawnSync } from 'child_process'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
function findInPath(cmd: string): string | null {
|
|
9
|
+
try {
|
|
10
|
+
// prefer command -v for POSIX
|
|
11
|
+
const res = spawnSync('command', ['-v', cmd], { encoding: 'utf8' })
|
|
12
|
+
if (res.status === 0 && res.stdout) return res.stdout.trim()
|
|
13
|
+
} catch (e: unknown) { console.debug(`[findInPath] command -v ${cmd} failed: ${String(e)}`) }
|
|
14
|
+
try {
|
|
15
|
+
const res = spawnSync('which', [cmd], { encoding: 'utf8' })
|
|
16
|
+
if (res.status === 0 && res.stdout) return res.stdout.trim()
|
|
17
|
+
} catch (e: unknown) { console.debug(`[findInPath] which ${cmd} failed: ${String(e)}`) }
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveAdbCmd(): string {
|
|
22
|
+
// Priority: explicit env ADB_PATH -> ANDROID_SDK_ROOT/platform-tools/adb -> ANDROID_HOME/platform-tools/adb -> ~/Library/Android/sdk/platform-tools/adb -> PATH discovery -> 'adb'
|
|
23
|
+
if (process.env.ADB_PATH && process.env.ADB_PATH.trim()) return process.env.ADB_PATH
|
|
24
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME
|
|
25
|
+
if (sdkRoot) {
|
|
26
|
+
const candidate = path.join(sdkRoot, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
|
|
27
|
+
if (existsSync(candidate)) return candidate
|
|
28
|
+
}
|
|
29
|
+
// common macOS user SDK path
|
|
30
|
+
const homeSdk = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
|
|
31
|
+
if (existsSync(homeSdk)) return homeSdk
|
|
32
|
+
const found = findInPath('adb')
|
|
33
|
+
if (found) return found
|
|
34
|
+
return 'adb'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getAdbCmd() { return resolveAdbCmd() }
|
|
38
|
+
|
|
39
|
+
export function ensureAdbAvailable() {
|
|
40
|
+
const adb = resolveAdbCmd()
|
|
41
|
+
try {
|
|
42
|
+
const res = spawnSync(adb, ['--version'], { encoding: 'utf8' })
|
|
43
|
+
if (res.status === 0) {
|
|
44
|
+
return { adbCmd: adb, ok: true, version: (res.stdout || res.stderr || '').trim() }
|
|
45
|
+
}
|
|
46
|
+
return { adbCmd: adb, ok: false, error: (res.stderr || res.stdout || '').trim() }
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
return { adbCmd: adb, ok: false, error: String(err) }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
8
51
|
|
|
9
52
|
/**
|
|
10
53
|
* Prepare Gradle execution options for building an Android project.
|
|
@@ -31,22 +74,58 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
|
|
|
31
74
|
|
|
32
75
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined)
|
|
33
76
|
const env = Object.assign({}, process.env)
|
|
77
|
+
|
|
78
|
+
// Ensure child processes can find Android platform-tools (adb, etc.) by
|
|
79
|
+
// prepending the platform-tools directory to PATH for spawned processes.
|
|
80
|
+
const adbPath = resolveAdbCmd()
|
|
81
|
+
let platformToolsDir: string | undefined = undefined
|
|
82
|
+
try {
|
|
83
|
+
if (adbPath && adbPath !== 'adb' && existsSync(adbPath)) {
|
|
84
|
+
platformToolsDir = path.dirname(adbPath)
|
|
85
|
+
}
|
|
86
|
+
} catch (e: unknown) { console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`) }
|
|
87
|
+
|
|
88
|
+
const pathParts: string[] = []
|
|
34
89
|
if (detectedJavaHome) {
|
|
35
90
|
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
36
91
|
env.JAVA_HOME = detectedJavaHome
|
|
37
|
-
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
|
|
38
92
|
}
|
|
93
|
+
const javaBin = path.join(detectedJavaHome, 'bin')
|
|
94
|
+
pathParts.push(javaBin)
|
|
39
95
|
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
|
|
40
96
|
gradleArgs.push('--no-daemon')
|
|
41
97
|
env.GRADLE_JAVA_HOME = detectedJavaHome
|
|
42
98
|
}
|
|
43
99
|
|
|
44
|
-
|
|
100
|
+
if (platformToolsDir) {
|
|
101
|
+
// Prepend platform-tools so gradle and child tools find adb without modifying global env
|
|
102
|
+
if (!env.PATH || !env.PATH.includes(platformToolsDir)) {
|
|
103
|
+
pathParts.push(platformToolsDir)
|
|
104
|
+
}
|
|
105
|
+
} else if (process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME) {
|
|
106
|
+
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || ''
|
|
107
|
+
const candidate = path.join(sdkRoot, 'platform-tools')
|
|
108
|
+
if (existsSync(candidate) && (!env.PATH || !env.PATH.includes(candidate))) {
|
|
109
|
+
pathParts.push(candidate)
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// also try common user sdk location
|
|
113
|
+
const homeSdkTools = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools')
|
|
114
|
+
if (existsSync(homeSdkTools) && (!env.PATH || !env.PATH.includes(homeSdkTools))) {
|
|
115
|
+
pathParts.push(homeSdkTools)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (pathParts.length > 0) {
|
|
120
|
+
env.PATH = `${pathParts.join(path.delimiter)}${path.delimiter}${env.PATH || ''}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try { delete env.SHELL } catch (e: unknown) { console.debug('[prepareGradle] failed to delete SHELL from env:', String(e)) }
|
|
45
124
|
|
|
46
125
|
const useWrapper = existsSync(gradlewPath)
|
|
47
126
|
const spawnOpts: any = { cwd: projectPath, env }
|
|
48
127
|
if (useWrapper) {
|
|
49
|
-
try { await fsPromises.chmod(gradlewPath, 0o755) } catch {}
|
|
128
|
+
try { await fsPromises.chmod(gradlewPath, 0o755) } catch (e: unknown) { console.debug('[prepareGradle] chmod failed for gradlew:', String(e)) }
|
|
50
129
|
spawnOpts.shell = false
|
|
51
130
|
} else {
|
|
52
131
|
spawnOpts.shell = true
|
|
@@ -125,8 +204,7 @@ export async function getAndroidDeviceMetadata(appId: string, deviceId?: string)
|
|
|
125
204
|
if (deviceLines.length === 1) {
|
|
126
205
|
resolvedDeviceId = deviceLines[0];
|
|
127
206
|
}
|
|
128
|
-
} catch {
|
|
129
|
-
// ignore and continue without resolvedDeviceId
|
|
207
|
+
} catch (e: unknown) { console.debug('[getAndroidDeviceMetadata] error detecting single device: ' + String(e))
|
|
130
208
|
}
|
|
131
209
|
}
|
|
132
210
|
|
|
@@ -139,7 +217,8 @@ export async function getAndroidDeviceMetadata(appId: string, deviceId?: string)
|
|
|
139
217
|
|
|
140
218
|
const simulator = simOutput === '1'
|
|
141
219
|
return { platform: 'android', id: resolvedDeviceId || 'default', osVersion, model, simulator }
|
|
142
|
-
} catch {
|
|
220
|
+
} catch (e: unknown) {
|
|
221
|
+
console.debug('[getAndroidDeviceMetadata] failed to gather metadata: ' + String(e))
|
|
143
222
|
return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false }
|
|
144
223
|
}
|
|
145
224
|
}
|
|
@@ -180,20 +259,14 @@ export async function listAndroidDevices(appId?: string): Promise<DeviceInfo[]>
|
|
|
180
259
|
try {
|
|
181
260
|
const pm = await execAdb(['shell', 'pm', 'path', appId], serial)
|
|
182
261
|
appInstalled = !!(pm && pm.includes('package:'))
|
|
183
|
-
} catch {
|
|
184
|
-
appInstalled = false
|
|
185
|
-
}
|
|
262
|
+
} catch (e: unknown) { console.debug(`[listAndroidDevices] pm check failed for ${serial}: ${String(e)}`); appInstalled = false }
|
|
186
263
|
}
|
|
187
264
|
return { platform: 'android', id: serial, osVersion, model, simulator, appInstalled } as DeviceInfo & { appInstalled?: boolean }
|
|
188
|
-
} catch {
|
|
189
|
-
return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false, appInstalled: false } as DeviceInfo & { appInstalled?: boolean }
|
|
190
|
-
}
|
|
265
|
+
} catch (e: unknown) { console.debug(`[listAndroidDevices] failed gathering metadata for ${serial}: ${String(e)}`); return { platform: 'android', id: serial, osVersion: '', model: '', simulator: false, appInstalled: false } as DeviceInfo & { appInstalled?: boolean } }
|
|
191
266
|
}))
|
|
192
267
|
|
|
193
268
|
return infos
|
|
194
|
-
} catch {
|
|
195
|
-
return []
|
|
196
|
-
}
|
|
269
|
+
} catch (e: unknown) { console.debug('[listAndroidDevices] failed to list devices: ' + String(e)); return [] }
|
|
197
270
|
}
|
|
198
271
|
|
|
199
272
|
// UI helper utilities shared by observe/interact
|
|
@@ -219,9 +292,7 @@ export async function getScreenResolution(deviceId?: string): Promise<{ width: n
|
|
|
219
292
|
if (match) {
|
|
220
293
|
return { width: parseInt(match[1]), height: parseInt(match[2]) };
|
|
221
294
|
}
|
|
222
|
-
} catch {
|
|
223
|
-
// ignore
|
|
224
|
-
}
|
|
295
|
+
} catch (e: unknown) { console.debug('[getScreenResolution] failed to detect screen resolution: ' + String(e)) }
|
|
225
296
|
return { width: 0, height: 0 };
|
|
226
297
|
}
|
|
227
298
|
|
|
@@ -40,8 +40,8 @@ function startCompanionIfNeeded(companionPath: string | null, udid: string | nul
|
|
|
40
40
|
const child = spawn(companionPath, ['--udid', udid], { detached: true, stdio: 'ignore' })
|
|
41
41
|
child.unref()
|
|
42
42
|
return { started: true }
|
|
43
|
-
} catch (e:
|
|
44
|
-
return { started: false, error: e.message }
|
|
43
|
+
} catch (e: unknown) {
|
|
44
|
+
return { started: false, error: e instanceof Error ? e.message : String(e) }
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
package/src/utils/ios/utils.ts
CHANGED
|
@@ -62,11 +62,12 @@ export async function isIDBInstalled(): Promise<boolean> {
|
|
|
62
62
|
try {
|
|
63
63
|
execSync(`command -v ${cmd}`, { stdio: ['ignore','pipe','ignore'] })
|
|
64
64
|
return true
|
|
65
|
-
} catch {
|
|
65
|
+
} catch (e: unknown) {
|
|
66
66
|
try {
|
|
67
67
|
execSync(`${cmd} list-targets --json`, { stdio: ['ignore','pipe','ignore'], timeout: 2000 })
|
|
68
68
|
return true
|
|
69
|
-
} catch {
|
|
69
|
+
} catch (e2: unknown) {
|
|
70
|
+
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`)
|
|
70
71
|
return false
|
|
71
72
|
}
|
|
72
73
|
}
|
package/src/utils/java.ts
CHANGED
|
@@ -2,68 +2,114 @@ import { execSync } from 'child_process'
|
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
|
|
5
|
+
function isJavaVersionAcceptable(output?: string | null): boolean {
|
|
6
|
+
if (!output) return false
|
|
7
|
+
const s = String(output)
|
|
8
|
+
// Accept Java 17 or 21 (common supported LTS for Android builds)
|
|
9
|
+
if (/\b17\b/.test(s) || /17\./.test(s)) return true
|
|
10
|
+
if (/\b21\b/.test(s) || /21\./.test(s)) return true
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from 'child_process'
|
|
15
|
+
function javaVersionOf(javaBin: string): string | undefined {
|
|
16
|
+
try {
|
|
17
|
+
const res = spawnSync(javaBin, ['-version'], { encoding: 'utf8' })
|
|
18
|
+
// Java prints version to stderr traditionally
|
|
19
|
+
const out = (res.stdout || '') + (res.stderr || '')
|
|
20
|
+
return out || undefined
|
|
21
|
+
} catch (e: unknown) { console.debug('[javaVersionOf] java -version failed: ' + String(e)); return undefined }
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
export async function detectJavaHome(): Promise<string | undefined> {
|
|
6
25
|
try {
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} catch {
|
|
15
|
-
console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK17')
|
|
26
|
+
// 1) Honor explicit ANDROID_STUDIO_JDK env (highest priority)
|
|
27
|
+
const envStudio = process.env.ANDROID_STUDIO_JDK || process.env.ANDROID_STUDIO_JBR
|
|
28
|
+
if (envStudio && existsSync(path.join(envStudio, 'bin', 'java'))) {
|
|
29
|
+
const v = javaVersionOf(path.join(envStudio, 'bin', 'java'))
|
|
30
|
+
if (isJavaVersionAcceptable(v)) {
|
|
31
|
+
console.debug('[java.detect] Using ANDROID_STUDIO_JDK from env:', envStudio)
|
|
32
|
+
return envStudio
|
|
16
33
|
}
|
|
34
|
+
console.debug('[java.detect] ANDROID_STUDIO_JDK present but java -version did not match expected versions')
|
|
17
35
|
}
|
|
18
36
|
|
|
19
|
-
//
|
|
20
|
-
const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home'
|
|
21
|
-
if (existsSync(explicit)) return explicit
|
|
22
|
-
|
|
23
|
-
// Android Studio JBR candidates
|
|
37
|
+
// 2) Android Studio JBR candidates (prefer these over JAVA_HOME)
|
|
24
38
|
const jbrCandidates = [
|
|
25
39
|
'/Applications/Android Studio.app/Contents/jbr',
|
|
40
|
+
'/Applications/Android Studio.app/Contents/jbr/Contents/Home',
|
|
26
41
|
'/Applications/Android Studio Preview.app/Contents/jbr',
|
|
42
|
+
'/Applications/Android Studio Preview.app/Contents/jbr/Contents/Home',
|
|
27
43
|
'/Applications/Android Studio Preview 2022.3.app/Contents/jbr',
|
|
28
|
-
'/Applications/Android Studio Preview
|
|
44
|
+
'/Applications/Android Studio Preview 2022.3.app/Contents/jbr/Contents/Home',
|
|
45
|
+
'/Applications/Android Studio Preview 2023.1.app/Contents/jbr',
|
|
46
|
+
'/Applications/Android Studio Preview 2023.1.app/Contents/jbr/Contents/Home'
|
|
29
47
|
]
|
|
30
48
|
for (const p of jbrCandidates) {
|
|
31
49
|
const javaBin = path.join(p, 'bin', 'java')
|
|
32
50
|
if (existsSync(javaBin)) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
const v = javaVersionOf(javaBin)
|
|
52
|
+
if (isJavaVersionAcceptable(v)) {
|
|
53
|
+
console.debug('[java.detect] Found Android Studio JBR at:', p)
|
|
54
|
+
return p
|
|
55
|
+
}
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
58
|
|
|
40
|
-
//
|
|
59
|
+
// 3) If JAVA_HOME set, validate it (accept 17 or 21)
|
|
60
|
+
if (process.env.JAVA_HOME) {
|
|
61
|
+
try {
|
|
62
|
+
const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java')
|
|
63
|
+
const v = javaVersionOf(javaBin)
|
|
64
|
+
if (isJavaVersionAcceptable(v)) {
|
|
65
|
+
console.debug('[java.detect] Using JAVA_HOME from env:', process.env.JAVA_HOME)
|
|
66
|
+
return process.env.JAVA_HOME
|
|
67
|
+
}
|
|
68
|
+
console.debug('[java.detect] Existing JAVA_HOME does not appear to be acceptable Java (17/21), will search')
|
|
69
|
+
} catch {
|
|
70
|
+
console.debug('[java.detect] Failed to validate existing JAVA_HOME, searching for JDK')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4) macOS explicit path for JDK 17
|
|
75
|
+
const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home'
|
|
76
|
+
if (existsSync(explicit)) return explicit
|
|
77
|
+
|
|
78
|
+
// 5) macOS /usr/libexec/java_home try supported versions
|
|
79
|
+
try {
|
|
80
|
+
const out17 = execSync('/usr/libexec/java_home -v 17', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim()
|
|
81
|
+
if (out17) return out17
|
|
82
|
+
} catch (e: unknown) { console.debug('[java.detect] /usr/libexec/java_home -v 17 failed: ' + String(e)) }
|
|
41
83
|
try {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
} catch {}
|
|
84
|
+
const out21 = execSync('/usr/libexec/java_home -v 21', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim()
|
|
85
|
+
if (out21) return out21
|
|
86
|
+
} catch (e: unknown) { console.debug('[java.detect] /usr/libexec/java_home -v 21 failed: ' + String(e)) }
|
|
45
87
|
|
|
46
|
-
// macOS common JDK locations
|
|
88
|
+
// 6) macOS common JDK locations
|
|
47
89
|
try {
|
|
48
90
|
const homes = execSync('ls -1 /Library/Java/JavaVirtualMachines || true', { stdio: ['ignore', 'pipe', 'inherit'] }).toString().split(/\r?\n/).filter(Boolean)
|
|
49
91
|
for (const h of homes) {
|
|
50
|
-
if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17')) {
|
|
92
|
+
if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17') || h.toLowerCase().includes('21') || h.toLowerCase().includes('jdk-21')) {
|
|
51
93
|
const candidate = `/Library/Java/JavaVirtualMachines/${h}/Contents/Home`
|
|
52
94
|
return candidate
|
|
53
95
|
}
|
|
54
96
|
}
|
|
55
|
-
} catch {}
|
|
97
|
+
} catch (e: unknown) { console.debug('[java.detect] listing /Library/Java/JavaVirtualMachines failed: ' + String(e)) }
|
|
56
98
|
|
|
57
|
-
// Linux locations
|
|
99
|
+
// 7) Linux locations
|
|
58
100
|
const linuxCandidates = [
|
|
59
101
|
'/usr/lib/jvm/java-17-openjdk-amd64',
|
|
60
102
|
'/usr/lib/jvm/java-17-openjdk',
|
|
61
103
|
'/usr/lib/jvm/zulu17',
|
|
62
|
-
'/usr/lib/jvm/temurin-17-jdk'
|
|
104
|
+
'/usr/lib/jvm/temurin-17-jdk',
|
|
105
|
+
'/usr/lib/jvm/temurin-21-jdk',
|
|
106
|
+
'/usr/lib/jvm/java-21-openjdk-amd64'
|
|
63
107
|
]
|
|
64
108
|
for (const p of linuxCandidates) {
|
|
65
|
-
try { if (existsSync(p)) return p } catch {}
|
|
109
|
+
try { if (existsSync(p)) return p } catch (e: unknown) { console.debug(`[java.detect] checking linux candidate ${p} failed: ${String(e)}`) }
|
|
66
110
|
}
|
|
67
|
-
} catch {
|
|
111
|
+
} catch (e: unknown) {
|
|
112
|
+
console.debug('[java.detect] error detecting java home:', e instanceof Error ? e.message : String(e))
|
|
113
|
+
}
|
|
68
114
|
return undefined
|
|
69
115
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { detectJavaHome } from '../../src/utils/java.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
// Create a temporary fake JDK that reports Java 17
|
|
7
|
+
const tmp = fs.mkdtempSync('/tmp/fakejdk-')
|
|
8
|
+
const bin = path.join(tmp, 'bin')
|
|
9
|
+
fs.mkdirSync(bin)
|
|
10
|
+
const javaSh = path.join(bin, 'java')
|
|
11
|
+
fs.writeFileSync(javaSh, '#!/bin/sh\necho "openjdk version \"17.0.2\"" >&2\nexit 0\n')
|
|
12
|
+
fs.chmodSync(javaSh, 0o755)
|
|
13
|
+
|
|
14
|
+
process.env.ANDROID_STUDIO_JDK = tmp
|
|
15
|
+
|
|
16
|
+
const detected = await detectJavaHome()
|
|
17
|
+
console.log('DETECTED:', detected)
|
|
18
|
+
if (detected !== tmp) {
|
|
19
|
+
console.error('TEST FAIL: expected', tmp, 'got', detected)
|
|
20
|
+
process.exit(2)
|
|
21
|
+
}
|
|
22
|
+
console.log('TEST PASS')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
run().catch(e => { console.error(e); process.exit(1) })
|