mobile-debug-mcp 0.30.2 → 0.31.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/manage/ios.js +1 -1
- 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/ios/utils.js +5 -9
- package/docs/CHANGELOG.md +3 -0
- package/docs/ROADMAP.md +4 -4
- package/docs/specs/baseline-spec-v0.md +1 -1
- package/docs/specs-deltas/016-environment-tools.md +533 -0
- package/docs/tools/TOOLS.md +2 -0
- package/docs/tools/manage.md +4 -2
- package/docs/tools/system.md +9 -2
- package/package.json +1 -1
- package/src/manage/ios.ts +1 -1
- package/src/server-core.ts +1 -1
- package/src/system/index.ts +572 -50
- package/src/system/ios.ts +14 -7
- 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/ios/utils.ts +4 -8
- package/test/unit/interact/tap_element.test.ts +1 -1
- 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
|
@@ -20,8 +20,10 @@ function findInPath(cmd: string): string | null {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function resolveAdbCmd(): string {
|
|
23
|
-
// Priority: explicit env ADB_PATH ->
|
|
23
|
+
// Priority: explicit env ADB_PATH -> PATH -> known SDK locations -> fallback to 'adb'
|
|
24
24
|
if (process.env.ADB_PATH && process.env.ADB_PATH.trim()) return process.env.ADB_PATH
|
|
25
|
+
const found = findInPath('adb')
|
|
26
|
+
if (found) return found
|
|
25
27
|
const sdkRoot = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME
|
|
26
28
|
if (sdkRoot) {
|
|
27
29
|
const candidate = path.join(sdkRoot, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
|
|
@@ -30,8 +32,6 @@ export function resolveAdbCmd(): string {
|
|
|
30
32
|
// common macOS user SDK path
|
|
31
33
|
const homeSdk = path.join(process.env.HOME || '', 'Library', 'Android', 'sdk', 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb')
|
|
32
34
|
if (existsSync(homeSdk)) return homeSdk
|
|
33
|
-
const found = findInPath('adb')
|
|
34
|
-
if (found) return found
|
|
35
35
|
return 'adb'
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -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
|
|
|
6
6
|
function which(cmd: string): string | null {
|
|
7
7
|
try {
|
|
@@ -48,7 +48,16 @@ try {
|
|
|
48
48
|
const idb = process.env.IDB_PATH || getIdbCmd()
|
|
49
49
|
print('idb:', idb)
|
|
50
50
|
if (idb && isIDBInstalled()) {
|
|
51
|
-
|
|
51
|
+
const version = getIdbPackageVersion()
|
|
52
|
+
if (version) print('fb-idb version:', version)
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync(`${idb} list-targets --json`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString().trim()
|
|
55
|
+
print('idb list-targets --json: ok')
|
|
56
|
+
if (output) print(output)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
print('idb list-targets --json: (failed)', e instanceof Error ? e.message : String(e))
|
|
59
|
+
process.exit(2)
|
|
60
|
+
}
|
|
52
61
|
const companion = which('idb_companion')
|
|
53
62
|
print('which idb_companion:', companion)
|
|
54
63
|
if (companion) try { print('idb_companion --version:', execSync('idb_companion --version', { stdio: ['ignore','pipe','ignore'] }).toString().trim()) } catch (e) { print('idb_companion --version: (failed)', e instanceof Error ? e.message : String(e)) }
|
|
@@ -59,17 +59,54 @@ export function getIdbCmd(): string | null {
|
|
|
59
59
|
return null
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export function
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
export function getIdbPackageVersion(): string | null {
|
|
63
|
+
const runners = ['python3', 'python']
|
|
64
|
+
for (const runner of runners) {
|
|
65
|
+
try {
|
|
66
|
+
const result = spawnSync(runner, ['-m', 'pip', 'show', 'fb-idb'], {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
69
|
+
})
|
|
70
|
+
if (result.status !== 0) continue
|
|
71
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
72
|
+
const match = output.match(/^Version:\s*(.+)$/mi)
|
|
73
|
+
if (match?.[1]) return match[1].trim()
|
|
74
|
+
} catch {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function probeIdb(cmd = getIdbCmd()): { ok: boolean; raw: string | null; parsedVersion: string | null } {
|
|
82
|
+
if (!cmd) return { ok: false, raw: null, parsedVersion: null }
|
|
83
|
+
|
|
70
84
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const result = spawnSync(cmd, ['list-targets', '--json'], {
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
88
|
+
timeout: 5000
|
|
89
|
+
})
|
|
90
|
+
if (result.status !== 0) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
raw: `${result.stdout || ''}${result.stderr || ''}`.trim() || null,
|
|
94
|
+
parsedVersion: null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const version = getIdbPackageVersion()
|
|
99
|
+
const raw = version ? `fb-idb ${version}` : `${result.stdout || ''}${result.stderr || ''}`.trim() || null
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
raw,
|
|
103
|
+
parsedVersion: version
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return { ok: false, raw: null, parsedVersion: null }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isIDBInstalled(): boolean {
|
|
111
|
+
return probeIdb().ok
|
|
75
112
|
}
|
package/src/utils/ios/utils.ts
CHANGED
|
@@ -59,17 +59,13 @@ export function getIdbCmd() {
|
|
|
59
59
|
|
|
60
60
|
export async function isIDBInstalled(): Promise<boolean> {
|
|
61
61
|
const cmd = getIdbCmd()
|
|
62
|
+
if (!cmd) return false
|
|
62
63
|
try {
|
|
63
|
-
execSync(
|
|
64
|
+
execSync(`${cmd} list-targets --json`, { stdio: ['ignore','pipe','pipe'], timeout: 5000 })
|
|
64
65
|
return true
|
|
65
66
|
} catch (e: unknown) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return true
|
|
69
|
-
} catch (e2: unknown) {
|
|
70
|
-
console.debug(`[isIDBInstalled] idb presence check failed for '${cmd}': ${e instanceof Error ? e.message : String(e2)}`)
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
67
|
+
console.debug(`[isIDBInstalled] idb probe failed for '${cmd}': ${e instanceof Error ? e.message : String(e)}`)
|
|
68
|
+
return false
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
|
|
@@ -9,6 +9,9 @@ async function run() {
|
|
|
9
9
|
assert(typeof (payload as any).status === 'string')
|
|
10
10
|
assert(Array.isArray(payload.issues))
|
|
11
11
|
assert((payload as any).summary && typeof (payload as any).summary.overall === 'string')
|
|
12
|
+
assert((payload as any).host && typeof (payload as any).host.os === 'string')
|
|
13
|
+
assert((payload as any).android && typeof (payload as any).android.status === 'string')
|
|
14
|
+
assert((payload as any).ios && typeof (payload as any).ios.status === 'string')
|
|
12
15
|
|
|
13
16
|
const adb = ensureAdbAvailable()
|
|
14
17
|
assert(adb && typeof adb.ok === 'boolean')
|
|
@@ -74,6 +74,7 @@ async function run() {
|
|
|
74
74
|
XCRUN_VERSION_OUTPUT: process.env.XCRUN_VERSION_OUTPUT,
|
|
75
75
|
XCRUN_VERSION_STATUS: process.env.XCRUN_VERSION_STATUS,
|
|
76
76
|
SIMCTL_LIST_OUTPUT: process.env.SIMCTL_LIST_OUTPUT,
|
|
77
|
+
MOBILE_DEBUG_MCP_HOST_OS: process.env.MOBILE_DEBUG_MCP_HOST_OS,
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
try {
|
|
@@ -82,6 +83,7 @@ async function run() {
|
|
|
82
83
|
process.env.XCRUN_PATH = xcrunPath
|
|
83
84
|
|
|
84
85
|
setScenario({
|
|
86
|
+
MOBILE_DEBUG_MCP_HOST_OS: 'darwin',
|
|
85
87
|
ADB_VERSION_STATUS: '0',
|
|
86
88
|
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
87
89
|
ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
|
|
@@ -94,12 +96,16 @@ async function run() {
|
|
|
94
96
|
const healthy = await systemStatus.getSystemStatus()
|
|
95
97
|
assert.strictEqual(healthy.success, true)
|
|
96
98
|
assert.strictEqual((healthy as any).status, 'ready')
|
|
99
|
+
assert.strictEqual((healthy as any).host.supported, true)
|
|
97
100
|
assert.strictEqual(healthy.adbAvailable, true)
|
|
98
101
|
assert.strictEqual(typeof healthy.adbVersion, 'string')
|
|
102
|
+
assert.strictEqual((healthy as any).android.status, 'ready')
|
|
103
|
+
assert.strictEqual((healthy as any).ios.status, 'ready')
|
|
99
104
|
assert.strictEqual((healthy as any).summary.android.ready, true)
|
|
100
105
|
assert.strictEqual(typeof (healthy as any).summary.ios.summary, 'string')
|
|
101
106
|
|
|
102
107
|
setScenario({
|
|
108
|
+
MOBILE_DEBUG_MCP_HOST_OS: 'darwin',
|
|
103
109
|
ADB_VERSION_STATUS: '1',
|
|
104
110
|
ADB_VERSION_OUTPUT: 'not found',
|
|
105
111
|
ADB_DEVICES_OUTPUT: 'List of devices attached\n',
|
|
@@ -112,6 +118,7 @@ async function run() {
|
|
|
112
118
|
assert(missingAdb.issues.some((issue: string) => issue.includes('ADB')))
|
|
113
119
|
|
|
114
120
|
setScenario({
|
|
121
|
+
MOBILE_DEBUG_MCP_HOST_OS: 'darwin',
|
|
115
122
|
ADB_VERSION_STATUS: '0',
|
|
116
123
|
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
117
124
|
ADB_DEVICES_OUTPUT: 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n',
|
|
@@ -125,6 +132,7 @@ async function run() {
|
|
|
125
132
|
assert(unauthorized.issues.some((issue: string) => issue.includes('offline')))
|
|
126
133
|
|
|
127
134
|
setScenario({
|
|
135
|
+
MOBILE_DEBUG_MCP_HOST_OS: 'darwin',
|
|
128
136
|
ADB_VERSION_STATUS: '0',
|
|
129
137
|
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
130
138
|
ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
|
|
@@ -136,6 +144,38 @@ async function run() {
|
|
|
136
144
|
assert.strictEqual(missingXcrun.adbAvailable, true)
|
|
137
145
|
assert.strictEqual(Array.isArray(missingXcrun.issues), true)
|
|
138
146
|
assert.strictEqual((missingXcrun as any).summary.ios.ready, false)
|
|
147
|
+
assert.strictEqual((missingXcrun as any).ios.status, 'unavailable')
|
|
148
|
+
|
|
149
|
+
setScenario({
|
|
150
|
+
MOBILE_DEBUG_MCP_HOST_OS: 'win32',
|
|
151
|
+
ADB_VERSION_STATUS: '0',
|
|
152
|
+
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
153
|
+
ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
|
|
154
|
+
XCRUN_VERSION_STATUS: '1',
|
|
155
|
+
XCRUN_VERSION_OUTPUT: 'not found',
|
|
156
|
+
})
|
|
157
|
+
const windowsHost = await systemStatus.getSystemStatus()
|
|
158
|
+
assert.strictEqual((windowsHost as any).host.os, 'windows')
|
|
159
|
+
assert.strictEqual((windowsHost as any).host.supported, true)
|
|
160
|
+
assert.strictEqual((windowsHost as any).ios.status, 'unsupported')
|
|
161
|
+
assert.deepStrictEqual((windowsHost as any).ios.tools, {})
|
|
162
|
+
assert.deepStrictEqual((windowsHost as any).ios.providers, {})
|
|
163
|
+
assert.deepStrictEqual((windowsHost as any).ios.capabilities, {})
|
|
164
|
+
assert.deepStrictEqual((windowsHost as any).ios.devices, [])
|
|
165
|
+
assert.strictEqual((windowsHost as any).ios.failures[0].code, 'PLATFORM_UNAVAILABLE')
|
|
166
|
+
assert.strictEqual((windowsHost as any).summary.ios.ready, false)
|
|
167
|
+
|
|
168
|
+
setScenario({ MOBILE_DEBUG_MCP_HOST_OS: 'freebsd' })
|
|
169
|
+
const unsupported = await systemStatus.getSystemStatus()
|
|
170
|
+
assert.strictEqual((unsupported as any).host.os, 'freebsd')
|
|
171
|
+
assert.strictEqual((unsupported as any).host.supported, false)
|
|
172
|
+
assert.strictEqual((unsupported as any).android.status, 'unsupported')
|
|
173
|
+
assert.strictEqual((unsupported as any).ios.status, 'unsupported')
|
|
174
|
+
assert.strictEqual((unsupported as any).android.failures[0].code, 'HOST_OS_UNSUPPORTED')
|
|
175
|
+
assert.strictEqual((unsupported as any).ios.failures[0].code, 'HOST_OS_UNSUPPORTED')
|
|
176
|
+
assert.strictEqual(unsupported.adbAvailable, false)
|
|
177
|
+
assert.strictEqual(unsupported.iosAvailable, false)
|
|
178
|
+
assert.strictEqual((unsupported as any).summary.overall, 'blocked')
|
|
139
179
|
|
|
140
180
|
console.log('system_status checks passed')
|
|
141
181
|
} finally {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
import { isIDBInstalled, probeIdb } from '../../../src/utils/cli/idb/idb-helper.js'
|
|
7
|
+
|
|
8
|
+
async function run() {
|
|
9
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-idb-health-'))
|
|
10
|
+
const idbPath = path.join(binDir, 'idb')
|
|
11
|
+
const python3Path = path.join(binDir, 'python3')
|
|
12
|
+
const pythonPath = path.join(binDir, 'python')
|
|
13
|
+
const pythonLog = path.join(binDir, 'python.log')
|
|
14
|
+
const originalIdbPath = process.env.MCP_IDB_PATH
|
|
15
|
+
const originalPath = process.env.PATH
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await fs.writeFile(idbPath, `#!/bin/sh
|
|
19
|
+
if [ "$1" = "list-targets" ] && [ "$2" = "--json" ]; then
|
|
20
|
+
printf '{"targets":[]}\n'
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
if [ "$1" = "--version" ]; then
|
|
24
|
+
printf 'idb: error: unrecognized arguments: --version\n' >&2
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
exit 0
|
|
28
|
+
`, { mode: 0o755 })
|
|
29
|
+
|
|
30
|
+
await fs.writeFile(python3Path, `#!/bin/sh
|
|
31
|
+
printf 'python3 called\\n' >> "${pythonLog}"
|
|
32
|
+
exit 0
|
|
33
|
+
`, { mode: 0o755 })
|
|
34
|
+
|
|
35
|
+
await fs.writeFile(pythonPath, `#!/bin/sh
|
|
36
|
+
printf 'python called\\n' >> "${pythonLog}"
|
|
37
|
+
exit 0
|
|
38
|
+
`, { mode: 0o755 })
|
|
39
|
+
|
|
40
|
+
process.env.MCP_IDB_PATH = idbPath
|
|
41
|
+
process.env.PATH = `${binDir}${path.delimiter}${originalPath || ''}`
|
|
42
|
+
|
|
43
|
+
const probe = probeIdb(idbPath)
|
|
44
|
+
assert.strictEqual(probe.ok, true)
|
|
45
|
+
assert.strictEqual(isIDBInstalled(), true)
|
|
46
|
+
assert(probe.raw === null || typeof probe.raw === 'string')
|
|
47
|
+
|
|
48
|
+
await fs.rm(pythonLog, { force: true }).catch(() => {})
|
|
49
|
+
const failedIdbPath = path.join(binDir, 'idb-failing')
|
|
50
|
+
await fs.writeFile(failedIdbPath, `#!/bin/sh
|
|
51
|
+
if [ "$1" = "list-targets" ] && [ "$2" = "--json" ]; then
|
|
52
|
+
printf 'idb unavailable\\n' >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
exit 2
|
|
56
|
+
`, { mode: 0o755 })
|
|
57
|
+
|
|
58
|
+
const failureProbe = probeIdb(failedIdbPath)
|
|
59
|
+
assert.strictEqual(failureProbe.ok, false)
|
|
60
|
+
assert.strictEqual(await fs.readFile(pythonLog, 'utf8').catch(() => ''), '')
|
|
61
|
+
|
|
62
|
+
console.log('idb health probe test passed')
|
|
63
|
+
} finally {
|
|
64
|
+
if (originalIdbPath === undefined) delete process.env.MCP_IDB_PATH
|
|
65
|
+
else process.env.MCP_IDB_PATH = originalIdbPath
|
|
66
|
+
|
|
67
|
+
if (originalPath === undefined) delete process.env.PATH
|
|
68
|
+
else process.env.PATH = originalPath
|
|
69
|
+
|
|
70
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|