mobile-debug-mcp 0.10.0 → 0.12.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 +20 -5
- package/dist/android/diagnostics.js +24 -0
- package/dist/android/interact.js +1 -145
- package/dist/android/manage.js +162 -0
- package/dist/android/observe.js +133 -88
- package/dist/android/run.js +187 -0
- package/dist/android/utils.js +137 -147
- package/dist/ios/interact.js +4 -175
- package/dist/ios/manage.js +169 -0
- package/dist/ios/observe.js +129 -13
- package/dist/ios/run.js +200 -0
- package/dist/ios/utils.js +138 -124
- package/dist/server.js +45 -17
- package/dist/tools/interact.js +21 -71
- package/dist/tools/manage.js +180 -0
- package/dist/tools/observe.js +23 -69
- package/dist/tools/run.js +180 -0
- package/dist/utils/diagnostics.js +25 -0
- package/docs/CHANGELOG.md +14 -0
- package/eslint.config.js +22 -1
- package/package.json +8 -5
- package/scripts/check-idb.js +83 -0
- package/scripts/check-idb.ts +73 -0
- package/scripts/idb-helper.ts +76 -0
- package/scripts/install-idb.js +88 -0
- package/scripts/install-idb.ts +90 -0
- package/scripts/run-ios-smoke.ts +34 -0
- package/scripts/run-ios-ui-tree-tap.ts +33 -0
- package/src/android/diagnostics.ts +23 -0
- package/src/android/interact.ts +2 -155
- package/src/android/manage.ts +157 -0
- package/src/android/observe.ts +129 -97
- package/src/android/utils.ts +147 -149
- package/src/ios/interact.ts +5 -181
- package/src/ios/manage.ts +164 -0
- package/src/ios/observe.ts +130 -14
- package/src/ios/utils.ts +127 -128
- package/src/server.ts +47 -17
- package/src/tools/interact.ts +23 -62
- package/src/tools/manage.ts +171 -0
- package/src/tools/observe.ts +24 -74
- package/src/types.ts +9 -0
- package/src/utils/diagnostics.ts +36 -0
- package/test/device/README.md +49 -0
- package/test/device/index.ts +27 -0
- package/test/device/manage/run-build-install-ios.ts +82 -0
- package/test/{integration → device/manage}/run-install-android.ts +4 -4
- package/test/{integration → device/manage}/run-install-ios.ts +4 -4
- package/test/{integration → device/observe}/logstream-real.ts +5 -4
- package/test/{integration → device/utils}/test-dist.ts +2 -2
- package/test/unit/index.ts +10 -6
- package/test/unit/manage/build.test.ts +83 -0
- package/test/unit/manage/build_and_install.test.ts +134 -0
- package/test/unit/manage/diagnostics.test.ts +85 -0
- package/test/unit/{install.test.ts → manage/install.test.ts} +27 -18
- package/test/unit/{logparse.test.ts → observe/logparse.test.ts} +1 -1
- package/test/unit/{logstream.test.ts → observe/logstream.test.ts} +9 -10
- package/test/unit/{wait_for_element_mock.ts → observe/wait_for_element_mock.ts} +3 -3
- package/test/unit/{detect-java.test.ts → utils/detect-java.test.ts} +5 -5
- package/tsconfig.json +2 -1
- package/test/integration/index.ts +0 -8
- package/test/integration/test-dist.mjs +0 -41
- /package/test/{integration → device/interact}/run-real-test.ts +0 -0
- /package/test/{integration → device/interact}/smoke-test.ts +0 -0
- /package/test/{integration → device/manage}/install.integration.ts +0 -0
- /package/test/{integration → device/observe}/test-ui-tree.ts +0 -0
- /package/test/{integration → device/observe}/wait_for_element_real.ts +0 -0
package/eslint.config.js
CHANGED
|
@@ -11,7 +11,8 @@ export default [
|
|
|
11
11
|
'.git/',
|
|
12
12
|
'.vscode/',
|
|
13
13
|
'coverage/',
|
|
14
|
-
'.env'
|
|
14
|
+
'.env',
|
|
15
|
+
'scripts/'
|
|
15
16
|
]
|
|
16
17
|
},
|
|
17
18
|
// Apply rules to JS/TS source
|
|
@@ -56,5 +57,25 @@ export default [
|
|
|
56
57
|
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
57
58
|
'@typescript-eslint/no-unused-vars': 'off'
|
|
58
59
|
}
|
|
60
|
+
},
|
|
61
|
+
// Apply rules to scripts and tooling (support TS syntax in scripts)
|
|
62
|
+
{
|
|
63
|
+
files: ['scripts/**/*.ts', 'scripts/**/*.js'],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parser: tsParser,
|
|
66
|
+
parserOptions: {
|
|
67
|
+
ecmaVersion: 2020,
|
|
68
|
+
sourceType: 'module'
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
plugins: {
|
|
72
|
+
'@typescript-eslint': tsPlugin,
|
|
73
|
+
'unused-imports': unusedImports
|
|
74
|
+
},
|
|
75
|
+
rules: {
|
|
76
|
+
'unused-imports/no-unused-imports': 'error',
|
|
77
|
+
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
78
|
+
'@typescript-eslint/no-unused-vars': 'off'
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node ./dist/server.js",
|
|
12
12
|
"prepare": "npm run build",
|
|
13
|
+
"healthcheck": "tsx ./scripts/check-idb.ts",
|
|
14
|
+
"install-idb": "tsx ./scripts/install-idb.ts",
|
|
13
15
|
"test:unit": "tsx test/unit/index.ts",
|
|
14
|
-
"test:integration": "tsx test/
|
|
15
|
-
"test": "npm run
|
|
16
|
+
"test:integration": "npm run build && tsx test/device/index.ts",
|
|
17
|
+
"test:device": "npm run build && tsx test/device/index.ts",
|
|
18
|
+
"test": "npm run test:unit",
|
|
16
19
|
"lint": "eslint --ext .ts,.js src test --quiet",
|
|
17
20
|
"lint:fix": "eslint --ext .ts,.js src test --fix"
|
|
18
21
|
},
|
|
@@ -26,8 +29,8 @@
|
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
28
31
|
"@types/node": "^25.4.0",
|
|
29
|
-
"@typescript-eslint/eslint-plugin": "^8.57.
|
|
30
|
-
"@typescript-eslint/parser": "^8.57.
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
33
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
31
34
|
"eslint": "^9.39.4",
|
|
32
35
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
33
36
|
"tsx": "^4.21.0",
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import { main as installMain } from './install-idb'
|
|
4
|
+
import { getIdbCmd, isIDBInstalled } from './idb-helper'
|
|
5
|
+
|
|
6
|
+
function which(cmd: string): string | null {
|
|
7
|
+
try {
|
|
8
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
9
|
+
if (r && r.status === 0 && r.stdout) return r.stdout.toString().trim()
|
|
10
|
+
} catch {}
|
|
11
|
+
try {
|
|
12
|
+
return execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function print(...args: any[]) {
|
|
19
|
+
console.log(...args)
|
|
20
|
+
}
|
|
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', './scripts/install-idb.ts'] : ['./scripts/install-idb.ts']
|
|
28
|
+
const res = spawnSync(runner, args, { stdio: 'inherit' as any })
|
|
29
|
+
return typeof res.status === 'number' ? res.status === 0 : false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// fallback: attempt to import and run the installer directly (may rely on ts-node/tsx)
|
|
33
|
+
try {
|
|
34
|
+
// call the exported main; it returns a promise
|
|
35
|
+
await installMain()
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('Failed to run installer:', e instanceof Error ? e.message : String(e))
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
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 { print('idb --version:', execSync(`${idb} --version`, { stdio: ['ignore','pipe','ignore'] }).toString().trim()) } catch (e) { print('idb --version: (failed)', e instanceof Error ? e.message : String(e)) }
|
|
52
|
+
const companion = which('idb_companion')
|
|
53
|
+
print('which idb_companion:', companion)
|
|
54
|
+
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)) }
|
|
55
|
+
process.exit(0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
print('idb not found or not responding')
|
|
59
|
+
const auto = process.env.MCP_AUTO_INSTALL_IDB === 'true'
|
|
60
|
+
if (auto) {
|
|
61
|
+
print('MCP_AUTO_INSTALL_IDB=true, attempting installer...')
|
|
62
|
+
const ok = runInstaller()
|
|
63
|
+
if (ok) process.exit(0)
|
|
64
|
+
print('Installer failed or did not produce idb')
|
|
65
|
+
process.exit(2)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
print('Set MCP_AUTO_INSTALL_IDB=true to attempt automatic installation (CI-friendly).')
|
|
69
|
+
process.exit(2)
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error('idb healthcheck failed:', e instanceof Error ? e.message : String(e))
|
|
72
|
+
process.exit(2)
|
|
73
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export function getConfiguredIdbPath(): string | undefined {
|
|
6
|
+
if (process.env.MCP_IDB_PATH) return process.env.MCP_IDB_PATH
|
|
7
|
+
if (process.env.IDB_PATH) 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) continue
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(p)) {
|
|
16
|
+
const raw = fs.readFileSync(p, 'utf8')
|
|
17
|
+
const json = JSON.parse(raw)
|
|
18
|
+
if (json) {
|
|
19
|
+
if (json.idbPath) return json.idbPath
|
|
20
|
+
if (json.IDB_PATH) return json.IDB_PATH
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function commandWhich(cmd: string): string | null {
|
|
29
|
+
try {
|
|
30
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
31
|
+
if (r && r.status === 0 && r.stdout) return r.stdout.toString().trim()
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
const p = execSync(`which ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
|
35
|
+
if (p) return p
|
|
36
|
+
} catch {}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getIdbCmd(): string | null {
|
|
41
|
+
const cfg = getConfiguredIdbPath()
|
|
42
|
+
if (cfg) return cfg
|
|
43
|
+
if (process.env.IDB_PATH) return process.env.IDB_PATH
|
|
44
|
+
|
|
45
|
+
// Prefer command -v/which
|
|
46
|
+
const found = commandWhich('idb')
|
|
47
|
+
if (found) return found
|
|
48
|
+
|
|
49
|
+
// Common locations
|
|
50
|
+
const common = [
|
|
51
|
+
process.env.HOME ? `${process.env.HOME}/Library/Python/3.9/bin/idb` : '',
|
|
52
|
+
process.env.HOME ? `${process.env.HOME}/Library/Python/3.10/bin/idb` : '',
|
|
53
|
+
'/opt/homebrew/bin/idb',
|
|
54
|
+
'/usr/local/bin/idb'
|
|
55
|
+
]
|
|
56
|
+
for (const c of common) {
|
|
57
|
+
if (!c) continue
|
|
58
|
+
try { execSync(`test -x ${c}`, { stdio: ['ignore', 'pipe', 'ignore'] }); return c } catch {}
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isIDBInstalled(): boolean {
|
|
64
|
+
const cmd = getIdbCmd()
|
|
65
|
+
if (!cmd) return false
|
|
66
|
+
try {
|
|
67
|
+
// command -v <cmd>
|
|
68
|
+
const r = spawnSync('command', ['-v', cmd], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
69
|
+
if (r && r.status === 0) return true
|
|
70
|
+
} catch {}
|
|
71
|
+
try {
|
|
72
|
+
execSync(`${cmd} list-targets --json`, { stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 })
|
|
73
|
+
return true
|
|
74
|
+
} catch {}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
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
|
+
function runCommand(cmd, args) {
|
|
14
|
+
const res = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
15
|
+
return typeof res.status === 'number' ? res.status : 1;
|
|
16
|
+
}
|
|
17
|
+
async function confirm(prompt) {
|
|
18
|
+
if (!process.stdin.isTTY)
|
|
19
|
+
return false;
|
|
20
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(`${prompt} (y/N): `, (ans) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(ans.trim().toLowerCase() === 'y');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
try {
|
|
30
|
+
const idbFromEnv = process.env.IDB_PATH;
|
|
31
|
+
const existing = idbFromEnv || which('idb') || which('command -v idb');
|
|
32
|
+
if (existing) {
|
|
33
|
+
console.log('idb already available at:', existing);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const auto = process.env.MCP_AUTO_INSTALL_IDB === 'true' || process.env.CI === 'true';
|
|
37
|
+
if (!auto) {
|
|
38
|
+
const ok = await confirm('idb not found. Attempt to install fb-idb now?');
|
|
39
|
+
if (!ok) {
|
|
40
|
+
console.log('Aborting install; set MCP_AUTO_INSTALL_IDB=true to auto-install in CI or non-interactive environments.');
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log('Auto-install enabled (MCP_AUTO_INSTALL_IDB=true or CI=true)');
|
|
46
|
+
}
|
|
47
|
+
const attempts = [];
|
|
48
|
+
if (which('pipx'))
|
|
49
|
+
attempts.push({ name: 'pipx', cmd: 'pipx', args: ['install', IDB_PKG] });
|
|
50
|
+
if (which('pip') || which('python3'))
|
|
51
|
+
attempts.push({ name: 'pip', cmd: which('pip') ? 'pip' : 'python3', args: which('pip') ? ['install', '--user', IDB_PKG] : ['-m', 'pip', 'install', '--user', IDB_PKG] });
|
|
52
|
+
// Add brew as a fallback on macOS if present (best-effort)
|
|
53
|
+
if (process.platform === 'darwin' && which('brew')) {
|
|
54
|
+
attempts.push({ name: 'brew', cmd: 'brew', args: ['install', 'idb'] });
|
|
55
|
+
}
|
|
56
|
+
if (attempts.length === 0) {
|
|
57
|
+
console.error('No installer tool (pipx/pip/brew) detected. Please install pipx or pip and re-run.');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
for (const a of attempts) {
|
|
61
|
+
console.log(`Attempting install with ${a.name}: ${a.cmd} ${a.args.join(' ')}`);
|
|
62
|
+
try {
|
|
63
|
+
const code = runCommand(a.cmd, a.args);
|
|
64
|
+
if (code !== 0) {
|
|
65
|
+
console.warn(`${a.name} install exited with code ${code}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
console.warn(`${a.name} install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
70
|
+
}
|
|
71
|
+
const found = which('idb') || which('command -v idb');
|
|
72
|
+
if (found) {
|
|
73
|
+
console.log('idb installed at:', found);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.error('idb was not installed by any installer tried. Please install fb-idb manually and re-run healthcheck.');
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.error('Installer failed:', e instanceof Error ? e.message : String(e));
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (require.main === module) {
|
|
86
|
+
main();
|
|
87
|
+
}
|
|
88
|
+
export { main };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import readline from 'readline'
|
|
4
|
+
import { getIdbCmd, isIDBInstalled, commandWhich } from './idb-helper'
|
|
5
|
+
|
|
6
|
+
const IDB_PKG = 'fb-idb'
|
|
7
|
+
|
|
8
|
+
function runCommand(cmd: string, args: string[]): number {
|
|
9
|
+
const res = spawnSync(cmd, args, { stdio: 'inherit' as any })
|
|
10
|
+
return typeof res.status === 'number' ? res.status : 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function confirm(prompt: string): Promise<boolean> {
|
|
14
|
+
if (!process.stdin.isTTY) return false
|
|
15
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(`${prompt} (y/N): `, (ans) => {
|
|
18
|
+
rl.close()
|
|
19
|
+
resolve(ans.trim().toLowerCase() === 'y')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
try {
|
|
26
|
+
const idbFromEnv = process.env.IDB_PATH
|
|
27
|
+
const existing = idbFromEnv || getIdbCmd()
|
|
28
|
+
if (existing && isIDBInstalled()) {
|
|
29
|
+
console.log('idb already available at:', existing)
|
|
30
|
+
process.exit(0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const auto = process.env.MCP_AUTO_INSTALL_IDB === 'true' || process.env.CI === 'true'
|
|
34
|
+
|
|
35
|
+
if (!auto) {
|
|
36
|
+
const ok = await confirm('idb not found. Attempt to install fb-idb now?')
|
|
37
|
+
if (!ok) {
|
|
38
|
+
console.log('Aborting install; set MCP_AUTO_INSTALL_IDB=true to auto-install in CI or non-interactive environments.')
|
|
39
|
+
process.exit(2)
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
console.log('Auto-install enabled (MCP_AUTO_INSTALL_IDB=true or CI=true)')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const attempts: { name: string; cmd: string; args: string[] }[] = []
|
|
46
|
+
if (commandWhich('pipx')) attempts.push({ name: 'pipx', cmd: 'pipx', args: ['install', IDB_PKG] })
|
|
47
|
+
if (commandWhich('pip') || commandWhich('python3')) attempts.push({ name: 'pip', cmd: commandWhich('pip') ? 'pip' : 'python3', args: commandWhich('pip') ? ['install', '--user', IDB_PKG] : ['-m', 'pip', 'install', '--user', IDB_PKG] })
|
|
48
|
+
|
|
49
|
+
// Add brew as a fallback on macOS if present (best-effort)
|
|
50
|
+
if (process.platform === 'darwin' && commandWhich('brew')) {
|
|
51
|
+
attempts.push({ name: 'brew', cmd: 'brew', args: ['install', 'idb'] })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (attempts.length === 0) {
|
|
55
|
+
console.error('No installer tool (pipx/pip/brew) detected. Please install pipx or pip and re-run.')
|
|
56
|
+
process.exit(2)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const a of attempts) {
|
|
60
|
+
console.log(`Attempting install with ${a.name}: ${a.cmd} ${a.args.join(' ')}`)
|
|
61
|
+
try {
|
|
62
|
+
const code = runCommand(a.cmd, a.args)
|
|
63
|
+
if (code !== 0) {
|
|
64
|
+
console.warn(`${a.name} install exited with code ${code}`)
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn(`${a.name} install failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const found = which('idb') || which('command -v idb')
|
|
71
|
+
if (found) {
|
|
72
|
+
console.log('idb installed at:', found)
|
|
73
|
+
process.exit(0)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.error('idb was not installed by any installer tried. Please install fb-idb manually and re-run healthcheck.')
|
|
78
|
+
process.exit(2)
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error('Installer failed:', e instanceof Error ? e.message : String(e))
|
|
81
|
+
process.exit(2)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const scriptPath = new URL(import.meta.url).pathname;
|
|
86
|
+
if (scriptPath === process.argv[1]) {
|
|
87
|
+
main().catch(e => { console.error('Installer failed:', e instanceof Error ? e.message : String(e)); process.exit(2); });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { main }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { iOSObserve } from '../src/ios/observe.js';
|
|
2
|
+
import { iOSManage } from '../src/ios/manage.js';
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const appId = process.argv[2] || 'com.apple.springboard';
|
|
6
|
+
const deviceId = 'booted';
|
|
7
|
+
const obs = new iOSObserve();
|
|
8
|
+
const manage = new iOSManage();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
console.log('[1] startApp ->', appId)
|
|
12
|
+
const start = await manage.startApp(appId, deviceId);
|
|
13
|
+
console.log('start result:', start)
|
|
14
|
+
|
|
15
|
+
console.log('[2] captureScreenshot')
|
|
16
|
+
const shot = await obs.captureScreenshot(deviceId);
|
|
17
|
+
console.log('screenshot OK? size:', shot && shot.screenshot ? shot.screenshot.length : 0)
|
|
18
|
+
|
|
19
|
+
console.log('[3] getLogs')
|
|
20
|
+
const logs = await obs.getLogs(appId, undefined);
|
|
21
|
+
console.log('logs count:', logs.logCount)
|
|
22
|
+
|
|
23
|
+
console.log('[4] terminateApp')
|
|
24
|
+
const term = await manage.terminateApp(appId, deviceId);
|
|
25
|
+
console.log('terminate:', term)
|
|
26
|
+
|
|
27
|
+
console.log('SMOKE OK')
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('SMOKE ERROR:', err instanceof Error ? err.message : String(err))
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { iOSObserve } from '../src/ios/observe.js';
|
|
2
|
+
import { iOSInteract } from '../src/ios/interact.js';
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const deviceId = 'booted';
|
|
6
|
+
const obs = new iOSObserve();
|
|
7
|
+
const interact = new iOSInteract();
|
|
8
|
+
|
|
9
|
+
console.log('Fetching UI tree...');
|
|
10
|
+
const tree = await obs.getUITree(deviceId as any);
|
|
11
|
+
if (tree.error) {
|
|
12
|
+
console.error('getUITree error:', tree.error);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
console.log('Elements found:', tree.elements.length);
|
|
16
|
+
if (!tree.elements || tree.elements.length === 0) {
|
|
17
|
+
console.error('No elements found; aborting');
|
|
18
|
+
process.exit(3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const clickable = tree.elements.find(e => e.clickable) || tree.elements[0];
|
|
22
|
+
console.log('Using element:', clickable.text || '(no text)', 'clickable=', clickable.clickable, 'center=', clickable.center);
|
|
23
|
+
const [x,y] = clickable.center || [0,0];
|
|
24
|
+
|
|
25
|
+
console.log(`Tapping at ${x},${y}...`);
|
|
26
|
+
const res = await interact.tap(x, y, deviceId as any);
|
|
27
|
+
console.log('Tap result:', res);
|
|
28
|
+
|
|
29
|
+
if (res.success) process.exit(0);
|
|
30
|
+
else process.exit(4);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|