mobile-debug-mcp 0.8.0 → 0.9.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 +18 -526
- package/dist/android/interact.js +86 -2
- package/dist/android/utils.js +95 -10
- package/dist/ios/interact.js +87 -20
- package/dist/server.js +59 -287
- package/dist/tools/app.js +45 -0
- package/dist/tools/devices.js +5 -0
- package/dist/tools/install.js +47 -0
- package/dist/tools/logs.js +62 -0
- package/dist/tools/screenshot.js +17 -0
- package/dist/tools/ui.js +57 -0
- package/docs/CHANGELOG.md +9 -0
- package/docs/TOOLS.md +272 -0
- package/package.json +1 -1
- package/src/android/interact.ts +89 -2
- package/src/android/utils.ts +83 -8
- package/src/ios/interact.ts +90 -24
- package/src/server.ts +76 -375
- package/src/tools/app.ts +46 -0
- package/src/tools/devices.ts +6 -0
- package/src/tools/install.ts +43 -0
- package/src/tools/logs.ts +62 -0
- package/src/tools/screenshot.ts +18 -0
- package/src/tools/ui.ts +62 -0
- package/test/integration/install.integration.ts +64 -0
- package/test/integration/run-install-android.ts +21 -0
- package/test/integration/run-install-ios.ts +21 -0
- package/test/integration/test-dist.ts +41 -0
- package/test/unit/index.ts +1 -0
- package/test/unit/install.test.ts +82 -0
package/src/ios/interact.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { spawn } from "child_process"
|
|
|
3
3
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, WaitForElementResponse, TapResponse } from "../types.js"
|
|
4
4
|
import { execCommand, getIOSDeviceMetadata, validateBundleId, IDB } from "./utils.js"
|
|
5
5
|
import { iOSObserve } from "./observe.js"
|
|
6
|
+
import path from "path"
|
|
7
|
+
import { existsSync } from "fs"
|
|
6
8
|
|
|
7
9
|
export class iOSInteract {
|
|
8
10
|
private observe = new iOSObserve();
|
|
@@ -82,37 +84,101 @@ export class iOSInteract {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
async installApp(appPath: string, deviceId: string = "booted"): Promise<import("../types.js").InstallAppResponse> {
|
|
85
|
-
// Try simulator install first
|
|
86
87
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
88
|
+
|
|
89
|
+
// Helper to find .app bundles under a directory
|
|
90
|
+
async function findAppBundle(dir: string): Promise<string | undefined> {
|
|
91
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
const full = path.join(dir, e.name)
|
|
94
|
+
if (e.isDirectory()) {
|
|
95
|
+
if (full.endsWith('.app')) return full
|
|
96
|
+
const found = await findAppBundle(full)
|
|
97
|
+
if (found) return found
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
87
103
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
104
|
+
let toInstall = appPath
|
|
105
|
+
|
|
106
|
+
const stat = await fs.stat(appPath).catch(() => null)
|
|
107
|
+
if (stat && stat.isDirectory()) {
|
|
108
|
+
// If directory already contains a .app, use it
|
|
109
|
+
const found = await findAppBundle(appPath)
|
|
110
|
+
if (found) {
|
|
111
|
+
toInstall = found
|
|
112
|
+
} else {
|
|
113
|
+
// Attempt to locate an Xcode project and build for simulator
|
|
114
|
+
const files = await fs.readdir(appPath).catch(() => [])
|
|
115
|
+
// Prefer workspace when present (CocoaPods / multi-project setups)
|
|
116
|
+
const workspace = files.find(f => f.endsWith('.xcworkspace'))
|
|
117
|
+
const proj = files.find(f => f.endsWith('.xcodeproj'))
|
|
118
|
+
if (!workspace && !proj) throw new Error('No .app bundle, .xcworkspace or .xcodeproj found in directory')
|
|
119
|
+
|
|
120
|
+
let buildArgs: string[]
|
|
121
|
+
let scheme: string
|
|
122
|
+
if (workspace) {
|
|
123
|
+
const workspacePath = path.join(appPath, workspace)
|
|
124
|
+
scheme = workspace.replace(/\.xcworkspace$/, '')
|
|
125
|
+
buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build', '-quiet']
|
|
126
|
+
} else {
|
|
127
|
+
const projectPath = path.join(appPath, proj!)
|
|
128
|
+
scheme = proj!.replace(/\.xcodeproj$/, '')
|
|
129
|
+
buildArgs = ['-project', projectPath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build', '-quiet']
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
await new Promise<void>((resolve, reject) => {
|
|
101
|
-
const proc = spawn(
|
|
102
|
-
let stderr = ''
|
|
103
|
-
proc.stderr
|
|
133
|
+
const proc = spawn('xcodebuild', buildArgs, { cwd: appPath })
|
|
134
|
+
let stderr = ''
|
|
135
|
+
proc.stderr?.on('data', d => stderr += d.toString())
|
|
104
136
|
proc.on('close', code => {
|
|
105
|
-
if (code === 0) resolve()
|
|
106
|
-
else reject(new Error(stderr || `
|
|
107
|
-
})
|
|
108
|
-
proc.on('error', err => reject(err))
|
|
109
|
-
})
|
|
110
|
-
|
|
137
|
+
if (code === 0) resolve()
|
|
138
|
+
else reject(new Error(stderr || `xcodebuild failed with code ${code}`))
|
|
139
|
+
})
|
|
140
|
+
proc.on('error', err => reject(err))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const built = await findAppBundle(appPath)
|
|
144
|
+
if (!built) throw new Error('Could not locate built .app after xcodebuild')
|
|
145
|
+
toInstall = built
|
|
111
146
|
}
|
|
112
|
-
} catch (inner) {
|
|
113
|
-
// fallthrough
|
|
114
147
|
}
|
|
115
148
|
|
|
149
|
+
// Try simulator install first
|
|
150
|
+
try {
|
|
151
|
+
const res = await execCommand(['simctl', 'install', deviceId, toInstall], deviceId)
|
|
152
|
+
return { device, installed: true, output: res.output }
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// If simctl fails and idb is available, try idb install for physical devices
|
|
155
|
+
try {
|
|
156
|
+
const child = spawn(IDB, ['--version'])
|
|
157
|
+
const idbExists = await new Promise<boolean>((resolve) => {
|
|
158
|
+
child.on('error', () => resolve(false));
|
|
159
|
+
child.on('close', (code) => resolve(code === 0));
|
|
160
|
+
});
|
|
161
|
+
if (idbExists) {
|
|
162
|
+
// Use idb to install (works for physical devices and simulators)
|
|
163
|
+
await new Promise<void>((resolve, reject) => {
|
|
164
|
+
const proc = spawn(IDB, ['install', toInstall, '--udid', device.id]);
|
|
165
|
+
let stderr = '';
|
|
166
|
+
proc.stderr.on('data', d => stderr += d.toString());
|
|
167
|
+
proc.on('close', code => {
|
|
168
|
+
if (code === 0) resolve();
|
|
169
|
+
else reject(new Error(stderr || `idb install failed with code ${code}`));
|
|
170
|
+
});
|
|
171
|
+
proc.on('error', err => reject(err));
|
|
172
|
+
});
|
|
173
|
+
return { device, installed: true }
|
|
174
|
+
}
|
|
175
|
+
} catch (inner) {
|
|
176
|
+
// fallthrough
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { device, installed: false, error: e instanceof Error ? e.message : String(e) }
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
116
182
|
return { device, installed: false, error: e instanceof Error ? e.message : String(e) }
|
|
117
183
|
}
|
|
118
184
|
}
|