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/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Mobile Dev
|
|
1
|
+
# Mobile Dev Tools
|
|
2
2
|
|
|
3
3
|
A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
|
|
4
4
|
|
|
5
|
-
> **Note:** iOS support is currently
|
|
5
|
+
> **Note:** iOS support is limited currently, Please use with caution and report any issues.
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
@@ -40,11 +40,3 @@ I have a crash on the app, can you diagnose it, fix and validate using the mcp t
|
|
|
40
40
|
|
|
41
41
|
MIT
|
|
42
42
|
|
|
43
|
-
## IDB/ADB healthcheck and diagnostics
|
|
44
|
-
|
|
45
|
-
The agent provides healthcheck and optional auto-install scripts for iOS (idb) and Android (adb).
|
|
46
|
-
|
|
47
|
-
- Run `npm run healthcheck` to verify idb is available. Set `MCP_AUTO_INSTALL_IDB=true` to allow the installer to run in CI or non-interactive environments.
|
|
48
|
-
- Override detection with `IDB_PATH` or `ADB_PATH` environment variables.
|
|
49
|
-
- Tools now return structured diagnostics on failures: { exitCode, stdout, stderr, command, args, envSnapshot, suggestedFixes } which helps agents decide corrective actions.
|
|
50
|
-
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { getAdbCmd } from './utils.js';
|
|
3
|
-
import { makeEnvSnapshot } from '../utils/diagnostics.js';
|
|
4
|
-
export function execAdbWithDiagnostics(args, deviceId) {
|
|
5
|
-
const adbArgs = deviceId ? ['-s', deviceId, ...args] : args;
|
|
6
|
-
const timeout = 120000;
|
|
7
|
-
const res = spawnSync(getAdbCmd(), adbArgs, { encoding: 'utf8', timeout });
|
|
8
|
-
const runResult = {
|
|
9
|
-
exitCode: typeof res.status === 'number' ? res.status : null,
|
|
10
|
-
stdout: res.stdout || '',
|
|
11
|
-
stderr: res.stderr || '',
|
|
12
|
-
envSnapshot: makeEnvSnapshot(['PATH', 'ADB_PATH', 'HOME', 'JAVA_HOME']),
|
|
13
|
-
command: getAdbCmd(),
|
|
14
|
-
args: adbArgs,
|
|
15
|
-
suggestedFixes: []
|
|
16
|
-
};
|
|
17
|
-
if (res.status !== 0) {
|
|
18
|
-
if ((runResult.stderr || '').includes('device not found'))
|
|
19
|
-
runResult.suggestedFixes.push('Ensure device is connected and adb is authorized (adb devices)');
|
|
20
|
-
if ((runResult.stderr || '').includes('No such file or directory'))
|
|
21
|
-
runResult.suggestedFixes.push('Verify ADB_PATH or that adb is installed');
|
|
22
|
-
}
|
|
23
|
-
return { runResult };
|
|
24
|
-
}
|
|
1
|
+
export { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
package/dist/android/manage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from './utils.js';
|
|
6
|
-
import { execAdbWithDiagnostics } from '
|
|
6
|
+
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
8
|
export class AndroidManage {
|
|
9
9
|
async build(projectPath, _variant) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process';
|
|
3
|
+
import { main as installMain } from './install-idb.js';
|
|
4
|
+
import { getIdbCmd, isIDBInstalled } from './idb-helper.js';
|
|
5
|
+
function which(cmd) {
|
|
6
|
+
try {
|
|
7
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
8
|
+
if (r && r.status === 0 && r.stdout)
|
|
9
|
+
return r.stdout.toString().trim();
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
try {
|
|
13
|
+
return execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function print(...args) {
|
|
20
|
+
console.log(...args);
|
|
21
|
+
}
|
|
22
|
+
async function runInstaller() {
|
|
23
|
+
try {
|
|
24
|
+
// prefer invoking the TS script via npx/tsx to ensure environment
|
|
25
|
+
const runner = which('npx') ? 'npx' : which('tsx') ? 'tsx' : null;
|
|
26
|
+
if (runner) {
|
|
27
|
+
const args = runner === 'npx' ? ['tsx', './src/cli/idb/install-idb.ts'] : ['./src/cli/idb/install-idb.ts'];
|
|
28
|
+
const res = spawnSync(runner, args, { stdio: 'inherit' });
|
|
29
|
+
return typeof res.status === 'number' ? res.status === 0 : false;
|
|
30
|
+
}
|
|
31
|
+
// fallback: attempt to import and run the installer directly (may rely on ts-node/tsx)
|
|
32
|
+
try {
|
|
33
|
+
// call the exported main; it returns a promise
|
|
34
|
+
await installMain();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error('Failed to run installer:', e instanceof Error ? e.message : String(e));
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
print('PATH=', process.env.PATH);
|
|
48
|
+
const idb = process.env.IDB_PATH || getIdbCmd();
|
|
49
|
+
print('idb:', idb);
|
|
50
|
+
if (idb && isIDBInstalled()) {
|
|
51
|
+
try {
|
|
52
|
+
print('idb --version:', execSync(`${idb} --version`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
print('idb --version: (failed)', e instanceof Error ? e.message : String(e));
|
|
56
|
+
}
|
|
57
|
+
const companion = which('idb_companion');
|
|
58
|
+
print('which idb_companion:', companion);
|
|
59
|
+
if (companion)
|
|
60
|
+
try {
|
|
61
|
+
print('idb_companion --version:', execSync('idb_companion --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim());
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
print('idb_companion --version: (failed)', e instanceof Error ? e.message : String(e));
|
|
65
|
+
}
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
print('idb not found or not responding');
|
|
69
|
+
const auto = process.env.MCP_AUTO_INSTALL_IDB === 'true';
|
|
70
|
+
if (auto) {
|
|
71
|
+
print('MCP_AUTO_INSTALL_IDB=true, attempting installer...');
|
|
72
|
+
const ok = await runInstaller();
|
|
73
|
+
if (ok)
|
|
74
|
+
process.exit(0);
|
|
75
|
+
print('Installer failed or did not produce idb');
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
print('Set MCP_AUTO_INSTALL_IDB=true to attempt automatic installation (CI-friendly).');
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error('idb healthcheck failed:', e instanceof Error ? e.message : String(e));
|
|
83
|
+
process.exit(2);
|
|
84
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
export function getConfiguredIdbPath() {
|
|
4
|
+
if (process.env.MCP_IDB_PATH)
|
|
5
|
+
return process.env.MCP_IDB_PATH;
|
|
6
|
+
if (process.env.IDB_PATH)
|
|
7
|
+
return process.env.IDB_PATH;
|
|
8
|
+
const cfgPaths = [
|
|
9
|
+
process.env.MCP_CONFIG_PATH || (process.env.HOME ? `${process.env.HOME}/.mcp/config.json` : ''),
|
|
10
|
+
`${process.cwd()}/mcp.config.json`
|
|
11
|
+
];
|
|
12
|
+
for (const p of cfgPaths) {
|
|
13
|
+
if (!p)
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(p)) {
|
|
17
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
18
|
+
const json = JSON.parse(raw);
|
|
19
|
+
if (json) {
|
|
20
|
+
if (json.idbPath)
|
|
21
|
+
return json.idbPath;
|
|
22
|
+
if (json.IDB_PATH)
|
|
23
|
+
return json.IDB_PATH;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function commandWhich(cmd) {
|
|
32
|
+
try {
|
|
33
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
34
|
+
if (r && r.status === 0 && r.stdout)
|
|
35
|
+
return r.stdout.toString().trim();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
try {
|
|
39
|
+
const p = execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
40
|
+
if (p)
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export function getIdbCmd() {
|
|
47
|
+
const cfg = getConfiguredIdbPath();
|
|
48
|
+
if (cfg)
|
|
49
|
+
return cfg;
|
|
50
|
+
if (process.env.IDB_PATH)
|
|
51
|
+
return process.env.IDB_PATH;
|
|
52
|
+
// Prefer command -v/which
|
|
53
|
+
const found = commandWhich('idb');
|
|
54
|
+
if (found)
|
|
55
|
+
return found;
|
|
56
|
+
// Common locations
|
|
57
|
+
const common = [
|
|
58
|
+
process.env.HOME ? `${process.env.HOME}/Library/Python/3.9/bin/idb` : '',
|
|
59
|
+
process.env.HOME ? `${process.env.HOME}/Library/Python/3.10/bin/idb` : '',
|
|
60
|
+
'/opt/homebrew/bin/idb',
|
|
61
|
+
'/usr/local/bin/idb'
|
|
62
|
+
];
|
|
63
|
+
for (const c of common) {
|
|
64
|
+
if (!c)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
execSync(`test -x ${c}`, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
export function isIDBInstalled() {
|
|
75
|
+
const cmd = getIdbCmd();
|
|
76
|
+
if (!cmd)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
// command -v <cmd>
|
|
80
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
81
|
+
if (r && r.status === 0)
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
try {
|
|
86
|
+
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
+
import { getIdbCmd, isIDBInstalled, commandWhich } from './idb-helper.js';
|
|
4
5
|
const IDB_PKG = 'fb-idb';
|
|
5
|
-
function which(cmd) {
|
|
6
|
-
try {
|
|
7
|
-
return execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
6
|
function runCommand(cmd, args) {
|
|
14
7
|
const res = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
15
8
|
return typeof res.status === 'number' ? res.status : 1;
|
|
@@ -28,8 +21,8 @@ async function confirm(prompt) {
|
|
|
28
21
|
async function main() {
|
|
29
22
|
try {
|
|
30
23
|
const idbFromEnv = process.env.IDB_PATH;
|
|
31
|
-
const existing = idbFromEnv ||
|
|
32
|
-
if (existing) {
|
|
24
|
+
const existing = idbFromEnv || getIdbCmd();
|
|
25
|
+
if (existing && isIDBInstalled()) {
|
|
33
26
|
console.log('idb already available at:', existing);
|
|
34
27
|
process.exit(0);
|
|
35
28
|
}
|
|
@@ -45,12 +38,12 @@ async function main() {
|
|
|
45
38
|
console.log('Auto-install enabled (MCP_AUTO_INSTALL_IDB=true or CI=true)');
|
|
46
39
|
}
|
|
47
40
|
const attempts = [];
|
|
48
|
-
if (
|
|
41
|
+
if (commandWhich('pipx'))
|
|
49
42
|
attempts.push({ name: 'pipx', cmd: 'pipx', args: ['install', IDB_PKG] });
|
|
50
|
-
if (
|
|
51
|
-
attempts.push({ name: 'pip', cmd:
|
|
43
|
+
if (commandWhich('pip') || commandWhich('python3'))
|
|
44
|
+
attempts.push({ name: 'pip', cmd: commandWhich('pip') ? 'pip' : 'python3', args: commandWhich('pip') ? ['install', '--user', IDB_PKG] : ['-m', 'pip', 'install', '--user', IDB_PKG] });
|
|
52
45
|
// Add brew as a fallback on macOS if present (best-effort)
|
|
53
|
-
if (process.platform === 'darwin' &&
|
|
46
|
+
if (process.platform === 'darwin' && commandWhich('brew')) {
|
|
54
47
|
attempts.push({ name: 'brew', cmd: 'brew', args: ['install', 'idb'] });
|
|
55
48
|
}
|
|
56
49
|
if (attempts.length === 0) {
|
|
@@ -68,7 +61,7 @@ async function main() {
|
|
|
68
61
|
catch (e) {
|
|
69
62
|
console.warn(`${a.name} install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
70
63
|
}
|
|
71
|
-
const found =
|
|
64
|
+
const found = commandWhich('idb') || commandWhich('command -v idb');
|
|
72
65
|
if (found) {
|
|
73
66
|
console.log('idb installed at:', found);
|
|
74
67
|
process.exit(0);
|
|
@@ -82,7 +75,8 @@ async function main() {
|
|
|
82
75
|
process.exit(2);
|
|
83
76
|
}
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const scriptPath = new URL(import.meta.url).pathname;
|
|
79
|
+
if (scriptPath === process.argv[1]) {
|
|
80
|
+
main().catch(e => { console.error('Installer failed:', e instanceof Error ? e.message : String(e)); process.exit(2); });
|
|
87
81
|
}
|
|
88
82
|
export { main };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { getIdbCmd, isIDBInstalled, commandWhich } from '../idb/idb-helper.js';
|
|
6
|
+
async function exists(p) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.promises.access(p);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function findFirst(root, patterns, maxDepth = 4) {
|
|
16
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
17
|
+
while (queue.length) {
|
|
18
|
+
const { dir, depth } = queue.shift();
|
|
19
|
+
try {
|
|
20
|
+
console.error('DEBUG findFirst: reading dir', dir, 'depth', depth);
|
|
21
|
+
const ents = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
22
|
+
console.error('DEBUG findFirst: entries', ents.map(e => e.name));
|
|
23
|
+
for (const e of ents) {
|
|
24
|
+
const full = path.join(dir, e.name);
|
|
25
|
+
for (const p of patterns) {
|
|
26
|
+
if (e.name.endsWith(p)) {
|
|
27
|
+
console.error('DEBUG findFirst: matched', full);
|
|
28
|
+
return full;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (e.isDirectory() && depth < maxDepth)
|
|
32
|
+
queue.push({ dir: full, depth: depth + 1 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error('DEBUG findFirst: read failed for', dir, err);
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function startCompanionIfNeeded(companionPath, udid) {
|
|
43
|
+
if (!companionPath || !udid)
|
|
44
|
+
return { started: false, error: 'missing companion or udid' };
|
|
45
|
+
try {
|
|
46
|
+
const child = spawn(companionPath, ['--udid', udid], { detached: true, stdio: 'ignore' });
|
|
47
|
+
child.unref();
|
|
48
|
+
return { started: true };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return { started: false, error: e.message };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function main() {
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
let projectArg;
|
|
57
|
+
let udid;
|
|
58
|
+
let startCompanion = false;
|
|
59
|
+
let kmpBuild = null;
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const a = args[i];
|
|
62
|
+
if (a === '--project' && args[i + 1]) {
|
|
63
|
+
projectArg = args[i + 1];
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
else if (a === '--udid' && args[i + 1]) {
|
|
67
|
+
udid = args[i + 1];
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
else if (a === '--start-companion')
|
|
71
|
+
startCompanion = true;
|
|
72
|
+
else if (!projectArg)
|
|
73
|
+
projectArg = a;
|
|
74
|
+
}
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const projectRoot = projectArg ? path.resolve(projectArg) : cwd;
|
|
77
|
+
// If user passed a direct .xcodeproj or .xcworkspace path, accept it as the projectFile
|
|
78
|
+
let projectFile = null;
|
|
79
|
+
try {
|
|
80
|
+
const stat = await fs.promises.stat(projectRoot);
|
|
81
|
+
if (stat.isFile() && (projectRoot.endsWith('.xcodeproj') || projectRoot.endsWith('.xcworkspace'))) {
|
|
82
|
+
projectFile = projectRoot;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
// detect project if not a direct file
|
|
89
|
+
if (!projectFile) {
|
|
90
|
+
const projectFound = await exists(projectRoot);
|
|
91
|
+
if (projectFound) {
|
|
92
|
+
projectFile = await findFirst(projectRoot, ['.xcworkspace', '.xcodeproj'], 2);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// attempt to find under cwd
|
|
96
|
+
projectFile = await findFirst(cwd, ['.xcworkspace', '.xcodeproj'], 3);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 2) KMP Shared.framework detection (search for *.framework named Shared.framework)
|
|
100
|
+
let kmpFramework = null;
|
|
101
|
+
const projectSearchRoot = projectFile ? (fs.existsSync(projectFile) && fs.lstatSync(projectFile).isDirectory() ? projectFile : path.dirname(projectFile)) : cwd;
|
|
102
|
+
kmpFramework = await findFirst(projectSearchRoot, ['Shared.framework'], 5);
|
|
103
|
+
if (!kmpFramework)
|
|
104
|
+
kmpFramework = await findFirst(cwd, ['Shared.framework'], 6);
|
|
105
|
+
// 3) idb detection
|
|
106
|
+
const idbPath = getIdbCmd();
|
|
107
|
+
const idbAvailable = idbPath ? isIDBInstalled() : false;
|
|
108
|
+
// 4) idb_companion
|
|
109
|
+
const companionPath = commandWhich('idb_companion');
|
|
110
|
+
const companionAvailable = !!companionPath;
|
|
111
|
+
const suggestions = [];
|
|
112
|
+
if (!projectFile)
|
|
113
|
+
suggestions.push('Provide correct project path or ensure .xcodeproj/.xcworkspace exists in project dir');
|
|
114
|
+
if (!kmpFramework)
|
|
115
|
+
suggestions.push('Run KMP Gradle task to produce Shared.framework before xcodebuild (e.g., :shared:embedAndSignAppleFrameworkForXcode)');
|
|
116
|
+
if (!idbAvailable)
|
|
117
|
+
suggestions.push('Ensure idb is in PATH or set MCP_IDB_PATH / IDB_PATH');
|
|
118
|
+
if (!companionAvailable)
|
|
119
|
+
suggestions.push('Install idb_companion and ensure it is in PATH');
|
|
120
|
+
const result = {
|
|
121
|
+
ok: !!projectFile && !!idbAvailable,
|
|
122
|
+
project: {
|
|
123
|
+
root: projectRoot,
|
|
124
|
+
found: !!projectFile,
|
|
125
|
+
projectFile: projectFile
|
|
126
|
+
},
|
|
127
|
+
kmp: {
|
|
128
|
+
found: !!kmpFramework,
|
|
129
|
+
path: kmpFramework,
|
|
130
|
+
build: kmpBuild
|
|
131
|
+
},
|
|
132
|
+
idb: {
|
|
133
|
+
cmd: idbPath,
|
|
134
|
+
installed: idbAvailable
|
|
135
|
+
},
|
|
136
|
+
idb_companion: {
|
|
137
|
+
cmd: companionPath,
|
|
138
|
+
installed: companionAvailable
|
|
139
|
+
},
|
|
140
|
+
suggestions
|
|
141
|
+
};
|
|
142
|
+
if (startCompanion && udid) {
|
|
143
|
+
const started = startCompanionIfNeeded(companionPath, udid);
|
|
144
|
+
result.idb_companion.start = started;
|
|
145
|
+
if (started.started)
|
|
146
|
+
result.ok = result.ok && true;
|
|
147
|
+
}
|
|
148
|
+
console.log(JSON.stringify(result, null, 2));
|
|
149
|
+
process.exit(result.ok ? 0 : 2);
|
|
150
|
+
}
|
|
151
|
+
main().catch(e => {
|
|
152
|
+
// Report structured error on stdout (avoid noisy stderr in normal runs)
|
|
153
|
+
console.log(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) }, null, 2));
|
|
154
|
+
process.exit(2);
|
|
155
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { iOSObserve } from '../../ios/observe.js';
|
|
2
|
+
import { iOSManage } from '../../ios/manage.js';
|
|
3
|
+
async function main() {
|
|
4
|
+
const appId = process.argv[2] || 'com.apple.springboard';
|
|
5
|
+
const deviceId = 'booted';
|
|
6
|
+
const obs = new iOSObserve();
|
|
7
|
+
const manage = new iOSManage();
|
|
8
|
+
try {
|
|
9
|
+
console.log('[1] startApp ->', appId);
|
|
10
|
+
const start = await manage.startApp(appId, deviceId);
|
|
11
|
+
console.log('start result:', start);
|
|
12
|
+
console.log('[2] captureScreenshot');
|
|
13
|
+
const shot = await obs.captureScreenshot(deviceId);
|
|
14
|
+
console.log('screenshot OK? size:', shot && shot.screenshot ? shot.screenshot.length : 0);
|
|
15
|
+
console.log('[3] getLogs');
|
|
16
|
+
const logs = await obs.getLogs(appId, undefined);
|
|
17
|
+
console.log('logs count:', logs.logCount);
|
|
18
|
+
console.log('[4] terminateApp');
|
|
19
|
+
const term = await manage.terminateApp(appId, deviceId);
|
|
20
|
+
console.log('terminate:', term);
|
|
21
|
+
console.log('SMOKE OK');
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error('SMOKE ERROR:', err instanceof Error ? err.message : String(err));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
main();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { iOSObserve } from '../../ios/observe.js';
|
|
2
|
+
import { iOSInteract } from '../../ios/interact.js';
|
|
3
|
+
async function main() {
|
|
4
|
+
const deviceId = 'booted';
|
|
5
|
+
const obs = new iOSObserve();
|
|
6
|
+
const interact = new iOSInteract();
|
|
7
|
+
console.log('Fetching UI tree...');
|
|
8
|
+
const tree = await obs.getUITree(deviceId);
|
|
9
|
+
if (tree.error) {
|
|
10
|
+
console.error('getUITree error:', tree.error);
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
console.log('Elements found:', tree.elements.length);
|
|
14
|
+
if (!tree.elements || tree.elements.length === 0) {
|
|
15
|
+
console.error('No elements found; aborting');
|
|
16
|
+
process.exit(3);
|
|
17
|
+
}
|
|
18
|
+
const clickable = tree.elements.find(e => e.clickable) || tree.elements[0];
|
|
19
|
+
console.log('Using element:', clickable.text || '(no text)', 'clickable=', clickable.clickable, 'center=', clickable.center);
|
|
20
|
+
const [x, y] = clickable.center || [0, 0];
|
|
21
|
+
console.log(`Tapping at ${x},${y}...`);
|
|
22
|
+
const res = await interact.tap(x, y, deviceId);
|
|
23
|
+
console.log('Tap result:', res);
|
|
24
|
+
if (res.success)
|
|
25
|
+
process.exit(0);
|
|
26
|
+
else
|
|
27
|
+
process.exit(4);
|
|
28
|
+
}
|
|
29
|
+
main();
|