mobile-debug-mcp 0.12.0 → 0.12.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/README.md +2 -10
- package/dist/android/diagnostics.js +1 -24
- package/dist/android/manage.js +1 -1
- package/dist/cli/idb/check-idb.js +84 -0
- package/dist/cli/idb/idb-helper.js +91 -0
- package/{scripts → dist/cli/idb}/install-idb.js +12 -18
- package/dist/cli/ios/preflight-ios.js +155 -0
- package/dist/cli/ios/run-ios-smoke.js +28 -0
- package/dist/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/ios/manage.js +161 -24
- package/dist/tools/interact.js +1 -1
- package/dist/tools/manage.js +1 -1
- package/dist/tools/observe.js +1 -1
- package/dist/utils/diagnostics.js +24 -0
- package/dist/utils/resolve-device.js +62 -0
- package/docs/CHANGELOG.md +9 -0
- package/eslint.config.js +2 -3
- package/package.json +5 -3
- package/src/android/manage.ts +1 -1
- package/{scripts → src/cli/idb}/check-idb.ts +4 -4
- package/{scripts → src/cli/idb}/idb-helper.ts +0 -1
- package/{scripts → src/cli/idb}/install-idb.ts +3 -3
- package/src/cli/ios/preflight-ios.ts +144 -0
- package/{scripts → src/cli/ios}/run-ios-smoke.ts +2 -2
- package/{scripts → src/cli/ios}/run-ios-ui-tree-tap.ts +2 -2
- package/src/ios/manage.ts +169 -22
- package/src/tools/interact.ts +1 -1
- package/src/tools/manage.ts +1 -1
- package/src/tools/observe.ts +1 -1
- package/src/utils/diagnostics.ts +24 -0
- package/src/{resolve-device.ts → utils/resolve-device.ts} +3 -11
- package/scripts/check-idb.js +0 -83
- package/src/android/diagnostics.ts +0 -23
package/src/ios/manage.ts
CHANGED
|
@@ -1,44 +1,191 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
|
-
import { spawn } from "child_process"
|
|
2
|
+
import { spawn, spawnSync } from "child_process"
|
|
3
3
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, InstallAppResponse } from "../types.js"
|
|
4
4
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "./utils.js"
|
|
5
5
|
import path from "path"
|
|
6
6
|
|
|
7
7
|
export class iOSManage {
|
|
8
|
-
async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string }> {
|
|
8
|
+
async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string, diagnostics?: any }> {
|
|
9
9
|
void _variant
|
|
10
10
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
|
|
12
|
+
async function findProject(root: string, maxDepth = 3): Promise<{ dir: string, workspace?: string, proj?: string } | null> {
|
|
13
|
+
try {
|
|
14
|
+
const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
|
|
15
|
+
for (const e of ents) {
|
|
16
|
+
// .xcworkspace and .xcodeproj are directories on disk (bundles), not regular files
|
|
17
|
+
if (e.name.endsWith('.xcworkspace')) return { dir: root, workspace: e.name }
|
|
18
|
+
if (e.name.endsWith('.xcodeproj')) return { dir: root, proj: e.name }
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
|
|
22
|
+
if (maxDepth <= 0) return null
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
|
|
26
|
+
for (const e of ents) {
|
|
27
|
+
if (e.isDirectory()) {
|
|
28
|
+
const candidate = await findProject(path.join(root, e.name), maxDepth - 1)
|
|
29
|
+
if (candidate) return candidate
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve projectPath to an absolute path to avoid cwd-relative resolution issues
|
|
38
|
+
const absProjectPath = path.resolve(projectPath)
|
|
39
|
+
const projectInfo = await findProject(absProjectPath, 3)
|
|
40
|
+
if (!projectInfo) return { error: 'No Xcode project or workspace found' }
|
|
41
|
+
const projectRootDir = projectInfo.dir || absProjectPath
|
|
42
|
+
const workspace = projectInfo.workspace
|
|
43
|
+
const proj = projectInfo.proj
|
|
44
|
+
|
|
45
|
+
// Determine destination: prefer explicit env var, otherwise use booted simulator UDID
|
|
46
|
+
let destinationUDID = process.env.MCP_XCODE_DESTINATION_UDID || process.env.MCP_XCODE_DESTINATION || ''
|
|
47
|
+
if (!destinationUDID) {
|
|
48
|
+
try {
|
|
49
|
+
const meta = await getIOSDeviceMetadata('booted')
|
|
50
|
+
if (meta && meta.id) destinationUDID = meta.id
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Determine xcode command early so it can be used when detecting schemes
|
|
55
|
+
const xcodeCmd = process.env.XCODEBUILD_PATH || 'xcodebuild'
|
|
56
|
+
|
|
57
|
+
// Determine available schemes by querying xcodebuild -list rather than guessing
|
|
58
|
+
async function detectScheme(xcodeCmdInner: string, workspacePath?: string, projectPathFull?: string, cwd?: string): Promise<string | null> {
|
|
59
|
+
try {
|
|
60
|
+
const args = workspacePath ? ['-list', '-workspace', workspacePath] : ['-list', '-project', projectPathFull!]
|
|
61
|
+
// Run xcodebuild directly to list schemes
|
|
62
|
+
const res = spawnSync(xcodeCmdInner, args, { cwd: cwd || projectRootDir, encoding: 'utf8', timeout: 20000 })
|
|
63
|
+
const out = res.stdout || ''
|
|
64
|
+
const schemesMatch = out.match(/Schemes:\s*\n([\s\S]*?)(?:\n\n|$)/m)
|
|
65
|
+
if (schemesMatch) {
|
|
66
|
+
const block = schemesMatch[1]
|
|
67
|
+
const schemes = block.split(/\n/).map(s => s.trim()).filter(Boolean)
|
|
68
|
+
if (schemes.length) return schemes[0]
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
15
73
|
|
|
16
74
|
let buildArgs: string[]
|
|
75
|
+
let chosenScheme: string | null = null
|
|
17
76
|
if (workspace) {
|
|
18
|
-
const workspacePath = path.join(
|
|
19
|
-
|
|
77
|
+
const workspacePath = path.join(projectRootDir, workspace)
|
|
78
|
+
chosenScheme = await detectScheme(xcodeCmd, workspacePath, undefined, projectRootDir)
|
|
79
|
+
const scheme = chosenScheme || workspace.replace(/\.xcworkspace$/, '')
|
|
20
80
|
buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build']
|
|
21
81
|
} else {
|
|
22
|
-
const projectPathFull = path.join(
|
|
23
|
-
|
|
82
|
+
const projectPathFull = path.join(projectRootDir, proj!)
|
|
83
|
+
chosenScheme = await detectScheme(xcodeCmd, undefined, projectPathFull, projectRootDir)
|
|
84
|
+
const scheme = chosenScheme || proj!.replace(/\.xcodeproj$/, '')
|
|
24
85
|
buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build']
|
|
25
86
|
}
|
|
26
87
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
88
|
+
// If we have a destination UDID, add an explicit destination to avoid xcodebuild picking an ambiguous target
|
|
89
|
+
if (destinationUDID) {
|
|
90
|
+
buildArgs.push('-destination', `platform=iOS Simulator,id=${destinationUDID}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add result bundle path for diagnostics
|
|
94
|
+
const resultsDir = path.join(projectPath, 'build-results')
|
|
95
|
+
// Remove any stale results to avoid xcodebuild complaining about existing result bundles
|
|
96
|
+
await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => {})
|
|
97
|
+
await fs.mkdir(resultsDir, { recursive: true }).catch(() => {})
|
|
98
|
+
// Skip specifying -resultBundlePath to avoid platform-specific collisions; rely on stdout/stderr logs
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
const XCODEBUILD_TIMEOUT = parseInt(process.env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000 // default 3 minutes
|
|
102
|
+
const MAX_RETRIES = parseInt(process.env.MCP_XCODEBUILD_RETRIES || '', 10) || 1
|
|
103
|
+
|
|
104
|
+
const tries = MAX_RETRIES + 1
|
|
105
|
+
let lastStdout = ''
|
|
106
|
+
let lastStderr = ''
|
|
107
|
+
let lastErr: any = null
|
|
108
|
+
|
|
109
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
110
|
+
// Run xcodebuild with a watchdog
|
|
111
|
+
const res = await new Promise<{ code: number | null, stdout: string, stderr: string, killedByWatchdog?: boolean }>((resolve) => {
|
|
112
|
+
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir })
|
|
113
|
+
let stdout = ''
|
|
114
|
+
let stderr = ''
|
|
115
|
+
|
|
116
|
+
proc.stdout?.on('data', d => stdout += d.toString())
|
|
117
|
+
proc.stderr?.on('data', d => stderr += d.toString())
|
|
118
|
+
|
|
119
|
+
let killed = false
|
|
120
|
+
const to = setTimeout(() => {
|
|
121
|
+
killed = true
|
|
122
|
+
try { proc.kill('SIGKILL') } catch {}
|
|
123
|
+
}, XCODEBUILD_TIMEOUT)
|
|
124
|
+
|
|
125
|
+
proc.on('close', (code) => {
|
|
126
|
+
clearTimeout(to)
|
|
127
|
+
resolve({ code, stdout, stderr, killedByWatchdog: killed })
|
|
128
|
+
})
|
|
129
|
+
proc.on('error', (err) => {
|
|
130
|
+
clearTimeout(to)
|
|
131
|
+
resolve({ code: null, stdout, stderr: String(err), killedByWatchdog: killed })
|
|
132
|
+
})
|
|
35
133
|
})
|
|
36
|
-
proc.on('error', err => reject(err))
|
|
37
|
-
})
|
|
38
134
|
|
|
135
|
+
lastStdout = res.stdout
|
|
136
|
+
lastStderr = res.stderr
|
|
137
|
+
|
|
138
|
+
if (res.code === 0) {
|
|
139
|
+
// success — clear any previous error and stop retrying
|
|
140
|
+
lastErr = null
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// record the failure for reporting
|
|
145
|
+
lastErr = new Error(res.stderr || `xcodebuild failed with code ${res.code}`)
|
|
146
|
+
// Attach exit code and watchdog info so diagnostics can include them
|
|
147
|
+
;(lastErr as any).code = res.code
|
|
148
|
+
;(lastErr as any).exitCode = res.code
|
|
149
|
+
;(lastErr as any).killedByWatchdog = !!res.killedByWatchdog
|
|
150
|
+
|
|
151
|
+
// write logs for diagnostics (helpful whether killed or not)
|
|
152
|
+
try {
|
|
153
|
+
await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stdout.log`), res.stdout).catch(() => {})
|
|
154
|
+
await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stderr.log`), res.stderr).catch(() => {})
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
// If killed by watchdog and there are remaining attempts, continue to retry
|
|
158
|
+
if (res.killedByWatchdog && attempt < tries) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// no more retries or not a watchdog kill — break to report lastErr
|
|
163
|
+
if (attempt >= tries) break
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (lastErr) {
|
|
167
|
+
// Include diagnostics and result bundle path when available; provide structured info useful for agents
|
|
168
|
+
const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`
|
|
169
|
+
const envSnapshot = { PATH: process.env.PATH }
|
|
170
|
+
return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: (lastErr as any).code || null, invokedCommand, cwd: projectRootDir, envSnapshot } }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Try to locate built .app. First search project tree, then DerivedData if necessary
|
|
39
174
|
const built = await findAppBundle(projectPath)
|
|
40
|
-
if (
|
|
41
|
-
|
|
175
|
+
if (built) return { artifactPath: built }
|
|
176
|
+
|
|
177
|
+
// Fallback: search DerivedData for matching product
|
|
178
|
+
const dd = path.join(process.env.HOME || '', 'Library', 'Developer', 'Xcode', 'DerivedData')
|
|
179
|
+
try {
|
|
180
|
+
const entries = await fs.readdir(dd).catch(() => [])
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
const candidate = path.join(dd, e)
|
|
183
|
+
const found = await findAppBundle(candidate).catch(() => undefined)
|
|
184
|
+
if (found) return { artifactPath: found }
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
|
|
188
|
+
return { error: 'Could not find .app after build' }
|
|
42
189
|
} catch (e) {
|
|
43
190
|
return { error: e instanceof Error ? e.message : String(e) }
|
|
44
191
|
}
|
package/src/tools/interact.ts
CHANGED
package/src/tools/manage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { resolveTargetDevice, listDevices } from '../resolve-device.js'
|
|
3
|
+
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js'
|
|
4
4
|
import { AndroidManage } from '../android/manage.js'
|
|
5
5
|
import { iOSManage } from '../ios/manage.js'
|
|
6
6
|
import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
package/src/tools/observe.ts
CHANGED
package/src/utils/diagnostics.ts
CHANGED
|
@@ -34,3 +34,27 @@ export class DiagnosticError extends Error {
|
|
|
34
34
|
this.runResult = runResult
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
// Exec ADB with diagnostics — moved from src/android/diagnostics.ts
|
|
39
|
+
import { spawnSync } from 'child_process'
|
|
40
|
+
import { getAdbCmd } from '../android/utils.js'
|
|
41
|
+
|
|
42
|
+
export function execAdbWithDiagnostics(args: string[], deviceId?: string) {
|
|
43
|
+
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args
|
|
44
|
+
const timeout = 120000
|
|
45
|
+
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout })
|
|
46
|
+
const runResult: RunResult = {
|
|
47
|
+
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
48
|
+
stdout: res.stdout || '',
|
|
49
|
+
stderr: res.stderr || '',
|
|
50
|
+
envSnapshot: makeEnvSnapshot(['PATH','ADB_PATH','HOME','JAVA_HOME']),
|
|
51
|
+
command: getAdbCmd(),
|
|
52
|
+
args: adbArgs,
|
|
53
|
+
suggestedFixes: []
|
|
54
|
+
}
|
|
55
|
+
if (res.status !== 0) {
|
|
56
|
+
if ((runResult.stderr || '').includes('device not found')) runResult.suggestedFixes!.push('Ensure device is connected and adb is authorized (adb devices)')
|
|
57
|
+
if ((runResult.stderr || '').includes('No such file or directory')) runResult.suggestedFixes!.push('Verify ADB_PATH or that adb is installed')
|
|
58
|
+
}
|
|
59
|
+
return { runResult }
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DeviceInfo } from "
|
|
2
|
-
import { listAndroidDevices } from "
|
|
3
|
-
import { listIOSDevices } from "
|
|
1
|
+
import { DeviceInfo } from "../types.js"
|
|
2
|
+
import { listAndroidDevices } from "../android/utils.js"
|
|
3
|
+
import { listIOSDevices } from "../ios/utils.js"
|
|
4
4
|
|
|
5
5
|
export interface ResolveOptions {
|
|
6
6
|
platform: "android" | "ios"
|
|
@@ -11,7 +11,6 @@ export interface ResolveOptions {
|
|
|
11
11
|
|
|
12
12
|
function parseNumericVersion(v: string): number {
|
|
13
13
|
if (!v) return 0
|
|
14
|
-
// extract first number groups like 17.0 -> 17.0 or Android 12 -> 12
|
|
15
14
|
const m = v.match(/(\d+)(?:[\.\-](\d+))?/)
|
|
16
15
|
if (!m) return 0
|
|
17
16
|
const major = parseInt(m[1], 10) || 0
|
|
@@ -23,7 +22,6 @@ export async function listDevices(platform?: "android" | "ios", appId?: string):
|
|
|
23
22
|
if (!platform || platform === "android") {
|
|
24
23
|
const android = await listAndroidDevices(appId)
|
|
25
24
|
if (platform === "android") return android
|
|
26
|
-
// if no platform specified, merge with ios below
|
|
27
25
|
const ios = await listIOSDevices(appId)
|
|
28
26
|
return [...android, ...ios]
|
|
29
27
|
}
|
|
@@ -42,11 +40,9 @@ export async function resolveTargetDevice(opts: ResolveOptions): Promise<DeviceI
|
|
|
42
40
|
|
|
43
41
|
let candidates = devices.slice()
|
|
44
42
|
|
|
45
|
-
// Apply prefer filter
|
|
46
43
|
if (prefer === "physical") candidates = candidates.filter(d => !d.simulator)
|
|
47
44
|
if (prefer === "emulator") candidates = candidates.filter(d => d.simulator)
|
|
48
45
|
|
|
49
|
-
// If appId provided, prefer devices with appInstalled
|
|
50
46
|
if (appId) {
|
|
51
47
|
const installed = candidates.filter(d => (d as any).appInstalled)
|
|
52
48
|
if (installed.length > 0) candidates = installed
|
|
@@ -55,21 +51,17 @@ export async function resolveTargetDevice(opts: ResolveOptions): Promise<DeviceI
|
|
|
55
51
|
if (candidates.length === 1) return candidates[0]
|
|
56
52
|
|
|
57
53
|
if (candidates.length > 1) {
|
|
58
|
-
// Prefer physical over emulator unless prefer=emulator
|
|
59
54
|
if (!prefer) {
|
|
60
55
|
const physical = candidates.filter(d => !d.simulator)
|
|
61
56
|
if (physical.length === 1) return physical[0]
|
|
62
57
|
if (physical.length > 1) candidates = physical
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
// Pick highest OS version
|
|
66
60
|
candidates.sort((a, b) => parseNumericVersion(b.osVersion) - parseNumericVersion(a.osVersion))
|
|
67
|
-
// If top is unique (numeric differs), return it
|
|
68
61
|
if (candidates.length > 1 && parseNumericVersion(candidates[0].osVersion) > parseNumericVersion(candidates[1].osVersion)) {
|
|
69
62
|
return candidates[0]
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
// Ambiguous: throw an error with candidate list so caller (agent) can present choices
|
|
73
65
|
const list = candidates.map(d => ({ id: d.id, platform: d.platform, osVersion: d.osVersion, model: d.model, simulator: d.simulator, appInstalled: (d as any).appInstalled }))
|
|
74
66
|
const err = new Error(`Multiple matching devices found: ${JSON.stringify(list, null, 2)}`)
|
|
75
67
|
;(err as any).devices = list
|
package/scripts/check-idb.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execSync, spawnSync } from 'child_process';
|
|
3
|
-
import { main as installMain } from './install-idb';
|
|
4
|
-
function which(cmd) {
|
|
5
|
-
try {
|
|
6
|
-
// Prefer POSIX `command -v` which can resolve shell builtins. Use spawnSync
|
|
7
|
-
// to avoid shell interpolation and injection risks. Fall back to `which`.
|
|
8
|
-
const res = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
9
|
-
if (res && res.status === 0 && res.stdout) return res.stdout.toString().trim();
|
|
10
|
-
return execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function print(...args) {
|
|
17
|
-
console.log(...args);
|
|
18
|
-
}
|
|
19
|
-
async function runInstaller() {
|
|
20
|
-
try {
|
|
21
|
-
// prefer invoking the TS script via npx/tsx to ensure environment
|
|
22
|
-
const runner = which('npx') ? 'npx' : which('tsx') ? 'tsx' : null;
|
|
23
|
-
if (runner) {
|
|
24
|
-
const args = runner === 'npx' ? ['tsx', './scripts/install-idb.ts'] : ['./scripts/install-idb.ts'];
|
|
25
|
-
const res = spawnSync(runner, args, { stdio: 'inherit' });
|
|
26
|
-
return typeof res.status === 'number' ? res.status === 0 : false;
|
|
27
|
-
}
|
|
28
|
-
// fallback: attempt to import and run the installer directly (may rely on ts-node/tsx)
|
|
29
|
-
try {
|
|
30
|
-
// call the exported main; it returns a promise
|
|
31
|
-
await installMain();
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
console.error('Failed to run installer:', e instanceof Error ? e.message : String(e));
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
(async () => {
|
|
44
|
-
try {
|
|
45
|
-
print('PATH=', process.env.PATH);
|
|
46
|
-
const idb = process.env.IDB_PATH || which('idb');
|
|
47
|
-
print('which idb:', idb);
|
|
48
|
-
if (idb) {
|
|
49
|
-
try {
|
|
50
|
-
print('idb --version:', execSync('idb --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
print('idb --version: (failed)', e instanceof Error ? e.message : String(e));
|
|
54
|
-
}
|
|
55
|
-
const companion = which('idb_companion');
|
|
56
|
-
print('which idb_companion:', companion);
|
|
57
|
-
if (companion)
|
|
58
|
-
try {
|
|
59
|
-
print('idb_companion --version:', execSync('idb_companion --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
60
|
-
}
|
|
61
|
-
catch (e) {
|
|
62
|
-
print('idb_companion --version: (failed)', e instanceof Error ? e.message : String(e));
|
|
63
|
-
}
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
print('idb not found');
|
|
67
|
-
const auto = process.env.MCP_AUTO_INSTALL_IDB === 'true';
|
|
68
|
-
if (auto) {
|
|
69
|
-
print('MCP_AUTO_INSTALL_IDB=true, attempting installer...');
|
|
70
|
-
const ok = await runInstaller();
|
|
71
|
-
if (ok)
|
|
72
|
-
process.exit(0);
|
|
73
|
-
print('Installer failed or did not produce idb');
|
|
74
|
-
process.exit(2);
|
|
75
|
-
}
|
|
76
|
-
print('Set MCP_AUTO_INSTALL_IDB=true to attempt automatic installation (CI-friendly).');
|
|
77
|
-
process.exit(2);
|
|
78
|
-
}
|
|
79
|
-
catch (e) {
|
|
80
|
-
console.error('idb healthcheck failed:', e instanceof Error ? e.message : String(e));
|
|
81
|
-
process.exit(2);
|
|
82
|
-
}
|
|
83
|
-
})();
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process'
|
|
2
|
-
import { getAdbCmd } from './utils.js'
|
|
3
|
-
import { RunResult, makeEnvSnapshot } from '../utils/diagnostics.js'
|
|
4
|
-
|
|
5
|
-
export function execAdbWithDiagnostics(args: string[], deviceId?: string) {
|
|
6
|
-
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args
|
|
7
|
-
const timeout = 120000
|
|
8
|
-
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout }) as any
|
|
9
|
-
const runResult: RunResult = {
|
|
10
|
-
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
11
|
-
stdout: res.stdout || '',
|
|
12
|
-
stderr: res.stderr || '',
|
|
13
|
-
envSnapshot: makeEnvSnapshot(['PATH','ADB_PATH','HOME','JAVA_HOME']),
|
|
14
|
-
command: getAdbCmd(),
|
|
15
|
-
args: adbArgs,
|
|
16
|
-
suggestedFixes: []
|
|
17
|
-
}
|
|
18
|
-
if (res.status !== 0) {
|
|
19
|
-
if ((runResult.stderr || '').includes('device not found')) runResult.suggestedFixes!.push('Ensure device is connected and adb is authorized (adb devices)')
|
|
20
|
-
if ((runResult.stderr || '').includes('No such file or directory')) runResult.suggestedFixes!.push('Verify ADB_PATH or that adb is installed')
|
|
21
|
-
}
|
|
22
|
-
return { runResult }
|
|
23
|
-
}
|