mobile-debug-mcp 0.30.2 → 0.32.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 +50 -5
- package/dist/interact/android.js +13 -8
- package/dist/interact/index.js +33 -22
- package/dist/interact/ios.js +5 -2
- package/dist/manage/android.js +20 -10
- package/dist/manage/ios.js +20 -11
- package/dist/observe/android.js +6 -1
- package/dist/observe/index.js +22 -5
- package/dist/observe/ios.js +7 -2
- package/dist/observe/snapshot-metadata.js +94 -15
- package/dist/server/common.js +14 -7
- package/dist/server/journey-schema.js +60 -0
- package/dist/server/tool-definitions.js +59 -6
- package/dist/server/tool-handlers.js +358 -55
- package/dist/server-core.js +1 -1
- package/dist/system/index.js +485 -48
- package/dist/system/ios.js +15 -7
- package/dist/utils/android/utils.js +4 -4
- package/dist/utils/cli/idb/check-idb.js +10 -3
- package/dist/utils/cli/idb/idb-helper.js +47 -13
- package/dist/utils/exec.js +4 -2
- package/dist/utils/ios/utils.js +10 -11
- package/dist/utils/operation-budget.js +25 -0
- package/dist/utils/resolve-device.js +12 -1
- package/dist/utils/timing.js +19 -0
- package/dist/utils/ui/index.js +2 -0
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +4 -4
- package/docs/benchmarks/interaction-latency/README.md +26 -0
- package/docs/benchmarks/interaction-latency/final.json +1810 -0
- package/docs/benchmarks/interaction-latency/final.md +17 -0
- package/docs/benchmarks/interaction-latency/ios-validation.md +11 -0
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/specs-deltas/018-interaction-latency-and-batched-journeys.md +301 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/interact.md +30 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/src/interact/android.ts +13 -8
- package/src/interact/index.ts +32 -23
- package/src/interact/ios.ts +5 -2
- package/src/manage/android.ts +19 -10
- package/src/manage/ios.ts +19 -11
- package/src/observe/android.ts +5 -1
- package/src/observe/index.ts +21 -5
- package/src/observe/ios.ts +6 -2
- package/src/observe/snapshot-metadata.ts +94 -15
- package/src/server/common.ts +13 -6
- package/src/server/journey-schema.ts +63 -0
- package/src/server/tool-definitions.ts +60 -7
- package/src/server/tool-handlers.ts +324 -53
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- package/src/types.ts +31 -2
- package/src/utils/android/utils.ts +3 -3
- package/src/utils/cli/idb/check-idb.ts +11 -2
- package/src/utils/cli/idb/idb-helper.ts +49 -12
- package/src/utils/exec.ts +4 -2
- package/src/utils/ios/utils.ts +9 -10
- package/src/utils/operation-budget.ts +22 -0
- package/src/utils/resolve-device.ts +13 -1
- package/src/utils/timing.ts +15 -0
- package/src/utils/ui/index.ts +2 -0
- package/test/device/automated/latency/benchmark.ts +136 -0
- package/test/device/automated/latency/report.ts +6 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +6 -0
- package/test/fixtures/latency-journey-app/README.md +10 -0
- package/test/fixtures/latency-journey-app/build.ts +26 -0
- package/test/fixtures/latency-journey-app/src/MainActivity.java +48 -0
- package/test/unit/interact/tap_element.test.ts +1 -1
- package/test/unit/manage/restart_budget.test.ts +37 -0
- package/test/unit/observe/capture_debug_snapshot.test.ts +6 -0
- package/test/unit/observe/snapshot_metadata.test.ts +20 -1
- package/test/unit/observe/state_delta.test.ts +15 -0
- package/test/unit/server/benchmark_report.test.ts +4 -0
- package/test/unit/server/contract.test.ts +1 -1
- package/test/unit/server/interaction_modes.test.ts +60 -0
- package/test/unit/server/operation_budget.test.ts +32 -0
- package/test/unit/server/response_shapes.test.ts +3 -3
- package/test/unit/server/run_journey.test.ts +90 -0
- package/test/unit/server/tap_selector.test.ts +68 -0
- package/test/unit/system/get_system_status.test.ts +3 -0
- package/test/unit/system/system_status.test.ts +40 -0
- package/test/unit/utils/idb_health.test.ts +74 -0
package/dist/system/index.js
CHANGED
|
@@ -1,68 +1,505 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import { checkAndroid } from './android.js';
|
|
2
|
-
import { checkIOS } from './ios.js';
|
|
3
4
|
import { checkGradle } from './gradle.js';
|
|
5
|
+
import { checkIOS } from './ios.js';
|
|
6
|
+
import { resolveAdbCmd } from '../utils/android/utils.js';
|
|
7
|
+
import { getIdbCmd, getXcrunCmd } from '../utils/ios/utils.js';
|
|
8
|
+
import { probeIdb } from '../utils/cli/idb/idb-helper.js';
|
|
9
|
+
import { detectJavaHome } from '../utils/java.js';
|
|
10
|
+
function normalizeHostOs(platform) {
|
|
11
|
+
switch (platform) {
|
|
12
|
+
case 'darwin':
|
|
13
|
+
return 'macos';
|
|
14
|
+
case 'linux':
|
|
15
|
+
return 'linux';
|
|
16
|
+
case 'win32':
|
|
17
|
+
return 'windows';
|
|
18
|
+
case 'freebsd':
|
|
19
|
+
case 'openbsd':
|
|
20
|
+
case 'netbsd':
|
|
21
|
+
case 'sunos':
|
|
22
|
+
return platform;
|
|
23
|
+
default:
|
|
24
|
+
return 'unknown';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isSupportedHost(hostOs) {
|
|
28
|
+
return hostOs === 'macos' || hostOs === 'linux' || hostOs === 'windows';
|
|
29
|
+
}
|
|
30
|
+
function firstVersionToken(raw) {
|
|
31
|
+
if (!raw)
|
|
32
|
+
return null;
|
|
33
|
+
const match = String(raw).match(/(\d+(?:\.\d+)+|\d+)/);
|
|
34
|
+
return match?.[1] ?? null;
|
|
35
|
+
}
|
|
36
|
+
function probeCommand(command, args, timeoutMs = 2000) {
|
|
37
|
+
try {
|
|
38
|
+
const result = spawnSync(command, args, { encoding: 'utf8', timeout: timeoutMs, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
|
+
const raw = `${result.stdout || ''}${result.stderr || ''}`.trim() || null;
|
|
40
|
+
return {
|
|
41
|
+
ok: result.status === 0,
|
|
42
|
+
raw,
|
|
43
|
+
parsedVersion: firstVersionToken(raw)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { ok: false, raw: null, parsedVersion: null };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function makeFailure(code, platform, message, opts = {}) {
|
|
51
|
+
return {
|
|
52
|
+
code,
|
|
53
|
+
platform,
|
|
54
|
+
tool: opts.tool ?? null,
|
|
55
|
+
message,
|
|
56
|
+
searchedLocations: opts.searchedLocations ?? [],
|
|
57
|
+
selectedProvider: opts.selectedProvider ?? null,
|
|
58
|
+
rawVersion: opts.rawVersion ?? null,
|
|
59
|
+
remediation: opts.remediation ?? []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function makeToolEntry(resolvedPath, discoverySource, rawVersionOutput, validationStatus) {
|
|
63
|
+
return {
|
|
64
|
+
resolvedPath,
|
|
65
|
+
discoverySource,
|
|
66
|
+
parsedVersion: firstVersionToken(rawVersionOutput),
|
|
67
|
+
rawVersionOutput,
|
|
68
|
+
validationStatus
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function describeAdbDiscovery(adbCmd) {
|
|
72
|
+
const searchedLocations = ['ADB_PATH', 'ANDROID_SDK_ROOT/platform-tools/adb', 'ANDROID_HOME/platform-tools/adb', '~/Library/Android/sdk/platform-tools/adb', 'PATH'];
|
|
73
|
+
if (process.env.ADB_PATH && process.env.ADB_PATH.trim())
|
|
74
|
+
return { discoverySource: 'ADB_PATH', searchedLocations };
|
|
75
|
+
if (process.env.ANDROID_SDK_ROOT)
|
|
76
|
+
return { discoverySource: 'ANDROID_SDK_ROOT', searchedLocations };
|
|
77
|
+
if (process.env.ANDROID_HOME)
|
|
78
|
+
return { discoverySource: 'ANDROID_HOME', searchedLocations };
|
|
79
|
+
if (adbCmd.includes('Library/Android/sdk'))
|
|
80
|
+
return { discoverySource: 'HOME_SDK', searchedLocations };
|
|
81
|
+
if (adbCmd === 'adb')
|
|
82
|
+
return { discoverySource: 'PATH_OR_FALLBACK', searchedLocations };
|
|
83
|
+
return { discoverySource: 'PATH', searchedLocations };
|
|
84
|
+
}
|
|
85
|
+
function describeXcrunDiscovery(xcrunCmd) {
|
|
86
|
+
const searchedLocations = ['XCRUN_PATH', 'PATH'];
|
|
87
|
+
if (process.env.XCRUN_PATH && process.env.XCRUN_PATH.trim())
|
|
88
|
+
return { discoverySource: 'XCRUN_PATH', searchedLocations };
|
|
89
|
+
return { discoverySource: xcrunCmd === 'xcrun' ? 'PATH' : 'PATH', searchedLocations };
|
|
90
|
+
}
|
|
91
|
+
function describeIdbDiscovery(idbCmd) {
|
|
92
|
+
const searchedLocations = ['MCP_IDB_PATH', 'IDB_PATH', 'mcp.config.json', '~/.mcp/config.json', 'PATH', '/opt/homebrew/bin/idb', '/usr/local/bin/idb'];
|
|
93
|
+
if (process.env.MCP_IDB_PATH && process.env.MCP_IDB_PATH.trim())
|
|
94
|
+
return { discoverySource: 'MCP_IDB_PATH', searchedLocations };
|
|
95
|
+
if (process.env.IDB_PATH && process.env.IDB_PATH.trim())
|
|
96
|
+
return { discoverySource: 'IDB_PATH', searchedLocations };
|
|
97
|
+
return { discoverySource: idbCmd === 'idb' ? 'PATH_OR_FALLBACK' : 'PATH', searchedLocations };
|
|
98
|
+
}
|
|
99
|
+
function describeJavaDiscovery(javaHome) {
|
|
100
|
+
const searchedLocations = ['GRADLE_JAVA_HOME', 'JAVA_HOME', 'GRADLE_HOME', 'detectJavaHome'];
|
|
101
|
+
if (process.env.GRADLE_JAVA_HOME && process.env.GRADLE_JAVA_HOME.trim())
|
|
102
|
+
return { discoverySource: 'GRADLE_JAVA_HOME', searchedLocations };
|
|
103
|
+
if (process.env.JAVA_HOME && process.env.JAVA_HOME.trim())
|
|
104
|
+
return { discoverySource: 'JAVA_HOME', searchedLocations };
|
|
105
|
+
if (process.env.GRADLE_HOME && process.env.GRADLE_HOME.trim())
|
|
106
|
+
return { discoverySource: 'GRADLE_HOME', searchedLocations };
|
|
107
|
+
return { discoverySource: javaHome ? 'detected' : null, searchedLocations };
|
|
108
|
+
}
|
|
109
|
+
function resolveJavaBinaryPath(javaHome, hostOs) {
|
|
110
|
+
const javaBinary = hostOs === 'windows' ? 'java.exe' : 'java';
|
|
111
|
+
return path.join(javaHome, 'bin', javaBinary);
|
|
112
|
+
}
|
|
113
|
+
function buildUnsupportedHostStatus(hostOs) {
|
|
114
|
+
const failureMessage = `Host operating system '${hostOs}' is not supported.`;
|
|
115
|
+
const remediation = ['Run Mobile Debug MCP on macOS, Linux, or Windows.'];
|
|
116
|
+
const failureAndroid = makeFailure('HOST_OS_UNSUPPORTED', 'android', failureMessage, {
|
|
117
|
+
remediation
|
|
118
|
+
});
|
|
119
|
+
const failureIos = makeFailure('HOST_OS_UNSUPPORTED', 'ios', failureMessage, {
|
|
120
|
+
remediation
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
status: 'blocked',
|
|
125
|
+
adbAvailable: false,
|
|
126
|
+
adbVersion: '',
|
|
127
|
+
devices: 0,
|
|
128
|
+
deviceStates: '',
|
|
129
|
+
logsAvailable: false,
|
|
130
|
+
envValid: false,
|
|
131
|
+
issues: [failureMessage],
|
|
132
|
+
appInstalled: undefined,
|
|
133
|
+
iosAvailable: false,
|
|
134
|
+
iosDevices: 0,
|
|
135
|
+
gradleJavaHome: undefined,
|
|
136
|
+
gradleValid: false,
|
|
137
|
+
gradleFilesChecked: [],
|
|
138
|
+
gradleSuggestedFixes: [],
|
|
139
|
+
host: { os: hostOs, supported: false },
|
|
140
|
+
android: {
|
|
141
|
+
status: 'unsupported',
|
|
142
|
+
tools: {},
|
|
143
|
+
providers: {},
|
|
144
|
+
capabilities: {},
|
|
145
|
+
devices: [],
|
|
146
|
+
failures: [failureAndroid]
|
|
147
|
+
},
|
|
148
|
+
ios: {
|
|
149
|
+
status: 'unsupported',
|
|
150
|
+
tools: {},
|
|
151
|
+
providers: {},
|
|
152
|
+
capabilities: {},
|
|
153
|
+
devices: [],
|
|
154
|
+
failures: [failureIos]
|
|
155
|
+
},
|
|
156
|
+
summary: {
|
|
157
|
+
overall: 'blocked',
|
|
158
|
+
android: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
|
|
159
|
+
ios: { ready: false, summary: `Host operating system '${hostOs}' is not supported.`, blockers: [failureMessage] },
|
|
160
|
+
gradle: { ready: false, summary: 'Gradle status unavailable on unsupported host', blockers: [], suggestedFixes: [] }
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function buildAndroidPlatform(android, hostSupported) {
|
|
165
|
+
const resolvedAdbCmd = resolveAdbCmd();
|
|
166
|
+
const adbDiscovery = describeAdbDiscovery(resolvedAdbCmd);
|
|
167
|
+
const adbTool = makeToolEntry(resolvedAdbCmd, adbDiscovery.discoverySource, android.adbVersion || null, android.adbAvailable ? 'valid' : 'invalid');
|
|
168
|
+
const adbAvailable = !!android.adbAvailable;
|
|
169
|
+
const deviceIssues = android.issues.filter((issue) => /unauthorized|offline|No Android devices connected/i.test(issue));
|
|
170
|
+
const envFailure = !android.envValid
|
|
171
|
+
? makeFailure('TOOL_VALIDATION_FAILED', 'android', 'Android environment prerequisites are incomplete.', {
|
|
172
|
+
tool: 'adb',
|
|
173
|
+
searchedLocations: ['ANDROID_SDK_ROOT', 'ANDROID_HOME', 'PATH'],
|
|
174
|
+
selectedProvider: 'adb',
|
|
175
|
+
rawVersion: android.adbVersion || null,
|
|
176
|
+
remediation: ['Set ANDROID_SDK_ROOT or ANDROID_HOME, or ensure adb is on PATH.']
|
|
177
|
+
})
|
|
178
|
+
: null;
|
|
179
|
+
const adbFailure = !adbAvailable
|
|
180
|
+
? makeFailure(process.env.ADB_PATH && process.env.ADB_PATH.trim() ? 'TOOL_NOT_EXECUTABLE' : 'TOOL_NOT_FOUND', 'android', 'Android adb could not be resolved or executed.', {
|
|
181
|
+
tool: 'adb',
|
|
182
|
+
searchedLocations: adbDiscovery.searchedLocations,
|
|
183
|
+
selectedProvider: null,
|
|
184
|
+
rawVersion: android.adbVersion || null,
|
|
185
|
+
remediation: [
|
|
186
|
+
'Install Android platform-tools.',
|
|
187
|
+
'Set ADB_PATH or ANDROID_SDK_ROOT/ANDROID_HOME if adb is installed in a non-standard location.'
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
: null;
|
|
191
|
+
const deviceFailure = adbAvailable && android.devices === 0
|
|
192
|
+
? makeFailure('DEVICE_NOT_FOUND', 'android', 'No Android devices or emulators are connected.', {
|
|
193
|
+
tool: 'adb',
|
|
194
|
+
searchedLocations: ['adb devices -l'],
|
|
195
|
+
selectedProvider: 'adb',
|
|
196
|
+
rawVersion: android.adbVersion || null,
|
|
197
|
+
remediation: ['Start an emulator or connect a device, then re-run adb devices.']
|
|
198
|
+
})
|
|
199
|
+
: null;
|
|
200
|
+
const diagnosticFailures = [adbFailure, envFailure, deviceFailure].filter((value) => value !== null);
|
|
201
|
+
const status = !hostSupported
|
|
202
|
+
? 'unsupported'
|
|
203
|
+
: !adbAvailable
|
|
204
|
+
? 'unavailable'
|
|
205
|
+
: (android.devices === 0 || deviceIssues.length > 0 || !android.logsAvailable || !android.envValid)
|
|
206
|
+
? 'partially_ready'
|
|
207
|
+
: 'ready';
|
|
208
|
+
const deviceEntries = android.devices > 0
|
|
209
|
+
? Array.from({ length: android.devices }, (_, index) => ({
|
|
210
|
+
id: `android-device-${index + 1}`,
|
|
211
|
+
state: deviceIssues.length > 0 ? 'degraded' : 'connected'
|
|
212
|
+
}))
|
|
213
|
+
: [];
|
|
214
|
+
const deviceDiscoveryAvailable = adbAvailable;
|
|
215
|
+
const installAvailable = adbAvailable && android.devices > 0 && deviceIssues.length === 0;
|
|
216
|
+
const logAvailable = adbAvailable && android.logsAvailable;
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
tools: {
|
|
220
|
+
adb: adbTool
|
|
221
|
+
},
|
|
222
|
+
providers: {
|
|
223
|
+
adb: { available: adbAvailable, selected: true, tool: 'adb' },
|
|
224
|
+
jdwp: { available: adbAvailable && android.devices > 0, selected: false, tool: 'adb' }
|
|
225
|
+
},
|
|
226
|
+
capabilities: {
|
|
227
|
+
device_discovery: {
|
|
228
|
+
availability: deviceDiscoveryAvailable,
|
|
229
|
+
selectedProvider: deviceDiscoveryAvailable ? 'adb' : null,
|
|
230
|
+
blockingFailure: deviceDiscoveryAvailable ? null : diagnosticFailures[0] ?? null
|
|
231
|
+
},
|
|
232
|
+
install_app: {
|
|
233
|
+
availability: installAvailable,
|
|
234
|
+
selectedProvider: installAvailable ? 'adb' : null,
|
|
235
|
+
blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
|
|
236
|
+
},
|
|
237
|
+
launch_app: {
|
|
238
|
+
availability: installAvailable,
|
|
239
|
+
selectedProvider: installAvailable ? 'adb' : null,
|
|
240
|
+
blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
|
|
241
|
+
},
|
|
242
|
+
ui_interaction: {
|
|
243
|
+
availability: installAvailable,
|
|
244
|
+
selectedProvider: installAvailable ? 'adb' : null,
|
|
245
|
+
blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
|
|
246
|
+
},
|
|
247
|
+
screenshot: {
|
|
248
|
+
availability: installAvailable,
|
|
249
|
+
selectedProvider: installAvailable ? 'adb' : null,
|
|
250
|
+
blockingFailure: installAvailable ? null : deviceFailure ?? adbFailure ?? envFailure
|
|
251
|
+
},
|
|
252
|
+
log_capture: {
|
|
253
|
+
availability: logAvailable,
|
|
254
|
+
selectedProvider: logAvailable ? 'adb' : null,
|
|
255
|
+
blockingFailure: logAvailable ? null : adbFailure ?? envFailure
|
|
256
|
+
},
|
|
257
|
+
debug: {
|
|
258
|
+
availability: adbAvailable && android.devices > 0,
|
|
259
|
+
selectedProvider: adbAvailable && android.devices > 0 ? 'JDWP' : null,
|
|
260
|
+
blockingFailure: adbAvailable && android.devices > 0 ? null : deviceFailure ?? adbFailure ?? envFailure
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
devices: deviceEntries,
|
|
264
|
+
failures: diagnosticFailures
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function buildIOSPlatform(ios, hostSupported, hostOs) {
|
|
268
|
+
const platformUnsupported = !hostSupported || hostOs === 'linux' || hostOs === 'windows';
|
|
269
|
+
if (platformUnsupported) {
|
|
270
|
+
return {
|
|
271
|
+
status: 'unsupported',
|
|
272
|
+
tools: {},
|
|
273
|
+
providers: {},
|
|
274
|
+
capabilities: {},
|
|
275
|
+
devices: [],
|
|
276
|
+
failures: [
|
|
277
|
+
makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
|
|
278
|
+
tool: null,
|
|
279
|
+
searchedLocations: [],
|
|
280
|
+
selectedProvider: null,
|
|
281
|
+
rawVersion: null,
|
|
282
|
+
remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
|
|
283
|
+
})
|
|
284
|
+
]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const xcrunCmd = getXcrunCmd();
|
|
288
|
+
const xcrunProbe = ios.iosAvailable ? probeCommand(xcrunCmd, ['--version']) : { ok: false, raw: null, parsedVersion: null };
|
|
289
|
+
const idbCmd = getIdbCmd();
|
|
290
|
+
const idbProbe = hostSupported && hostOs === 'macos' ? probeIdb(idbCmd) : { ok: false, raw: null, parsedVersion: null };
|
|
291
|
+
const idbCompanionProbe = hostSupported && hostOs === 'macos' ? probeCommand('idb_companion', ['--version']) : { ok: false, raw: null, parsedVersion: null };
|
|
292
|
+
const xcrunTool = makeToolEntry(xcrunCmd, describeXcrunDiscovery(xcrunCmd).discoverySource, xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid');
|
|
293
|
+
const idbTool = makeToolEntry(idbCmd, describeIdbDiscovery(idbCmd).discoverySource, idbProbe.raw, idbProbe.ok ? 'valid' : 'invalid');
|
|
294
|
+
const idbCompanionTool = makeToolEntry('idb_companion', 'PATH', idbCompanionProbe.raw, idbCompanionProbe.ok ? 'valid' : 'invalid');
|
|
295
|
+
const lldbTool = makeToolEntry('xcrun lldb', 'xcrun', xcrunProbe.raw, xcrunProbe.ok ? 'valid' : 'invalid');
|
|
296
|
+
const simctlAvailable = !!ios.iosAvailable;
|
|
297
|
+
const idbAvailable = !!idbProbe.ok;
|
|
298
|
+
const bootedDevices = ios.iosDevices > 0
|
|
299
|
+
? Array.from({ length: ios.iosDevices }, (_, index) => ({
|
|
300
|
+
id: `ios-simulator-${index + 1}`,
|
|
301
|
+
state: 'booted'
|
|
302
|
+
}))
|
|
303
|
+
: [];
|
|
304
|
+
const platformUnavailableFailure = !platformUnsupported && !simctlAvailable
|
|
305
|
+
? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is unavailable on this host.', {
|
|
306
|
+
tool: 'xcrun',
|
|
307
|
+
searchedLocations: describeXcrunDiscovery(xcrunCmd).searchedLocations,
|
|
308
|
+
selectedProvider: null,
|
|
309
|
+
rawVersion: xcrunProbe.raw,
|
|
310
|
+
remediation: ['Install Xcode Command Line Tools and ensure xcrun is on PATH.']
|
|
311
|
+
})
|
|
312
|
+
: null;
|
|
313
|
+
const deviceFailure = simctlAvailable && !bootedDevices.length
|
|
314
|
+
? makeFailure('DEVICE_NOT_FOUND', 'ios', 'No iOS simulators are booted.', {
|
|
315
|
+
tool: 'simctl',
|
|
316
|
+
searchedLocations: ['xcrun simctl list devices booted --json'],
|
|
317
|
+
selectedProvider: 'simctl',
|
|
318
|
+
rawVersion: xcrunProbe.raw,
|
|
319
|
+
remediation: ['Boot a simulator in Xcode or via xcrun simctl boot.']
|
|
320
|
+
})
|
|
321
|
+
: null;
|
|
322
|
+
const selectedInstallProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null;
|
|
323
|
+
const selectedDebugProvider = simctlAvailable ? 'LLDB' : null;
|
|
324
|
+
const selectedDiscoveryProvider = idbAvailable ? 'idb' : simctlAvailable ? 'simctl' : null;
|
|
325
|
+
const status = platformUnsupported
|
|
326
|
+
? 'unsupported'
|
|
327
|
+
: !simctlAvailable
|
|
328
|
+
? 'unavailable'
|
|
329
|
+
: !bootedDevices.length
|
|
330
|
+
? 'partially_ready'
|
|
331
|
+
: 'ready';
|
|
332
|
+
const iosFailure = platformUnsupported
|
|
333
|
+
? makeFailure('PLATFORM_UNAVAILABLE', 'ios', 'iOS tooling is only supported on macOS.', {
|
|
334
|
+
tool: null,
|
|
335
|
+
searchedLocations: [],
|
|
336
|
+
selectedProvider: null,
|
|
337
|
+
rawVersion: null,
|
|
338
|
+
remediation: ['Run Mobile Debug MCP on macOS for iOS toolchain discovery.']
|
|
339
|
+
})
|
|
340
|
+
: platformUnavailableFailure ?? deviceFailure ?? null;
|
|
341
|
+
const failureList = [iosFailure].filter((value) => value !== null);
|
|
342
|
+
return {
|
|
343
|
+
status,
|
|
344
|
+
tools: {
|
|
345
|
+
xcrun: xcrunTool,
|
|
346
|
+
idb: idbTool,
|
|
347
|
+
idb_companion: idbCompanionTool,
|
|
348
|
+
lldb: lldbTool
|
|
349
|
+
},
|
|
350
|
+
providers: {
|
|
351
|
+
idb: { available: idbAvailable, selected: selectedInstallProvider === 'idb', tool: 'idb' },
|
|
352
|
+
simctl: { available: simctlAvailable, selected: selectedInstallProvider === 'simctl', tool: 'xcrun' },
|
|
353
|
+
lldb: { available: simctlAvailable, selected: selectedDebugProvider === 'LLDB', tool: 'xcrun' }
|
|
354
|
+
},
|
|
355
|
+
capabilities: {
|
|
356
|
+
device_discovery: {
|
|
357
|
+
availability: !!selectedDiscoveryProvider,
|
|
358
|
+
selectedProvider: selectedDiscoveryProvider,
|
|
359
|
+
blockingFailure: selectedDiscoveryProvider ? null : iosFailure
|
|
360
|
+
},
|
|
361
|
+
install_app: {
|
|
362
|
+
availability: !!selectedInstallProvider && bootedDevices.length > 0,
|
|
363
|
+
selectedProvider: selectedInstallProvider,
|
|
364
|
+
blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
|
|
365
|
+
},
|
|
366
|
+
launch_app: {
|
|
367
|
+
availability: !!selectedInstallProvider && bootedDevices.length > 0,
|
|
368
|
+
selectedProvider: selectedInstallProvider,
|
|
369
|
+
blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
|
|
370
|
+
},
|
|
371
|
+
ui_interaction: {
|
|
372
|
+
availability: !!selectedInstallProvider && bootedDevices.length > 0,
|
|
373
|
+
selectedProvider: selectedInstallProvider,
|
|
374
|
+
blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
|
|
375
|
+
},
|
|
376
|
+
screenshot: {
|
|
377
|
+
availability: !!selectedInstallProvider && bootedDevices.length > 0,
|
|
378
|
+
selectedProvider: selectedInstallProvider,
|
|
379
|
+
blockingFailure: selectedInstallProvider && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
|
|
380
|
+
},
|
|
381
|
+
log_capture: {
|
|
382
|
+
availability: simctlAvailable && bootedDevices.length > 0,
|
|
383
|
+
selectedProvider: simctlAvailable ? 'simctl' : null,
|
|
384
|
+
blockingFailure: simctlAvailable && bootedDevices.length > 0 ? null : deviceFailure ?? iosFailure
|
|
385
|
+
},
|
|
386
|
+
debug: {
|
|
387
|
+
availability: !!selectedDebugProvider,
|
|
388
|
+
selectedProvider: selectedDebugProvider,
|
|
389
|
+
blockingFailure: selectedDebugProvider ? null : iosFailure
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
devices: bootedDevices,
|
|
393
|
+
failures: failureList
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function buildLegacySummary(androidStatus, iosStatus, gradle) {
|
|
397
|
+
const androidReady = androidStatus.status === 'ready';
|
|
398
|
+
const iosReady = iosStatus.status === 'ready';
|
|
399
|
+
const gradleReady = (gradle.issues || []).length === 0;
|
|
400
|
+
const overallStatus = (androidReady || iosReady || gradleReady)
|
|
401
|
+
? ((androidReady && iosReady && gradleReady) ? 'ready' : 'degraded')
|
|
402
|
+
: 'blocked';
|
|
403
|
+
return {
|
|
404
|
+
overall: overallStatus,
|
|
405
|
+
android: {
|
|
406
|
+
ready: androidReady,
|
|
407
|
+
summary: androidStatus.status === 'ready'
|
|
408
|
+
? `${androidStatus.devices.length} Android device(s) connected; log access available`
|
|
409
|
+
: androidStatus.status === 'partially_ready'
|
|
410
|
+
? 'Android toolchain available but device-dependent capabilities are limited'
|
|
411
|
+
: androidStatus.status === 'unsupported'
|
|
412
|
+
? 'Android unsupported on this host'
|
|
413
|
+
: 'ADB unavailable',
|
|
414
|
+
blockers: androidStatus.failures.map((failure) => failure.message)
|
|
415
|
+
},
|
|
416
|
+
ios: {
|
|
417
|
+
ready: iosReady,
|
|
418
|
+
summary: iosStatus.status === 'ready'
|
|
419
|
+
? `${iosStatus.devices.length} iOS simulator(s) booted`
|
|
420
|
+
: iosStatus.status === 'partially_ready'
|
|
421
|
+
? 'iOS toolchain available but device-dependent capabilities are limited'
|
|
422
|
+
: iosStatus.status === 'unsupported'
|
|
423
|
+
? 'iOS unsupported on this host'
|
|
424
|
+
: 'xcrun unavailable',
|
|
425
|
+
blockers: iosStatus.failures.map((failure) => failure.message)
|
|
426
|
+
},
|
|
427
|
+
gradle: {
|
|
428
|
+
ready: gradleReady,
|
|
429
|
+
summary: !gradle.gradleJavaHome
|
|
430
|
+
? 'No explicit Gradle JDK override detected'
|
|
431
|
+
: gradleReady
|
|
432
|
+
? `Gradle JDK configured at ${gradle.gradleJavaHome}`
|
|
433
|
+
: `Gradle JDK override invalid: ${gradle.gradleJavaHome}`,
|
|
434
|
+
blockers: gradle.issues || [],
|
|
435
|
+
suggestedFixes: gradle.suggestedFixes || []
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
4
439
|
export async function getSystemStatus() {
|
|
440
|
+
const hostOs = normalizeHostOs((process.env.MOBILE_DEBUG_MCP_HOST_OS || process.platform));
|
|
441
|
+
if (!isSupportedHost(hostOs)) {
|
|
442
|
+
return buildUnsupportedHostStatus(hostOs);
|
|
443
|
+
}
|
|
5
444
|
try {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
445
|
+
const [androidRaw, gradleRaw] = await Promise.all([checkAndroid(), checkGradle()]);
|
|
446
|
+
const iosRaw = hostOs === 'macos'
|
|
447
|
+
? await checkIOS()
|
|
448
|
+
: {
|
|
449
|
+
iosAvailable: false,
|
|
450
|
+
iosDevices: 0,
|
|
451
|
+
issues: ['iOS tooling is only supported on macOS.']
|
|
452
|
+
};
|
|
453
|
+
const androidStatus = buildAndroidPlatform(androidRaw, true);
|
|
454
|
+
const iosStatus = buildIOSPlatform(iosRaw, hostOs === 'macos', hostOs);
|
|
455
|
+
const gradle = gradleRaw;
|
|
456
|
+
const issues = [...androidRaw.issues, ...iosRaw.issues, ...(gradle.issues || [])];
|
|
10
457
|
const success = issues.length === 0;
|
|
11
|
-
const androidReady =
|
|
12
|
-
const iosReady =
|
|
13
|
-
const gradleReady = (gradle.issues || []).length === 0;
|
|
458
|
+
const androidReady = androidRaw.adbAvailable && androidRaw.devices > 0 && !androidRaw.issues.some((issue) => /unauthorized|offline/i.test(issue));
|
|
459
|
+
const iosReady = hostOs === 'macos' ? (iosRaw.iosAvailable && iosRaw.iosDevices > 0) : false;
|
|
14
460
|
const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked');
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
: android.logsAvailable
|
|
20
|
-
? `${android.devices} Android device(s) connected; log access available`
|
|
21
|
-
: `${android.devices} Android device(s) connected; log access unavailable`;
|
|
22
|
-
const iosSummary = !ios.iosAvailable
|
|
23
|
-
? 'xcrun unavailable'
|
|
24
|
-
: ios.iosDevices === 0
|
|
25
|
-
? 'xcrun available but no iOS simulators booted'
|
|
26
|
-
: `${ios.iosDevices} iOS simulator(s) booted`;
|
|
27
|
-
const gradleSummary = !gradle.gradleJavaHome
|
|
28
|
-
? 'No explicit Gradle JDK override detected'
|
|
29
|
-
: gradleReady
|
|
30
|
-
? `Gradle JDK configured at ${gradle.gradleJavaHome}`
|
|
31
|
-
: `Gradle JDK override invalid: ${gradle.gradleJavaHome}`;
|
|
461
|
+
const javaHome = gradle.gradleJavaHome || (await detectJavaHome().catch(() => undefined));
|
|
462
|
+
const javaBin = javaHome ? resolveJavaBinaryPath(javaHome, hostOs) : null;
|
|
463
|
+
const javaProbe = javaBin ? probeCommand(javaBin, ['-version']) : { ok: false, raw: null, parsedVersion: null };
|
|
464
|
+
const javaTool = makeToolEntry(javaBin, describeJavaDiscovery(javaHome ?? null).discoverySource, javaProbe.raw, javaProbe.ok ? 'valid' : (javaHome ? 'invalid' : 'missing'));
|
|
32
465
|
return {
|
|
33
466
|
success,
|
|
34
467
|
status: overallStatus,
|
|
35
|
-
adbAvailable:
|
|
36
|
-
adbVersion:
|
|
37
|
-
devices:
|
|
38
|
-
deviceStates:
|
|
39
|
-
logsAvailable:
|
|
40
|
-
envValid:
|
|
468
|
+
adbAvailable: androidRaw.adbAvailable,
|
|
469
|
+
adbVersion: androidRaw.adbVersion,
|
|
470
|
+
devices: androidRaw.devices,
|
|
471
|
+
deviceStates: androidRaw.deviceStates,
|
|
472
|
+
logsAvailable: androidRaw.logsAvailable,
|
|
473
|
+
envValid: androidRaw.envValid,
|
|
41
474
|
issues,
|
|
42
|
-
appInstalled:
|
|
43
|
-
iosAvailable:
|
|
44
|
-
iosDevices:
|
|
475
|
+
appInstalled: androidRaw.appInstalled,
|
|
476
|
+
iosAvailable: iosRaw.iosAvailable,
|
|
477
|
+
iosDevices: iosRaw.iosDevices,
|
|
45
478
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
46
479
|
gradleValid: gradle.gradleValid,
|
|
47
480
|
gradleFilesChecked: gradle.filesChecked,
|
|
48
481
|
gradleSuggestedFixes: gradle.suggestedFixes,
|
|
49
|
-
|
|
50
|
-
|
|
482
|
+
host: {
|
|
483
|
+
os: hostOs,
|
|
484
|
+
supported: true
|
|
485
|
+
},
|
|
486
|
+
android: {
|
|
487
|
+
...androidStatus,
|
|
488
|
+
status: androidStatus.status
|
|
489
|
+
},
|
|
490
|
+
ios: {
|
|
491
|
+
...iosStatus,
|
|
492
|
+
status: iosStatus.status
|
|
493
|
+
},
|
|
494
|
+
summary: buildLegacySummary(androidStatus, iosStatus, gradle),
|
|
495
|
+
tools: {
|
|
51
496
|
android: {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
blockers: android.issues
|
|
55
|
-
},
|
|
56
|
-
ios: {
|
|
57
|
-
ready: iosReady,
|
|
58
|
-
summary: iosSummary,
|
|
59
|
-
blockers: ios.issues
|
|
497
|
+
adb: androidStatus.tools.adb,
|
|
498
|
+
java: javaTool
|
|
60
499
|
},
|
|
500
|
+
ios: iosStatus.tools,
|
|
61
501
|
gradle: {
|
|
62
|
-
|
|
63
|
-
summary: gradleSummary,
|
|
64
|
-
blockers: gradle.issues || [],
|
|
65
|
-
suggestedFixes: gradle.suggestedFixes || []
|
|
502
|
+
java: javaTool
|
|
66
503
|
}
|
|
67
504
|
}
|
|
68
505
|
};
|
package/dist/system/ios.js
CHANGED
|
@@ -11,14 +11,22 @@ export async function checkIOS() {
|
|
|
11
11
|
iosAvailable = true;
|
|
12
12
|
try {
|
|
13
13
|
const simOut = execSync(`${xcrun} simctl list devices booted --json`, { encoding: 'utf8', timeout: 1500, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(simOut);
|
|
16
|
+
let count = 0;
|
|
17
|
+
for (const k in data.devices) {
|
|
18
|
+
const arr = data.devices[k];
|
|
19
|
+
if (Array.isArray(arr))
|
|
20
|
+
count += arr.filter((d) => (d.state || '').toLowerCase() === 'booted').length;
|
|
21
|
+
}
|
|
22
|
+
iosDevices = count;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Some xcrun builds include extra text around the JSON response.
|
|
26
|
+
// Fall back to counting explicit booted state markers so readiness remains deterministic.
|
|
27
|
+
const bootedMatches = (simOut.match(/"state"\s*:\s*"Booted"/gi) || []).length;
|
|
28
|
+
iosDevices = bootedMatches;
|
|
20
29
|
}
|
|
21
|
-
iosDevices = count;
|
|
22
30
|
if (iosDevices === 0)
|
|
23
31
|
issues.push('No iOS simulators/devices booted');
|
|
24
32
|
}
|
|
@@ -25,9 +25,12 @@ function findInPath(cmd) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
export function resolveAdbCmd() {
|
|
28
|
-
// Priority: explicit env ADB_PATH ->
|
|
28
|
+
// Priority: explicit env ADB_PATH -> PATH -> known SDK locations -> fallback to 'adb'
|
|
29
29
|
if (process.env.ADB_PATH && process.env.ADB_PATH.trim())
|
|
30
30
|
return process.env.ADB_PATH;
|
|
31
|
+
const found = findInPath('adb');
|
|
32
|
+
if (found)
|
|
33
|
+
return found;
|
|
31
34
|
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;
|
|
32
35
|
if (sdkRoot) {
|
|
33
36
|
const candidate = path.join(sdkRoot, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
@@ -38,9 +41,6 @@ export function resolveAdbCmd() {
|
|
|
38
41
|
const homeSdk = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
|
|
39
42
|
if (existsSync(homeSdk))
|
|
40
43
|
return homeSdk;
|
|
41
|
-
const found = findInPath('adb');
|
|
42
|
-
if (found)
|
|
43
|
-
return found;
|
|
44
44
|
return 'adb';
|
|
45
45
|
}
|
|
46
46
|
export function getAdbCmd() { return resolveAdbCmd(); }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync } from 'child_process';
|
|
3
3
|
import { main as installMain } from './install-idb.js';
|
|
4
|
-
import { getIdbCmd, isIDBInstalled } from './idb-helper.js';
|
|
4
|
+
import { getIdbCmd, getIdbPackageVersion, isIDBInstalled } from './idb-helper.js';
|
|
5
5
|
function which(cmd) {
|
|
6
6
|
try {
|
|
7
7
|
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
@@ -48,11 +48,18 @@ try {
|
|
|
48
48
|
const idb = process.env.IDB_PATH || getIdbCmd();
|
|
49
49
|
print('idb:', idb);
|
|
50
50
|
if (idb && isIDBInstalled()) {
|
|
51
|
+
const version = getIdbPackageVersion();
|
|
52
|
+
if (version)
|
|
53
|
+
print('fb-idb version:', version);
|
|
51
54
|
try {
|
|
52
|
-
|
|
55
|
+
const output = execSync(`${idb} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString().trim();
|
|
56
|
+
print('idb list-targets --json: ok');
|
|
57
|
+
if (output)
|
|
58
|
+
print(output);
|
|
53
59
|
}
|
|
54
60
|
catch (e) {
|
|
55
|
-
print('idb --
|
|
61
|
+
print('idb list-targets --json: (failed)', e instanceof Error ? e.message : String(e));
|
|
62
|
+
process.exit(2);
|
|
56
63
|
}
|
|
57
64
|
const companion = which('idb_companion');
|
|
58
65
|
print('which idb_companion:', companion);
|