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.
@@ -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
- const res = await execCommand(['simctl', 'install', deviceId, appPath], deviceId)
89
- return { device, installed: true, output: res.output }
90
- } catch (e) {
91
- // If simctl fails and idb is available, try idb install for physical devices
92
- try {
93
- const child = spawn(IDB, ['--version'])
94
- const idbExists = await new Promise<boolean>((resolve) => {
95
- child.on('error', () => resolve(false));
96
- child.on('close', (code) => resolve(code === 0));
97
- });
98
- if (idbExists) {
99
- // Use idb to install (works for physical devices and simulators)
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(IDB, ['install', appPath, '--udid', device.id]);
102
- let stderr = '';
103
- proc.stderr.on('data', d => stderr += d.toString());
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 || `idb install failed with code ${code}`));
107
- });
108
- proc.on('error', err => reject(err));
109
- });
110
- return { device, installed: true }
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
  }