@metamask/device-mcp 0.1.0 → 0.2.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/CHANGELOG.md +9 -14
- package/LICENSE +6 -0
- package/LICENSE.APACHE2 +201 -0
- package/LICENSE.MIT +21 -0
- package/README.md +61 -33
- package/dist/backends/adb-backend.cjs +9 -0
- package/dist/backends/adb-backend.cjs.map +1 -1
- package/dist/backends/adb-backend.d.cts +1 -0
- package/dist/backends/adb-backend.d.cts.map +1 -1
- package/dist/backends/adb-backend.d.mts +1 -0
- package/dist/backends/adb-backend.d.mts.map +1 -1
- package/dist/backends/adb-backend.mjs +9 -0
- package/dist/backends/adb-backend.mjs.map +1 -1
- package/dist/backends/appium-backend.cjs +9 -0
- package/dist/backends/appium-backend.cjs.map +1 -1
- package/dist/backends/appium-backend.d.cts +1 -0
- package/dist/backends/appium-backend.d.cts.map +1 -1
- package/dist/backends/appium-backend.d.mts +1 -0
- package/dist/backends/appium-backend.d.mts.map +1 -1
- package/dist/backends/appium-backend.mjs +9 -0
- package/dist/backends/appium-backend.mjs.map +1 -1
- package/dist/backends/idb-backend.cjs +84 -33
- package/dist/backends/idb-backend.cjs.map +1 -1
- package/dist/backends/idb-backend.d.cts +1 -0
- package/dist/backends/idb-backend.d.cts.map +1 -1
- package/dist/backends/idb-backend.d.mts +1 -0
- package/dist/backends/idb-backend.d.mts.map +1 -1
- package/dist/backends/idb-backend.mjs +85 -34
- package/dist/backends/idb-backend.mjs.map +1 -1
- package/dist/backends/index.cjs +33 -12
- package/dist/backends/index.cjs.map +1 -1
- package/dist/backends/index.d.cts +8 -11
- package/dist/backends/index.d.cts.map +1 -1
- package/dist/backends/index.d.mts +8 -11
- package/dist/backends/index.d.mts.map +1 -1
- package/dist/backends/index.mjs +34 -13
- package/dist/backends/index.mjs.map +1 -1
- package/dist/backends/types.cjs.map +1 -1
- package/dist/backends/types.d.cts +1 -0
- package/dist/backends/types.d.cts.map +1 -1
- package/dist/backends/types.d.mts +1 -0
- package/dist/backends/types.d.mts.map +1 -1
- package/dist/backends/types.mjs.map +1 -1
- package/dist/cli/device-mcp.cjs +11 -2
- package/dist/cli/device-mcp.cjs.map +1 -1
- package/dist/cli/device-mcp.mjs +11 -2
- package/dist/cli/device-mcp.mjs.map +1 -1
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/server.cjs +5 -0
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -2
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +6 -1
- package/dist/server.mjs.map +1 -1
- package/dist/tools/get-element-text.cjs +46 -0
- package/dist/tools/get-element-text.cjs.map +1 -0
- package/dist/tools/get-element-text.d.cts +4 -0
- package/dist/tools/get-element-text.d.cts.map +1 -0
- package/dist/tools/get-element-text.d.mts +4 -0
- package/dist/tools/get-element-text.d.mts.map +1 -0
- package/dist/tools/get-element-text.mjs +43 -0
- package/dist/tools/get-element-text.mjs.map +1 -0
- package/dist/tools/index.cjs +7 -1
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.cts +3 -0
- package/dist/tools/index.d.cts.map +1 -1
- package/dist/tools/index.d.mts +3 -0
- package/dist/tools/index.d.mts.map +1 -1
- package/dist/tools/index.mjs +3 -0
- package/dist/tools/index.mjs.map +1 -1
- package/dist/tools/list-devices.cjs +36 -0
- package/dist/tools/list-devices.cjs.map +1 -0
- package/dist/tools/list-devices.d.cts +4 -0
- package/dist/tools/list-devices.d.cts.map +1 -0
- package/dist/tools/list-devices.d.mts +4 -0
- package/dist/tools/list-devices.d.mts.map +1 -0
- package/dist/tools/list-devices.mjs +33 -0
- package/dist/tools/list-devices.mjs.map +1 -0
- package/dist/tools/select-device.cjs +38 -0
- package/dist/tools/select-device.cjs.map +1 -0
- package/dist/tools/select-device.d.cts +4 -0
- package/dist/tools/select-device.d.cts.map +1 -0
- package/dist/tools/select-device.d.mts +4 -0
- package/dist/tools/select-device.d.mts.map +1 -0
- package/dist/tools/select-device.mjs +35 -0
- package/dist/tools/select-device.mjs.map +1 -0
- package/dist/utils/exec.cjs +11 -9
- package/dist/utils/exec.cjs.map +1 -1
- package/dist/utils/exec.d.cts.map +1 -1
- package/dist/utils/exec.d.mts.map +1 -1
- package/dist/utils/exec.mjs +11 -9
- package/dist/utils/exec.mjs.map +1 -1
- package/dist/utils/platform.cjs +121 -36
- package/dist/utils/platform.cjs.map +1 -1
- package/dist/utils/platform.d.cts +14 -3
- package/dist/utils/platform.d.cts.map +1 -1
- package/dist/utils/platform.d.mts +14 -3
- package/dist/utils/platform.d.mts.map +1 -1
- package/dist/utils/platform.mjs +117 -36
- package/dist/utils/platform.mjs.map +1 -1
- package/package.json +2 -2
package/dist/utils/platform.cjs
CHANGED
|
@@ -1,29 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MultipleDevicesError = void 0;
|
|
4
|
+
exports.detectAllDevices = detectAllDevices;
|
|
3
5
|
exports.detectPlatform = detectPlatform;
|
|
6
|
+
exports.resolveIdbPath = resolveIdbPath;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
4
8
|
const exec_js_1 = require("./exec.cjs");
|
|
5
|
-
|
|
9
|
+
class MultipleDevicesError extends Error {
|
|
10
|
+
constructor(devices) {
|
|
11
|
+
const lines = devices.map((d) => ` ${d.platform}\t${d.deviceId}`);
|
|
12
|
+
super(`Multiple devices detected. Set DEVICE_ID to one of:\n${lines.join('\n')}`);
|
|
13
|
+
this.name = 'MultipleDevicesError';
|
|
14
|
+
this.devices = devices;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.MultipleDevicesError = MultipleDevicesError;
|
|
18
|
+
/**
|
|
19
|
+
* Detect all connected devices across iOS and Android.
|
|
20
|
+
*
|
|
21
|
+
* @param platform - Optional filter to only return devices of a specific platform.
|
|
22
|
+
* @returns Array of detected devices (may be empty).
|
|
23
|
+
*/
|
|
24
|
+
async function detectAllDevices(platform) {
|
|
25
|
+
const searches = [];
|
|
26
|
+
if (platform !== 'android') {
|
|
27
|
+
searches.push(findAllBootedIOSSimulators().catch(() => []));
|
|
28
|
+
}
|
|
29
|
+
if (platform !== 'ios') {
|
|
30
|
+
searches.push(findAllConnectedAndroidDevices().catch(() => []));
|
|
31
|
+
}
|
|
32
|
+
const results = await Promise.all(searches);
|
|
33
|
+
return results.flat();
|
|
34
|
+
}
|
|
35
|
+
async function detectPlatform(explicitDeviceId, explicitPlatform) {
|
|
6
36
|
if (explicitDeviceId) {
|
|
7
37
|
return detectFromExplicitId(explicitDeviceId);
|
|
8
38
|
}
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
]);
|
|
13
|
-
if (hasIdb) {
|
|
14
|
-
const iosDevice = await findBootedIOSDevice();
|
|
15
|
-
if (iosDevice) {
|
|
16
|
-
return iosDevice;
|
|
17
|
-
}
|
|
39
|
+
const devices = await detectAllDevices(explicitPlatform);
|
|
40
|
+
if (devices.length === 1) {
|
|
41
|
+
return devices[0];
|
|
18
42
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
if (androidDevice) {
|
|
22
|
-
return androidDevice;
|
|
23
|
-
}
|
|
43
|
+
if (devices.length > 1) {
|
|
44
|
+
throw new MultipleDevicesError(devices);
|
|
24
45
|
}
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
const hasAdb = explicitPlatform === 'ios' ? false : await (0, exec_js_1.isCommandAvailable)('adb');
|
|
47
|
+
const platformLabel = explicitPlatform ?? 'any';
|
|
48
|
+
throw new Error(`No connected ${platformLabel} device found.\n` +
|
|
49
|
+
` iOS (simctl): no booted simulator\n` +
|
|
27
50
|
` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\n` +
|
|
28
51
|
'Boot a simulator/emulator or set DEVICE_ID explicitly.');
|
|
29
52
|
}
|
|
@@ -55,39 +78,101 @@ async function detectFromExplicitId(deviceId) {
|
|
|
55
78
|
throw new Error(`Cannot determine platform for device ID: ${deviceId}. ` +
|
|
56
79
|
'Ensure the device is connected and idb/adb is available.');
|
|
57
80
|
}
|
|
58
|
-
async function
|
|
59
|
-
const result = await (0, exec_js_1.exec)('
|
|
81
|
+
async function findAllBootedIOSSimulators() {
|
|
82
|
+
const result = await (0, exec_js_1.exec)('xcrun', [
|
|
83
|
+
'simctl',
|
|
84
|
+
'list',
|
|
85
|
+
'devices',
|
|
86
|
+
'booted',
|
|
87
|
+
'--json',
|
|
88
|
+
]);
|
|
60
89
|
if (result.exitCode !== 0) {
|
|
61
|
-
return
|
|
90
|
+
return [];
|
|
62
91
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(result.stdout);
|
|
94
|
+
const devices = [];
|
|
95
|
+
for (const runtimeDevices of Object.values(parsed.devices)) {
|
|
96
|
+
for (const device of runtimeDevices) {
|
|
97
|
+
if (device.state === 'Booted' && device.isAvailable) {
|
|
98
|
+
devices.push({ platform: 'ios', deviceId: device.udid });
|
|
99
|
+
}
|
|
71
100
|
}
|
|
72
101
|
}
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
return devices;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function findPythonBinDirs(base) {
|
|
109
|
+
try {
|
|
110
|
+
return (0, node_fs_1.readdirSync)(base, { withFileTypes: true })
|
|
111
|
+
.filter((e) => (e.isDirectory() && e.name.startsWith('python')) ||
|
|
112
|
+
/^\d+\.\d+$/u.test(e.name))
|
|
113
|
+
.map((e) => `${base}/${e.name}/bin`);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function resolveIdbPath() {
|
|
120
|
+
const result = await (0, exec_js_1.exec)('idb', ['--help']).catch(() => null);
|
|
121
|
+
if (result?.exitCode === 0) {
|
|
122
|
+
return 'idb';
|
|
123
|
+
}
|
|
124
|
+
const home = process.env.HOME ?? '';
|
|
125
|
+
const candidates = [
|
|
126
|
+
'/usr/local/bin/idb',
|
|
127
|
+
'/opt/homebrew/bin/idb',
|
|
128
|
+
...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),
|
|
129
|
+
...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),
|
|
130
|
+
];
|
|
131
|
+
for (const candidate of candidates) {
|
|
132
|
+
const probe = await (0, exec_js_1.exec)(candidate, ['--help']).catch(() => null);
|
|
133
|
+
if (probe?.exitCode === 0) {
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw new Error('idb not found.\n' +
|
|
138
|
+
'Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb');
|
|
139
|
+
}
|
|
140
|
+
async function resolveAdbPath() {
|
|
141
|
+
const result = await (0, exec_js_1.exec)('adb', ['version']).catch(() => null);
|
|
142
|
+
if (result?.exitCode === 0) {
|
|
143
|
+
return 'adb';
|
|
144
|
+
}
|
|
145
|
+
const home = process.env.HOME ?? '';
|
|
146
|
+
const candidates = [
|
|
147
|
+
process.env.ANDROID_HOME &&
|
|
148
|
+
`${process.env.ANDROID_HOME}/platform-tools/adb`,
|
|
149
|
+
process.env.ANDROID_SDK_ROOT &&
|
|
150
|
+
`${process.env.ANDROID_SDK_ROOT}/platform-tools/adb`,
|
|
151
|
+
`${home}/Library/Android/sdk/platform-tools/adb`,
|
|
152
|
+
`${home}/Android/Sdk/platform-tools/adb`,
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
const probe = await (0, exec_js_1.exec)(candidate, ['version']).catch(() => null);
|
|
156
|
+
if (probe?.exitCode === 0) {
|
|
157
|
+
return candidate;
|
|
75
158
|
}
|
|
76
159
|
}
|
|
77
|
-
|
|
160
|
+
throw new Error('adb not found');
|
|
78
161
|
}
|
|
79
|
-
async function
|
|
80
|
-
const
|
|
162
|
+
async function findAllConnectedAndroidDevices() {
|
|
163
|
+
const adb = await resolveAdbPath();
|
|
164
|
+
const result = await (0, exec_js_1.exec)(adb, ['devices']);
|
|
81
165
|
if (result.exitCode !== 0) {
|
|
82
|
-
return
|
|
166
|
+
return [];
|
|
83
167
|
}
|
|
168
|
+
const devices = [];
|
|
84
169
|
const lines = result.stdout.trim().split('\n').slice(1);
|
|
85
170
|
for (const line of lines) {
|
|
86
171
|
const [serial, state] = line.trim().split(/\s+/u);
|
|
87
172
|
if (serial && state === 'device') {
|
|
88
|
-
|
|
173
|
+
devices.push({ platform: 'android', deviceId: serial });
|
|
89
174
|
}
|
|
90
175
|
}
|
|
91
|
-
return
|
|
176
|
+
return devices;
|
|
92
177
|
}
|
|
93
178
|
//# sourceMappingURL=platform.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.cjs","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"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","sourcesContent":["import { exec, isCommandAvailable } from './exec.js';\nimport type { Platform } from '../backends/types.js';\n\ntype DetectedDevice = {\n platform: Platform;\n deviceId: string;\n};\n\nexport async function detectPlatform(\n explicitDeviceId?: string,\n): Promise<DetectedDevice> {\n if (explicitDeviceId) {\n return detectFromExplicitId(explicitDeviceId);\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const iosDevice = await findBootedIOSDevice();\n if (iosDevice) {\n return iosDevice;\n }\n }\n\n if (hasAdb) {\n const androidDevice = await findConnectedAndroidDevice();\n if (androidDevice) {\n return androidDevice;\n }\n }\n\n throw new Error(\n 'No connected device found.\\n' +\n ` iOS (idb): ${hasIdb ? 'available, no booted simulator' : 'not installed'}\\n` +\n ` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\\n` +\n 'Boot a simulator/emulator or set DEVICE_ID explicitly.',\n );\n}\n\nasync function detectFromExplicitId(deviceId: string): Promise<DetectedDevice> {\n // UUIDs with dashes are typically iOS simulator UDIDs\n if (/^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/iu.test(deviceId)) {\n return { platform: 'ios', deviceId };\n }\n\n // Android emulator or device serial patterns\n if (deviceId.startsWith('emulator-') || /^[A-Za-z0-9]+$/u.test(deviceId)) {\n return { platform: 'android', deviceId };\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const result = await exec('idb', ['describe', '--udid', deviceId]);\n if (result.exitCode === 0) {\n return { platform: 'ios', deviceId };\n }\n }\n\n if (hasAdb) {\n const result = await exec('adb', ['-s', deviceId, 'get-state']);\n if (result.exitCode === 0) {\n return { platform: 'android', deviceId };\n }\n }\n\n throw new Error(\n `Cannot determine platform for device ID: ${deviceId}. ` +\n 'Ensure the device is connected and idb/adb is available.',\n );\n}\n\nasync function findBootedIOSDevice(): Promise<DetectedDevice | null> {\n const result = await exec('idb', ['list-targets', '--json']);\n if (result.exitCode !== 0) {\n return null;\n }\n\n for (const line of result.stdout.trim().split('\\n')) {\n if (!line.trim()) {\n continue;\n }\n try {\n const target = JSON.parse(line);\n if (target.state === 'Booted' && target.type === 'simulator') {\n return { platform: 'ios', deviceId: target.udid };\n }\n } catch {\n continue;\n }\n }\n return null;\n}\n\nasync function findConnectedAndroidDevice(): Promise<DetectedDevice | null> {\n const result = await exec('adb', ['devices']);\n if (result.exitCode !== 0) {\n return null;\n }\n\n const lines = result.stdout.trim().split('\\n').slice(1);\n for (const line of lines) {\n const [serial, state] = line.trim().split(/\\s+/u);\n if (serial && state === 'device') {\n return { platform: 'android', deviceId: serial };\n }\n }\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"platform.cjs","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync } from 'node:fs';\n\nimport { exec, isCommandAvailable } from './exec.js';\nimport type { Platform } from '../backends/types.js';\n\nexport type DetectedDevice = {\n platform: Platform;\n deviceId: string;\n};\n\nexport class MultipleDevicesError extends Error {\n readonly devices: DetectedDevice[];\n\n constructor(devices: DetectedDevice[]) {\n const lines = devices.map((d) => ` ${d.platform}\\t${d.deviceId}`);\n super(\n `Multiple devices detected. Set DEVICE_ID to one of:\\n${lines.join('\\n')}`,\n );\n this.name = 'MultipleDevicesError';\n this.devices = devices;\n }\n}\n\n/**\n * Detect all connected devices across iOS and Android.\n *\n * @param platform - Optional filter to only return devices of a specific platform.\n * @returns Array of detected devices (may be empty).\n */\nexport async function detectAllDevices(\n platform?: Platform,\n): Promise<DetectedDevice[]> {\n const searches: Promise<DetectedDevice[]>[] = [];\n\n if (platform !== 'android') {\n searches.push(findAllBootedIOSSimulators().catch(() => []));\n }\n if (platform !== 'ios') {\n searches.push(findAllConnectedAndroidDevices().catch(() => []));\n }\n\n const results = await Promise.all(searches);\n return results.flat();\n}\n\nexport async function detectPlatform(\n explicitDeviceId?: string,\n explicitPlatform?: Platform,\n): Promise<DetectedDevice> {\n if (explicitDeviceId) {\n return detectFromExplicitId(explicitDeviceId);\n }\n\n const devices = await detectAllDevices(explicitPlatform);\n\n if (devices.length === 1) {\n return devices[0];\n }\n\n if (devices.length > 1) {\n throw new MultipleDevicesError(devices);\n }\n\n const hasAdb =\n explicitPlatform === 'ios' ? false : await isCommandAvailable('adb');\n\n const platformLabel = explicitPlatform ?? 'any';\n throw new Error(\n `No connected ${platformLabel} device found.\\n` +\n ` iOS (simctl): no booted simulator\\n` +\n ` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\\n` +\n 'Boot a simulator/emulator or set DEVICE_ID explicitly.',\n );\n}\n\nasync function detectFromExplicitId(deviceId: string): Promise<DetectedDevice> {\n // UUIDs with dashes are typically iOS simulator UDIDs\n if (/^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/iu.test(deviceId)) {\n return { platform: 'ios', deviceId };\n }\n\n // Android emulator or device serial patterns\n if (deviceId.startsWith('emulator-') || /^[A-Za-z0-9]+$/u.test(deviceId)) {\n return { platform: 'android', deviceId };\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const result = await exec('idb', ['describe', '--udid', deviceId]);\n if (result.exitCode === 0) {\n return { platform: 'ios', deviceId };\n }\n }\n\n if (hasAdb) {\n const result = await exec('adb', ['-s', deviceId, 'get-state']);\n if (result.exitCode === 0) {\n return { platform: 'android', deviceId };\n }\n }\n\n throw new Error(\n `Cannot determine platform for device ID: ${deviceId}. ` +\n 'Ensure the device is connected and idb/adb is available.',\n );\n}\n\ntype SimctlDevice = {\n udid: string;\n name: string;\n state: string;\n isAvailable: boolean;\n};\n\nasync function findAllBootedIOSSimulators(): Promise<DetectedDevice[]> {\n const result = await exec('xcrun', [\n 'simctl',\n 'list',\n 'devices',\n 'booted',\n '--json',\n ]);\n if (result.exitCode !== 0) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(result.stdout) as {\n devices: Record<string, SimctlDevice[]>;\n };\n const devices: DetectedDevice[] = [];\n for (const runtimeDevices of Object.values(parsed.devices)) {\n for (const device of runtimeDevices) {\n if (device.state === 'Booted' && device.isAvailable) {\n devices.push({ platform: 'ios', deviceId: device.udid });\n }\n }\n }\n return devices;\n } catch {\n return [];\n }\n}\n\nfunction findPythonBinDirs(base: string): string[] {\n try {\n return readdirSync(base, { withFileTypes: true })\n .filter(\n (e) =>\n (e.isDirectory() && e.name.startsWith('python')) ||\n /^\\d+\\.\\d+$/u.test(e.name),\n )\n .map((e) => `${base}/${e.name}/bin`);\n } catch {\n return [];\n }\n}\n\nexport async function resolveIdbPath(): Promise<string> {\n const result = await exec('idb', ['--help']).catch(() => null);\n if (result?.exitCode === 0) {\n return 'idb';\n }\n\n const home = process.env.HOME ?? '';\n const candidates = [\n '/usr/local/bin/idb',\n '/opt/homebrew/bin/idb',\n ...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),\n ...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),\n ];\n\n for (const candidate of candidates) {\n const probe = await exec(candidate, ['--help']).catch(() => null);\n if (probe?.exitCode === 0) {\n return candidate;\n }\n }\n\n throw new Error(\n 'idb not found.\\n' +\n 'Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb',\n );\n}\n\nasync function resolveAdbPath(): Promise<string> {\n const result = await exec('adb', ['version']).catch(() => null);\n if (result?.exitCode === 0) {\n return 'adb';\n }\n\n const home = process.env.HOME ?? '';\n const candidates = [\n process.env.ANDROID_HOME &&\n `${process.env.ANDROID_HOME}/platform-tools/adb`,\n process.env.ANDROID_SDK_ROOT &&\n `${process.env.ANDROID_SDK_ROOT}/platform-tools/adb`,\n `${home}/Library/Android/sdk/platform-tools/adb`,\n `${home}/Android/Sdk/platform-tools/adb`,\n ].filter(Boolean) as string[];\n\n for (const candidate of candidates) {\n const probe = await exec(candidate, ['version']).catch(() => null);\n if (probe?.exitCode === 0) {\n return candidate;\n }\n }\n\n throw new Error('adb not found');\n}\n\nasync function findAllConnectedAndroidDevices(): Promise<DetectedDevice[]> {\n const adb = await resolveAdbPath();\n const result = await exec(adb, ['devices']);\n if (result.exitCode !== 0) {\n return [];\n }\n\n const devices: DetectedDevice[] = [];\n const lines = result.stdout.trim().split('\\n').slice(1);\n for (const line of lines) {\n const [serial, state] = line.trim().split(/\\s+/u);\n if (serial && state === 'device') {\n devices.push({ platform: 'android', deviceId: serial });\n }\n }\n return devices;\n}\n"]}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { Platform } from "../backends/types.cjs";
|
|
2
|
-
type DetectedDevice = {
|
|
2
|
+
export type DetectedDevice = {
|
|
3
3
|
platform: Platform;
|
|
4
4
|
deviceId: string;
|
|
5
5
|
};
|
|
6
|
-
export declare
|
|
7
|
-
|
|
6
|
+
export declare class MultipleDevicesError extends Error {
|
|
7
|
+
readonly devices: DetectedDevice[];
|
|
8
|
+
constructor(devices: DetectedDevice[]);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Detect all connected devices across iOS and Android.
|
|
12
|
+
*
|
|
13
|
+
* @param platform - Optional filter to only return devices of a specific platform.
|
|
14
|
+
* @returns Array of detected devices (may be empty).
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectAllDevices(platform?: Platform): Promise<DetectedDevice[]>;
|
|
17
|
+
export declare function detectPlatform(explicitDeviceId?: string, explicitPlatform?: Platform): Promise<DetectedDevice>;
|
|
18
|
+
export declare function resolveIdbPath(): Promise<string>;
|
|
8
19
|
//# sourceMappingURL=platform.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.cts","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"platform.d.cts","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,8BAA6B;AAErD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;gBAEvB,OAAO,EAAE,cAAc,EAAE;CAQtC;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAY3B;AAED,wBAAsB,cAAc,CAClC,gBAAgB,CAAC,EAAE,MAAM,EACzB,gBAAgB,CAAC,EAAE,QAAQ,GAC1B,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAyFD,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAyBtD"}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { Platform } from "../backends/types.mjs";
|
|
2
|
-
type DetectedDevice = {
|
|
2
|
+
export type DetectedDevice = {
|
|
3
3
|
platform: Platform;
|
|
4
4
|
deviceId: string;
|
|
5
5
|
};
|
|
6
|
-
export declare
|
|
7
|
-
|
|
6
|
+
export declare class MultipleDevicesError extends Error {
|
|
7
|
+
readonly devices: DetectedDevice[];
|
|
8
|
+
constructor(devices: DetectedDevice[]);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Detect all connected devices across iOS and Android.
|
|
12
|
+
*
|
|
13
|
+
* @param platform - Optional filter to only return devices of a specific platform.
|
|
14
|
+
* @returns Array of detected devices (may be empty).
|
|
15
|
+
*/
|
|
16
|
+
export declare function detectAllDevices(platform?: Platform): Promise<DetectedDevice[]>;
|
|
17
|
+
export declare function detectPlatform(explicitDeviceId?: string, explicitPlatform?: Platform): Promise<DetectedDevice>;
|
|
18
|
+
export declare function resolveIdbPath(): Promise<string>;
|
|
8
19
|
//# sourceMappingURL=platform.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.mts","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"platform.d.mts","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,8BAA6B;AAErD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;gBAEvB,OAAO,EAAE,cAAc,EAAE;CAQtC;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAY3B;AAED,wBAAsB,cAAc,CAClC,gBAAgB,CAAC,EAAE,MAAM,EACzB,gBAAgB,CAAC,EAAE,QAAQ,GAC1B,OAAO,CAAC,cAAc,CAAC,CAyBzB;AAyFD,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAyBtD"}
|
package/dist/utils/platform.mjs
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
1
2
|
import { exec, isCommandAvailable } from "./exec.mjs";
|
|
2
|
-
export
|
|
3
|
+
export class MultipleDevicesError extends Error {
|
|
4
|
+
constructor(devices) {
|
|
5
|
+
const lines = devices.map((d) => ` ${d.platform}\t${d.deviceId}`);
|
|
6
|
+
super(`Multiple devices detected. Set DEVICE_ID to one of:\n${lines.join('\n')}`);
|
|
7
|
+
this.name = 'MultipleDevicesError';
|
|
8
|
+
this.devices = devices;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Detect all connected devices across iOS and Android.
|
|
13
|
+
*
|
|
14
|
+
* @param platform - Optional filter to only return devices of a specific platform.
|
|
15
|
+
* @returns Array of detected devices (may be empty).
|
|
16
|
+
*/
|
|
17
|
+
export async function detectAllDevices(platform) {
|
|
18
|
+
const searches = [];
|
|
19
|
+
if (platform !== 'android') {
|
|
20
|
+
searches.push(findAllBootedIOSSimulators().catch(() => []));
|
|
21
|
+
}
|
|
22
|
+
if (platform !== 'ios') {
|
|
23
|
+
searches.push(findAllConnectedAndroidDevices().catch(() => []));
|
|
24
|
+
}
|
|
25
|
+
const results = await Promise.all(searches);
|
|
26
|
+
return results.flat();
|
|
27
|
+
}
|
|
28
|
+
export async function detectPlatform(explicitDeviceId, explicitPlatform) {
|
|
3
29
|
if (explicitDeviceId) {
|
|
4
30
|
return detectFromExplicitId(explicitDeviceId);
|
|
5
31
|
}
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
]);
|
|
10
|
-
if (hasIdb) {
|
|
11
|
-
const iosDevice = await findBootedIOSDevice();
|
|
12
|
-
if (iosDevice) {
|
|
13
|
-
return iosDevice;
|
|
14
|
-
}
|
|
32
|
+
const devices = await detectAllDevices(explicitPlatform);
|
|
33
|
+
if (devices.length === 1) {
|
|
34
|
+
return devices[0];
|
|
15
35
|
}
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
if (androidDevice) {
|
|
19
|
-
return androidDevice;
|
|
20
|
-
}
|
|
36
|
+
if (devices.length > 1) {
|
|
37
|
+
throw new MultipleDevicesError(devices);
|
|
21
38
|
}
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
const hasAdb = explicitPlatform === 'ios' ? false : await isCommandAvailable('adb');
|
|
40
|
+
const platformLabel = explicitPlatform ?? 'any';
|
|
41
|
+
throw new Error(`No connected ${platformLabel} device found.\n` +
|
|
42
|
+
` iOS (simctl): no booted simulator\n` +
|
|
24
43
|
` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\n` +
|
|
25
44
|
'Boot a simulator/emulator or set DEVICE_ID explicitly.');
|
|
26
45
|
}
|
|
@@ -52,39 +71,101 @@ async function detectFromExplicitId(deviceId) {
|
|
|
52
71
|
throw new Error(`Cannot determine platform for device ID: ${deviceId}. ` +
|
|
53
72
|
'Ensure the device is connected and idb/adb is available.');
|
|
54
73
|
}
|
|
55
|
-
async function
|
|
56
|
-
const result = await exec('
|
|
74
|
+
async function findAllBootedIOSSimulators() {
|
|
75
|
+
const result = await exec('xcrun', [
|
|
76
|
+
'simctl',
|
|
77
|
+
'list',
|
|
78
|
+
'devices',
|
|
79
|
+
'booted',
|
|
80
|
+
'--json',
|
|
81
|
+
]);
|
|
57
82
|
if (result.exitCode !== 0) {
|
|
58
|
-
return
|
|
83
|
+
return [];
|
|
59
84
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(result.stdout);
|
|
87
|
+
const devices = [];
|
|
88
|
+
for (const runtimeDevices of Object.values(parsed.devices)) {
|
|
89
|
+
for (const device of runtimeDevices) {
|
|
90
|
+
if (device.state === 'Booted' && device.isAvailable) {
|
|
91
|
+
devices.push({ platform: 'ios', deviceId: device.udid });
|
|
92
|
+
}
|
|
68
93
|
}
|
|
69
94
|
}
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
return devices;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function findPythonBinDirs(base) {
|
|
102
|
+
try {
|
|
103
|
+
return readdirSync(base, { withFileTypes: true })
|
|
104
|
+
.filter((e) => (e.isDirectory() && e.name.startsWith('python')) ||
|
|
105
|
+
/^\d+\.\d+$/u.test(e.name))
|
|
106
|
+
.map((e) => `${base}/${e.name}/bin`);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function resolveIdbPath() {
|
|
113
|
+
const result = await exec('idb', ['--help']).catch(() => null);
|
|
114
|
+
if (result?.exitCode === 0) {
|
|
115
|
+
return 'idb';
|
|
116
|
+
}
|
|
117
|
+
const home = process.env.HOME ?? '';
|
|
118
|
+
const candidates = [
|
|
119
|
+
'/usr/local/bin/idb',
|
|
120
|
+
'/opt/homebrew/bin/idb',
|
|
121
|
+
...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),
|
|
122
|
+
...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),
|
|
123
|
+
];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const probe = await exec(candidate, ['--help']).catch(() => null);
|
|
126
|
+
if (probe?.exitCode === 0) {
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new Error('idb not found.\n' +
|
|
131
|
+
'Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb');
|
|
132
|
+
}
|
|
133
|
+
async function resolveAdbPath() {
|
|
134
|
+
const result = await exec('adb', ['version']).catch(() => null);
|
|
135
|
+
if (result?.exitCode === 0) {
|
|
136
|
+
return 'adb';
|
|
137
|
+
}
|
|
138
|
+
const home = process.env.HOME ?? '';
|
|
139
|
+
const candidates = [
|
|
140
|
+
process.env.ANDROID_HOME &&
|
|
141
|
+
`${process.env.ANDROID_HOME}/platform-tools/adb`,
|
|
142
|
+
process.env.ANDROID_SDK_ROOT &&
|
|
143
|
+
`${process.env.ANDROID_SDK_ROOT}/platform-tools/adb`,
|
|
144
|
+
`${home}/Library/Android/sdk/platform-tools/adb`,
|
|
145
|
+
`${home}/Android/Sdk/platform-tools/adb`,
|
|
146
|
+
].filter(Boolean);
|
|
147
|
+
for (const candidate of candidates) {
|
|
148
|
+
const probe = await exec(candidate, ['version']).catch(() => null);
|
|
149
|
+
if (probe?.exitCode === 0) {
|
|
150
|
+
return candidate;
|
|
72
151
|
}
|
|
73
152
|
}
|
|
74
|
-
|
|
153
|
+
throw new Error('adb not found');
|
|
75
154
|
}
|
|
76
|
-
async function
|
|
77
|
-
const
|
|
155
|
+
async function findAllConnectedAndroidDevices() {
|
|
156
|
+
const adb = await resolveAdbPath();
|
|
157
|
+
const result = await exec(adb, ['devices']);
|
|
78
158
|
if (result.exitCode !== 0) {
|
|
79
|
-
return
|
|
159
|
+
return [];
|
|
80
160
|
}
|
|
161
|
+
const devices = [];
|
|
81
162
|
const lines = result.stdout.trim().split('\n').slice(1);
|
|
82
163
|
for (const line of lines) {
|
|
83
164
|
const [serial, state] = line.trim().split(/\s+/u);
|
|
84
165
|
if (serial && state === 'device') {
|
|
85
|
-
|
|
166
|
+
devices.push({ platform: 'android', deviceId: serial });
|
|
86
167
|
}
|
|
87
168
|
}
|
|
88
|
-
return
|
|
169
|
+
return devices;
|
|
89
170
|
}
|
|
90
171
|
//# sourceMappingURL=platform.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.mjs","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,kBAAkB,EAAE,mBAAkB;AAQrD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,gBAAyB;IAEzB,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACzC,kBAAkB,CAAC,KAAK,CAAC;QACzB,kBAAkB,CAAC,KAAK,CAAC;KAC1B,CAAC,CAAC;IAEH,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,SAAS,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC9C,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,aAAa,GAAG,MAAM,0BAA0B,EAAE,CAAC;QACzD,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,aAAa,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,8BAA8B;QAC5B,gBAAgB,MAAM,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,eAAe,IAAI;QAC/E,oBAAoB,MAAM,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,eAAe,IAAI;QACnF,wDAAwD,CAC3D,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IAClD,sDAAsD;IACtD,IAAI,+CAA+C,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IACvC,CAAC;IAED,6CAA6C;IAC7C,IAAI,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACzC,kBAAkB,CAAC,KAAK,CAAC;QACzB,kBAAkB,CAAC,KAAK,CAAC;KAC1B,CAAC,CAAC;IAEH,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnE,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QACvC,CAAC;IACH,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,4CAA4C,QAAQ,IAAI;QACtD,0DAA0D,CAC7D,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,mBAAmB;IAChC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC7D,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YACpD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,0BAA0B;IACvC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC9C,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,MAAM,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { exec, isCommandAvailable } from './exec.js';\nimport type { Platform } from '../backends/types.js';\n\ntype DetectedDevice = {\n platform: Platform;\n deviceId: string;\n};\n\nexport async function detectPlatform(\n explicitDeviceId?: string,\n): Promise<DetectedDevice> {\n if (explicitDeviceId) {\n return detectFromExplicitId(explicitDeviceId);\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const iosDevice = await findBootedIOSDevice();\n if (iosDevice) {\n return iosDevice;\n }\n }\n\n if (hasAdb) {\n const androidDevice = await findConnectedAndroidDevice();\n if (androidDevice) {\n return androidDevice;\n }\n }\n\n throw new Error(\n 'No connected device found.\\n' +\n ` iOS (idb): ${hasIdb ? 'available, no booted simulator' : 'not installed'}\\n` +\n ` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\\n` +\n 'Boot a simulator/emulator or set DEVICE_ID explicitly.',\n );\n}\n\nasync function detectFromExplicitId(deviceId: string): Promise<DetectedDevice> {\n // UUIDs with dashes are typically iOS simulator UDIDs\n if (/^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/iu.test(deviceId)) {\n return { platform: 'ios', deviceId };\n }\n\n // Android emulator or device serial patterns\n if (deviceId.startsWith('emulator-') || /^[A-Za-z0-9]+$/u.test(deviceId)) {\n return { platform: 'android', deviceId };\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const result = await exec('idb', ['describe', '--udid', deviceId]);\n if (result.exitCode === 0) {\n return { platform: 'ios', deviceId };\n }\n }\n\n if (hasAdb) {\n const result = await exec('adb', ['-s', deviceId, 'get-state']);\n if (result.exitCode === 0) {\n return { platform: 'android', deviceId };\n }\n }\n\n throw new Error(\n `Cannot determine platform for device ID: ${deviceId}. ` +\n 'Ensure the device is connected and idb/adb is available.',\n );\n}\n\nasync function findBootedIOSDevice(): Promise<DetectedDevice | null> {\n const result = await exec('idb', ['list-targets', '--json']);\n if (result.exitCode !== 0) {\n return null;\n }\n\n for (const line of result.stdout.trim().split('\\n')) {\n if (!line.trim()) {\n continue;\n }\n try {\n const target = JSON.parse(line);\n if (target.state === 'Booted' && target.type === 'simulator') {\n return { platform: 'ios', deviceId: target.udid };\n }\n } catch {\n continue;\n }\n }\n return null;\n}\n\nasync function findConnectedAndroidDevice(): Promise<DetectedDevice | null> {\n const result = await exec('adb', ['devices']);\n if (result.exitCode !== 0) {\n return null;\n }\n\n const lines = result.stdout.trim().split('\\n').slice(1);\n for (const line of lines) {\n const [serial, state] = line.trim().split(/\\s+/u);\n if (serial && state === 'device') {\n return { platform: 'android', deviceId: serial };\n }\n }\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"platform.mjs","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync } from 'node:fs';\n\nimport { exec, isCommandAvailable } from './exec.js';\nimport type { Platform } from '../backends/types.js';\n\nexport type DetectedDevice = {\n platform: Platform;\n deviceId: string;\n};\n\nexport class MultipleDevicesError extends Error {\n readonly devices: DetectedDevice[];\n\n constructor(devices: DetectedDevice[]) {\n const lines = devices.map((d) => ` ${d.platform}\\t${d.deviceId}`);\n super(\n `Multiple devices detected. Set DEVICE_ID to one of:\\n${lines.join('\\n')}`,\n );\n this.name = 'MultipleDevicesError';\n this.devices = devices;\n }\n}\n\n/**\n * Detect all connected devices across iOS and Android.\n *\n * @param platform - Optional filter to only return devices of a specific platform.\n * @returns Array of detected devices (may be empty).\n */\nexport async function detectAllDevices(\n platform?: Platform,\n): Promise<DetectedDevice[]> {\n const searches: Promise<DetectedDevice[]>[] = [];\n\n if (platform !== 'android') {\n searches.push(findAllBootedIOSSimulators().catch(() => []));\n }\n if (platform !== 'ios') {\n searches.push(findAllConnectedAndroidDevices().catch(() => []));\n }\n\n const results = await Promise.all(searches);\n return results.flat();\n}\n\nexport async function detectPlatform(\n explicitDeviceId?: string,\n explicitPlatform?: Platform,\n): Promise<DetectedDevice> {\n if (explicitDeviceId) {\n return detectFromExplicitId(explicitDeviceId);\n }\n\n const devices = await detectAllDevices(explicitPlatform);\n\n if (devices.length === 1) {\n return devices[0];\n }\n\n if (devices.length > 1) {\n throw new MultipleDevicesError(devices);\n }\n\n const hasAdb =\n explicitPlatform === 'ios' ? false : await isCommandAvailable('adb');\n\n const platformLabel = explicitPlatform ?? 'any';\n throw new Error(\n `No connected ${platformLabel} device found.\\n` +\n ` iOS (simctl): no booted simulator\\n` +\n ` Android (adb): ${hasAdb ? 'available, no connected device' : 'not installed'}\\n` +\n 'Boot a simulator/emulator or set DEVICE_ID explicitly.',\n );\n}\n\nasync function detectFromExplicitId(deviceId: string): Promise<DetectedDevice> {\n // UUIDs with dashes are typically iOS simulator UDIDs\n if (/^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$/iu.test(deviceId)) {\n return { platform: 'ios', deviceId };\n }\n\n // Android emulator or device serial patterns\n if (deviceId.startsWith('emulator-') || /^[A-Za-z0-9]+$/u.test(deviceId)) {\n return { platform: 'android', deviceId };\n }\n\n const [hasIdb, hasAdb] = await Promise.all([\n isCommandAvailable('idb'),\n isCommandAvailable('adb'),\n ]);\n\n if (hasIdb) {\n const result = await exec('idb', ['describe', '--udid', deviceId]);\n if (result.exitCode === 0) {\n return { platform: 'ios', deviceId };\n }\n }\n\n if (hasAdb) {\n const result = await exec('adb', ['-s', deviceId, 'get-state']);\n if (result.exitCode === 0) {\n return { platform: 'android', deviceId };\n }\n }\n\n throw new Error(\n `Cannot determine platform for device ID: ${deviceId}. ` +\n 'Ensure the device is connected and idb/adb is available.',\n );\n}\n\ntype SimctlDevice = {\n udid: string;\n name: string;\n state: string;\n isAvailable: boolean;\n};\n\nasync function findAllBootedIOSSimulators(): Promise<DetectedDevice[]> {\n const result = await exec('xcrun', [\n 'simctl',\n 'list',\n 'devices',\n 'booted',\n '--json',\n ]);\n if (result.exitCode !== 0) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(result.stdout) as {\n devices: Record<string, SimctlDevice[]>;\n };\n const devices: DetectedDevice[] = [];\n for (const runtimeDevices of Object.values(parsed.devices)) {\n for (const device of runtimeDevices) {\n if (device.state === 'Booted' && device.isAvailable) {\n devices.push({ platform: 'ios', deviceId: device.udid });\n }\n }\n }\n return devices;\n } catch {\n return [];\n }\n}\n\nfunction findPythonBinDirs(base: string): string[] {\n try {\n return readdirSync(base, { withFileTypes: true })\n .filter(\n (e) =>\n (e.isDirectory() && e.name.startsWith('python')) ||\n /^\\d+\\.\\d+$/u.test(e.name),\n )\n .map((e) => `${base}/${e.name}/bin`);\n } catch {\n return [];\n }\n}\n\nexport async function resolveIdbPath(): Promise<string> {\n const result = await exec('idb', ['--help']).catch(() => null);\n if (result?.exitCode === 0) {\n return 'idb';\n }\n\n const home = process.env.HOME ?? '';\n const candidates = [\n '/usr/local/bin/idb',\n '/opt/homebrew/bin/idb',\n ...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),\n ...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),\n ];\n\n for (const candidate of candidates) {\n const probe = await exec(candidate, ['--help']).catch(() => null);\n if (probe?.exitCode === 0) {\n return candidate;\n }\n }\n\n throw new Error(\n 'idb not found.\\n' +\n 'Install: brew tap facebook/fb && brew install idb-companion && pip3 install fb-idb',\n );\n}\n\nasync function resolveAdbPath(): Promise<string> {\n const result = await exec('adb', ['version']).catch(() => null);\n if (result?.exitCode === 0) {\n return 'adb';\n }\n\n const home = process.env.HOME ?? '';\n const candidates = [\n process.env.ANDROID_HOME &&\n `${process.env.ANDROID_HOME}/platform-tools/adb`,\n process.env.ANDROID_SDK_ROOT &&\n `${process.env.ANDROID_SDK_ROOT}/platform-tools/adb`,\n `${home}/Library/Android/sdk/platform-tools/adb`,\n `${home}/Android/Sdk/platform-tools/adb`,\n ].filter(Boolean) as string[];\n\n for (const candidate of candidates) {\n const probe = await exec(candidate, ['version']).catch(() => null);\n if (probe?.exitCode === 0) {\n return candidate;\n }\n }\n\n throw new Error('adb not found');\n}\n\nasync function findAllConnectedAndroidDevices(): Promise<DetectedDevice[]> {\n const adb = await resolveAdbPath();\n const result = await exec(adb, ['devices']);\n if (result.exitCode !== 0) {\n return [];\n }\n\n const devices: DetectedDevice[] = [];\n const lines = result.stdout.trim().split('\\n').slice(1);\n for (const line of lines) {\n const [serial, state] = line.trim().split(/\\s+/u);\n if (serial && state === 'device') {\n devices.push({ platform: 'android', deviceId: serial });\n }\n }\n return devices;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask/device-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for mobile device interaction — iOS (IDB), Android (ADB), and Appium/BrowserStack",
|
|
5
5
|
"homepage": "https://github.com/MetaMask/device-mcp#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vite": "^6.4.1",
|
|
87
87
|
"vitest": "^3.2.1"
|
|
88
88
|
},
|
|
89
|
-
"packageManager": "yarn@4.
|
|
89
|
+
"packageManager": "yarn@4.16.0",
|
|
90
90
|
"engines": {
|
|
91
91
|
"node": "^20 || ^22 || >=24"
|
|
92
92
|
},
|