@metamask/device-mcp 0.3.2 → 0.4.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 +18 -1
- package/README.md +110 -21
- package/dist/android/NOTICE.md +65 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk.idsig +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.manifest.json +10 -0
- package/dist/backends/adb-backend.cjs +326 -17
- 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 +327 -18
- package/dist/backends/adb-backend.mjs.map +1 -1
- package/dist/backends/android-instrumentation/adb.cjs +28 -0
- package/dist/backends/android-instrumentation/adb.cjs.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.cts +24 -0
- package/dist/backends/android-instrumentation/adb.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.mts +24 -0
- package/dist/backends/android-instrumentation/adb.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/adb.mjs +25 -0
- package/dist/backends/android-instrumentation/adb.mjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.cjs +139 -0
- package/dist/backends/android-instrumentation/artifact.cjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.cts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.mts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.mjs +154 -0
- package/dist/backends/android-instrumentation/artifact.mjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.cjs +79 -0
- package/dist/backends/android-instrumentation/errors.cjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.cts +68 -0
- package/dist/backends/android-instrumentation/errors.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.mts +68 -0
- package/dist/backends/android-instrumentation/errors.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/errors.mjs +73 -0
- package/dist/backends/android-instrumentation/errors.mjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.cjs +235 -0
- package/dist/backends/android-instrumentation/installer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.cts +123 -0
- package/dist/backends/android-instrumentation/installer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.mts +123 -0
- package/dist/backends/android-instrumentation/installer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/installer.mjs +225 -0
- package/dist/backends/android-instrumentation/installer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.cjs +435 -0
- package/dist/backends/android-instrumentation/signer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.cts +40 -0
- package/dist/backends/android-instrumentation/signer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.mts +40 -0
- package/dist/backends/android-instrumentation/signer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/signer.mjs +431 -0
- package/dist/backends/android-instrumentation/signer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.cjs +159 -0
- package/dist/backends/android-instrumentation/snapshot.cjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.mjs +154 -0
- package/dist/backends/android-instrumentation/snapshot.mjs.map +1 -0
- package/dist/backends/appium-backend.cjs +19 -1
- 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 +19 -1
- package/dist/backends/appium-backend.mjs.map +1 -1
- package/dist/backends/idb-backend.cjs +128 -31
- 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 +129 -32
- package/dist/backends/idb-backend.mjs.map +1 -1
- package/dist/backends/index.cjs +3 -0
- package/dist/backends/index.cjs.map +1 -1
- package/dist/backends/index.d.cts.map +1 -1
- package/dist/backends/index.d.mts.map +1 -1
- package/dist/backends/index.mjs +3 -0
- package/dist/backends/index.mjs.map +1 -1
- package/dist/backends/types.cjs.map +1 -1
- package/dist/backends/types.d.cts +6 -0
- package/dist/backends/types.d.cts.map +1 -1
- package/dist/backends/types.d.mts +6 -0
- package/dist/backends/types.d.mts.map +1 -1
- package/dist/backends/types.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/tools/swipe.cjs +3 -1
- package/dist/tools/swipe.cjs.map +1 -1
- package/dist/tools/swipe.d.cts.map +1 -1
- package/dist/tools/swipe.d.mts.map +1 -1
- package/dist/tools/swipe.mjs +3 -1
- package/dist/tools/swipe.mjs.map +1 -1
- package/dist/utils/output-path.cjs +13 -0
- package/dist/utils/output-path.cjs.map +1 -1
- package/dist/utils/output-path.d.cts +10 -0
- package/dist/utils/output-path.d.cts.map +1 -1
- package/dist/utils/output-path.d.mts +10 -0
- package/dist/utils/output-path.d.mts.map +1 -1
- package/dist/utils/output-path.mjs +12 -0
- package/dist/utils/output-path.mjs.map +1 -1
- package/dist/utils/platform.cjs +65 -7
- package/dist/utils/platform.cjs.map +1 -1
- package/dist/utils/platform.d.cts +33 -0
- package/dist/utils/platform.d.cts.map +1 -1
- package/dist/utils/platform.d.mts +33 -0
- package/dist/utils/platform.d.mts.map +1 -1
- package/dist/utils/platform.mjs +63 -7
- package/dist/utils/platform.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swipe.d.mts","sourceRoot":"","sources":["../../src/tools/swipe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAE1D,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,
|
|
1
|
+
{"version":3,"file":"swipe.d.mts","sourceRoot":"","sources":["../../src/tools/swipe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAE1D,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,CAsCN"}
|
package/dist/tools/swipe.mjs
CHANGED
|
@@ -4,7 +4,9 @@ export function registerSwipeTool(server, backend) {
|
|
|
4
4
|
server.registerTool('device_swipe', {
|
|
5
5
|
title: 'Swipe',
|
|
6
6
|
description: 'Swipe on the device screen. Use natural scrolling: swipe "up" to scroll down. ' +
|
|
7
|
-
'Optionally specify start coordinates and distance.'
|
|
7
|
+
'Optionally specify start coordinates and distance. Coordinates are logical points, ' +
|
|
8
|
+
'not screenshot pixels. The gesture runs with a fixed 0.3s duration; command ' +
|
|
9
|
+
'completion does not verify that content moved — confirm with a screenshot or snapshot.',
|
|
8
10
|
inputSchema: {
|
|
9
11
|
direction: z
|
|
10
12
|
.enum(['up', 'down', 'left', 'right'])
|
package/dist/tools/swipe.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"swipe.mjs","sourceRoot":"","sources":["../../src/tools/swipe.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,YAAY;AAExB,OAAO,EAAE,WAAW,EAAE,qBAAoB;AAG1C,MAAM,UAAU,iBAAiB,CAC/B,MAAiB,EACjB,OAAsB;IAEtB,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,OAAO;QACd,WAAW,EACT,gFAAgF;YAChF,
|
|
1
|
+
{"version":3,"file":"swipe.mjs","sourceRoot":"","sources":["../../src/tools/swipe.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,YAAY;AAExB,OAAO,EAAE,WAAW,EAAE,qBAAoB;AAG1C,MAAM,UAAU,iBAAiB,CAC/B,MAAiB,EACjB,OAAsB;IAEtB,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,KAAK,EAAE,OAAO;QACd,WAAW,EACT,gFAAgF;YAChF,qFAAqF;YACrF,8EAA8E;YAC9E,wFAAwF;QAC1F,WAAW,EAAE;YACX,SAAS,EAAE,CAAC;iBACT,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;iBACrC,QAAQ,CAAC,iBAAiB,CAAC;YAC9B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;YAC5D,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;YAC5D,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,yCAAyC,CAAC;SACvD;KACF,EACD,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;QAChD,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;YACzD,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,UAAU,SAAS,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,KAAK,MAAM,GAAG,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,QAAQ,IAAI,EAAE;qBACnJ;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { errorResult } from './shared.js';\nimport type { DeviceBackend } from '../backends/types.js';\n\nexport function registerSwipeTool(\n server: McpServer,\n backend: DeviceBackend,\n): void {\n server.registerTool(\n 'device_swipe',\n {\n title: 'Swipe',\n description:\n 'Swipe on the device screen. Use natural scrolling: swipe \"up\" to scroll down. ' +\n 'Optionally specify start coordinates and distance. Coordinates are logical points, ' +\n 'not screenshot pixels. The gesture runs with a fixed 0.3s duration; command ' +\n 'completion does not verify that content moved — confirm with a screenshot or snapshot.',\n inputSchema: {\n direction: z\n .enum(['up', 'down', 'left', 'right'])\n .describe('Swipe direction'),\n startX: z.number().optional().describe('Start X coordinate'),\n startY: z.number().optional().describe('Start Y coordinate'),\n distance: z\n .number()\n .optional()\n .describe('Swipe distance in pixels (default: 500)'),\n },\n },\n async ({ direction, startX, startY, distance }) => {\n try {\n await backend.swipe(direction, startX, startY, distance);\n return {\n content: [\n {\n type: 'text' as const,\n text: `Swiped ${direction}${startX === undefined ? '' : ` from (${startX}, ${startY})`}${distance === undefined ? '' : ` distance=${distance}px`}`,\n },\n ],\n };\n } catch (error) {\n return errorResult(error);\n }\n },\n );\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPrivateTempDir = createPrivateTempDir;
|
|
3
4
|
exports.resolveArtifactPath = resolveArtifactPath;
|
|
4
5
|
exports.writeArtifactFile = writeArtifactFile;
|
|
5
6
|
exports.hardenArtifactFile = hardenArtifactFile;
|
|
@@ -43,6 +44,18 @@ function getSessionTempDir() {
|
|
|
43
44
|
}
|
|
44
45
|
return sessionTempDir;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a fresh private temporary directory (`0700`, unpredictable name) and
|
|
49
|
+
* return its absolute path. Unlike {@link getSessionTempDir} this is NOT
|
|
50
|
+
* memoized: each call yields a new directory, so callers can pull and inspect
|
|
51
|
+
* device files in isolation and remove the directory when done.
|
|
52
|
+
*
|
|
53
|
+
* @param prefix - A short label folded into the directory name.
|
|
54
|
+
* @returns The absolute path to the newly created directory.
|
|
55
|
+
*/
|
|
56
|
+
function createPrivateTempDir(prefix) {
|
|
57
|
+
return (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), `device-mcp-${prefix}-`));
|
|
58
|
+
}
|
|
46
59
|
/**
|
|
47
60
|
* Resolve the optional output-directory allowlist from the environment.
|
|
48
61
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-path.cjs","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"output-path.cjs","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { chmodSync, constants as fsConstants, mkdtempSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\n\n/** Kinds of on-disk device artifact and their file extension. */\nconst ARTIFACT_EXTENSION = {\n screenshot: 'png',\n recording: 'mp4',\n} as const;\n\nexport type ArtifactKind = keyof typeof ARTIFACT_EXTENSION;\n\n/** Owner read/write only — device artifacts may contain sensitive UI (seed phrases, keys, balances). */\nconst ARTIFACT_MODE = 0o600;\n\n/**\n * Open flags for writing an artifact: create/truncate, write-only, and\n * `O_NOFOLLOW` so a pre-planted symlink at the destination is never followed\n * (defeats `/tmp` symlink / TOCTOU attacks). `O_NOFOLLOW` is a no-op on\n * platforms that do not define it.\n */\n/* eslint-disable no-bitwise -- POSIX open(2) flags are combined with bitwise OR */\nconst ARTIFACT_WRITE_FLAGS =\n fsConstants.O_WRONLY |\n fsConstants.O_CREAT |\n fsConstants.O_TRUNC |\n (fsConstants.O_NOFOLLOW ?? 0);\n/* eslint-enable no-bitwise */\n\nlet sessionTempDir: string | null = null;\n\n/**\n * Lazily create (once per process) a private temporary directory with\n * owner-only permissions (`0700`) to hold default artifacts. Because the\n * directory itself is unreadable to other users, files placed inside it are\n * protected even when written by an external tool (idb/adb) that we cannot\n * pass secure open flags to.\n *\n * @returns The absolute path to the per-process private temp directory.\n */\nfunction getSessionTempDir(): string {\n if (!sessionTempDir) {\n sessionTempDir = mkdtempSync(join(tmpdir(), 'device-mcp-'));\n }\n return sessionTempDir;\n}\n\n/**\n * Create a fresh private temporary directory (`0700`, unpredictable name) and\n * return its absolute path. Unlike {@link getSessionTempDir} this is NOT\n * memoized: each call yields a new directory, so callers can pull and inspect\n * device files in isolation and remove the directory when done.\n *\n * @param prefix - A short label folded into the directory name.\n * @returns The absolute path to the newly created directory.\n */\nexport function createPrivateTempDir(prefix: string): string {\n return mkdtempSync(join(tmpdir(), `device-mcp-${prefix}-`));\n}\n\n/**\n * Resolve the optional output-directory allowlist from the environment.\n *\n * @returns The absolute allowed base directory, or `null` when unrestricted.\n */\nfunction allowedBaseDir(): string | null {\n const configured = process.env.DEVICE_MCP_OUTPUT_DIR?.trim();\n return configured ? resolve(configured) : null;\n}\n\n/**\n * Reject a caller-supplied path that escapes `DEVICE_MCP_OUTPUT_DIR` when that\n * sandbox is configured. No-op when the env var is unset.\n *\n * @param target - The already-resolved absolute destination path.\n */\nfunction assertWithinAllowedBase(target: string): void {\n const base = allowedBaseDir();\n if (!base) {\n return;\n }\n const rel = relative(base, target);\n if (rel.length === 0 || rel.startsWith('..') || isAbsolute(rel)) {\n throw new Error(\n `Refusing to write outside DEVICE_MCP_OUTPUT_DIR (${base}): ${target}`,\n );\n }\n}\n\n/**\n * Compute a safe absolute path for a device artifact.\n *\n * - When no `outputPath` is given, returns an unpredictable file name inside a\n * private per-process temp directory. This replaces the previous predictable\n * `/tmp/device-mcp-*-${Date.now()}` names, which were vulnerable to symlink\n * and information-disclosure attacks in the shared temp directory.\n * - When an `outputPath` is given, it is rejected if it contains a NUL byte,\n * resolved to an absolute path, and — when `DEVICE_MCP_OUTPUT_DIR` is set —\n * confined to that directory.\n *\n * @param outputPath - Optional caller-supplied destination path.\n * @param kind - The artifact kind, used to pick the default file extension.\n * @returns The resolved, validated absolute path to write to.\n */\nexport function resolveArtifactPath(\n outputPath: string | undefined,\n kind: ArtifactKind,\n): string {\n if (outputPath === undefined) {\n const token = randomBytes(8).toString('hex');\n return join(\n getSessionTempDir(),\n `${kind}-${token}.${ARTIFACT_EXTENSION[kind]}`,\n );\n }\n\n if (outputPath.includes('\\0')) {\n throw new Error('Invalid output path: contains a NUL byte');\n }\n\n const target = resolve(outputPath);\n assertWithinAllowedBase(target);\n return target;\n}\n\n/**\n * Write bytes to an artifact path with hardened permissions and open flags:\n * owner-only (`0600`) and `O_NOFOLLOW`. Use for artifacts we write directly\n * in-process (e.g. the Appium backend decoding a base64 screenshot).\n *\n * @param path - The destination path (should come from `resolveArtifactPath`).\n * @param data - The bytes to write.\n */\nexport async function writeArtifactFile(\n path: string,\n data: Buffer,\n): Promise<void> {\n await writeFile(path, data, {\n mode: ARTIFACT_MODE,\n flag: ARTIFACT_WRITE_FLAGS,\n });\n}\n\n/**\n * Best-effort tightening of permissions on a file written by an external tool\n * (idb/adb create the file themselves, so we cannot pass secure open flags).\n * Silently ignored when unsupported or when the file is absent.\n *\n * @param path - The path of the file to restrict to owner-only access.\n */\nexport function hardenArtifactFile(path: string): void {\n try {\n chmodSync(path, ARTIFACT_MODE);\n } catch {\n // Best-effort: the filesystem may not support chmod (e.g. some mounts on\n // Windows) or the external tool may have failed to create the file.\n }\n}\n"]}
|
|
@@ -4,6 +4,16 @@ declare const ARTIFACT_EXTENSION: {
|
|
|
4
4
|
readonly recording: "mp4";
|
|
5
5
|
};
|
|
6
6
|
export type ArtifactKind = keyof typeof ARTIFACT_EXTENSION;
|
|
7
|
+
/**
|
|
8
|
+
* Create a fresh private temporary directory (`0700`, unpredictable name) and
|
|
9
|
+
* return its absolute path. Unlike {@link getSessionTempDir} this is NOT
|
|
10
|
+
* memoized: each call yields a new directory, so callers can pull and inspect
|
|
11
|
+
* device files in isolation and remove the directory when done.
|
|
12
|
+
*
|
|
13
|
+
* @param prefix - A short label folded into the directory name.
|
|
14
|
+
* @returns The absolute path to the newly created directory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createPrivateTempDir(prefix: string): string;
|
|
7
17
|
/**
|
|
8
18
|
* Compute a safe absolute path for a device artifact.
|
|
9
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-path.d.cts","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"AAMA,iEAAiE;AACjE,QAAA,MAAM,kBAAkB;;;CAGd,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"output-path.d.cts","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"AAMA,iEAAiE;AACjE,QAAA,MAAM,kBAAkB;;;CAGd,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAqC3D;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AA+BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,IAAI,EAAE,YAAY,GACjB,MAAM,CAgBR;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOrD"}
|
|
@@ -4,6 +4,16 @@ declare const ARTIFACT_EXTENSION: {
|
|
|
4
4
|
readonly recording: "mp4";
|
|
5
5
|
};
|
|
6
6
|
export type ArtifactKind = keyof typeof ARTIFACT_EXTENSION;
|
|
7
|
+
/**
|
|
8
|
+
* Create a fresh private temporary directory (`0700`, unpredictable name) and
|
|
9
|
+
* return its absolute path. Unlike {@link getSessionTempDir} this is NOT
|
|
10
|
+
* memoized: each call yields a new directory, so callers can pull and inspect
|
|
11
|
+
* device files in isolation and remove the directory when done.
|
|
12
|
+
*
|
|
13
|
+
* @param prefix - A short label folded into the directory name.
|
|
14
|
+
* @returns The absolute path to the newly created directory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createPrivateTempDir(prefix: string): string;
|
|
7
17
|
/**
|
|
8
18
|
* Compute a safe absolute path for a device artifact.
|
|
9
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-path.d.mts","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"AAMA,iEAAiE;AACjE,QAAA,MAAM,kBAAkB;;;CAGd,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"output-path.d.mts","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"AAMA,iEAAiE;AACjE,QAAA,MAAM,kBAAkB;;;CAGd,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAqC3D;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AA+BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,IAAI,EAAE,YAAY,GACjB,MAAM,CAgBR;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOrD"}
|
|
@@ -38,6 +38,18 @@ function getSessionTempDir() {
|
|
|
38
38
|
}
|
|
39
39
|
return sessionTempDir;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a fresh private temporary directory (`0700`, unpredictable name) and
|
|
43
|
+
* return its absolute path. Unlike {@link getSessionTempDir} this is NOT
|
|
44
|
+
* memoized: each call yields a new directory, so callers can pull and inspect
|
|
45
|
+
* device files in isolation and remove the directory when done.
|
|
46
|
+
*
|
|
47
|
+
* @param prefix - A short label folded into the directory name.
|
|
48
|
+
* @returns The absolute path to the newly created directory.
|
|
49
|
+
*/
|
|
50
|
+
export function createPrivateTempDir(prefix) {
|
|
51
|
+
return mkdtempSync(join(tmpdir(), `device-mcp-${prefix}-`));
|
|
52
|
+
}
|
|
41
53
|
/**
|
|
42
54
|
* Resolve the optional output-directory allowlist from the environment.
|
|
43
55
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-path.mjs","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { chmodSync, constants as fsConstants, mkdtempSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\n\n/** Kinds of on-disk device artifact and their file extension. */\nconst ARTIFACT_EXTENSION = {\n screenshot: 'png',\n recording: 'mp4',\n} as const;\n\nexport type ArtifactKind = keyof typeof ARTIFACT_EXTENSION;\n\n/** Owner read/write only — device artifacts may contain sensitive UI (seed phrases, keys, balances). */\nconst ARTIFACT_MODE = 0o600;\n\n/**\n * Open flags for writing an artifact: create/truncate, write-only, and\n * `O_NOFOLLOW` so a pre-planted symlink at the destination is never followed\n * (defeats `/tmp` symlink / TOCTOU attacks). `O_NOFOLLOW` is a no-op on\n * platforms that do not define it.\n */\n/* eslint-disable no-bitwise -- POSIX open(2) flags are combined with bitwise OR */\nconst ARTIFACT_WRITE_FLAGS =\n fsConstants.O_WRONLY |\n fsConstants.O_CREAT |\n fsConstants.O_TRUNC |\n (fsConstants.O_NOFOLLOW ?? 0);\n/* eslint-enable no-bitwise */\n\nlet sessionTempDir: string | null = null;\n\n/**\n * Lazily create (once per process) a private temporary directory with\n * owner-only permissions (`0700`) to hold default artifacts. Because the\n * directory itself is unreadable to other users, files placed inside it are\n * protected even when written by an external tool (idb/adb) that we cannot\n * pass secure open flags to.\n *\n * @returns The absolute path to the per-process private temp directory.\n */\nfunction getSessionTempDir(): string {\n if (!sessionTempDir) {\n sessionTempDir = mkdtempSync(join(tmpdir(), 'device-mcp-'));\n }\n return sessionTempDir;\n}\n\n/**\n * Resolve the optional output-directory allowlist from the environment.\n *\n * @returns The absolute allowed base directory, or `null` when unrestricted.\n */\nfunction allowedBaseDir(): string | null {\n const configured = process.env.DEVICE_MCP_OUTPUT_DIR?.trim();\n return configured ? resolve(configured) : null;\n}\n\n/**\n * Reject a caller-supplied path that escapes `DEVICE_MCP_OUTPUT_DIR` when that\n * sandbox is configured. No-op when the env var is unset.\n *\n * @param target - The already-resolved absolute destination path.\n */\nfunction assertWithinAllowedBase(target: string): void {\n const base = allowedBaseDir();\n if (!base) {\n return;\n }\n const rel = relative(base, target);\n if (rel.length === 0 || rel.startsWith('..') || isAbsolute(rel)) {\n throw new Error(\n `Refusing to write outside DEVICE_MCP_OUTPUT_DIR (${base}): ${target}`,\n );\n }\n}\n\n/**\n * Compute a safe absolute path for a device artifact.\n *\n * - When no `outputPath` is given, returns an unpredictable file name inside a\n * private per-process temp directory. This replaces the previous predictable\n * `/tmp/device-mcp-*-${Date.now()}` names, which were vulnerable to symlink\n * and information-disclosure attacks in the shared temp directory.\n * - When an `outputPath` is given, it is rejected if it contains a NUL byte,\n * resolved to an absolute path, and — when `DEVICE_MCP_OUTPUT_DIR` is set —\n * confined to that directory.\n *\n * @param outputPath - Optional caller-supplied destination path.\n * @param kind - The artifact kind, used to pick the default file extension.\n * @returns The resolved, validated absolute path to write to.\n */\nexport function resolveArtifactPath(\n outputPath: string | undefined,\n kind: ArtifactKind,\n): string {\n if (outputPath === undefined) {\n const token = randomBytes(8).toString('hex');\n return join(\n getSessionTempDir(),\n `${kind}-${token}.${ARTIFACT_EXTENSION[kind]}`,\n );\n }\n\n if (outputPath.includes('\\0')) {\n throw new Error('Invalid output path: contains a NUL byte');\n }\n\n const target = resolve(outputPath);\n assertWithinAllowedBase(target);\n return target;\n}\n\n/**\n * Write bytes to an artifact path with hardened permissions and open flags:\n * owner-only (`0600`) and `O_NOFOLLOW`. Use for artifacts we write directly\n * in-process (e.g. the Appium backend decoding a base64 screenshot).\n *\n * @param path - The destination path (should come from `resolveArtifactPath`).\n * @param data - The bytes to write.\n */\nexport async function writeArtifactFile(\n path: string,\n data: Buffer,\n): Promise<void> {\n await writeFile(path, data, {\n mode: ARTIFACT_MODE,\n flag: ARTIFACT_WRITE_FLAGS,\n });\n}\n\n/**\n * Best-effort tightening of permissions on a file written by an external tool\n * (idb/adb create the file themselves, so we cannot pass secure open flags).\n * Silently ignored when unsupported or when the file is absent.\n *\n * @param path - The path of the file to restrict to owner-only access.\n */\nexport function hardenArtifactFile(path: string): void {\n try {\n chmodSync(path, ARTIFACT_MODE);\n } catch {\n // Best-effort: the filesystem may not support chmod (e.g. some mounts on\n // Windows) or the external tool may have failed to create the file.\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"output-path.mjs","sourceRoot":"","sources":["../../src/utils/output-path.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { chmodSync, constants as fsConstants, mkdtempSync } from 'node:fs';\nimport { writeFile } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\n\n/** Kinds of on-disk device artifact and their file extension. */\nconst ARTIFACT_EXTENSION = {\n screenshot: 'png',\n recording: 'mp4',\n} as const;\n\nexport type ArtifactKind = keyof typeof ARTIFACT_EXTENSION;\n\n/** Owner read/write only — device artifacts may contain sensitive UI (seed phrases, keys, balances). */\nconst ARTIFACT_MODE = 0o600;\n\n/**\n * Open flags for writing an artifact: create/truncate, write-only, and\n * `O_NOFOLLOW` so a pre-planted symlink at the destination is never followed\n * (defeats `/tmp` symlink / TOCTOU attacks). `O_NOFOLLOW` is a no-op on\n * platforms that do not define it.\n */\n/* eslint-disable no-bitwise -- POSIX open(2) flags are combined with bitwise OR */\nconst ARTIFACT_WRITE_FLAGS =\n fsConstants.O_WRONLY |\n fsConstants.O_CREAT |\n fsConstants.O_TRUNC |\n (fsConstants.O_NOFOLLOW ?? 0);\n/* eslint-enable no-bitwise */\n\nlet sessionTempDir: string | null = null;\n\n/**\n * Lazily create (once per process) a private temporary directory with\n * owner-only permissions (`0700`) to hold default artifacts. Because the\n * directory itself is unreadable to other users, files placed inside it are\n * protected even when written by an external tool (idb/adb) that we cannot\n * pass secure open flags to.\n *\n * @returns The absolute path to the per-process private temp directory.\n */\nfunction getSessionTempDir(): string {\n if (!sessionTempDir) {\n sessionTempDir = mkdtempSync(join(tmpdir(), 'device-mcp-'));\n }\n return sessionTempDir;\n}\n\n/**\n * Create a fresh private temporary directory (`0700`, unpredictable name) and\n * return its absolute path. Unlike {@link getSessionTempDir} this is NOT\n * memoized: each call yields a new directory, so callers can pull and inspect\n * device files in isolation and remove the directory when done.\n *\n * @param prefix - A short label folded into the directory name.\n * @returns The absolute path to the newly created directory.\n */\nexport function createPrivateTempDir(prefix: string): string {\n return mkdtempSync(join(tmpdir(), `device-mcp-${prefix}-`));\n}\n\n/**\n * Resolve the optional output-directory allowlist from the environment.\n *\n * @returns The absolute allowed base directory, or `null` when unrestricted.\n */\nfunction allowedBaseDir(): string | null {\n const configured = process.env.DEVICE_MCP_OUTPUT_DIR?.trim();\n return configured ? resolve(configured) : null;\n}\n\n/**\n * Reject a caller-supplied path that escapes `DEVICE_MCP_OUTPUT_DIR` when that\n * sandbox is configured. No-op when the env var is unset.\n *\n * @param target - The already-resolved absolute destination path.\n */\nfunction assertWithinAllowedBase(target: string): void {\n const base = allowedBaseDir();\n if (!base) {\n return;\n }\n const rel = relative(base, target);\n if (rel.length === 0 || rel.startsWith('..') || isAbsolute(rel)) {\n throw new Error(\n `Refusing to write outside DEVICE_MCP_OUTPUT_DIR (${base}): ${target}`,\n );\n }\n}\n\n/**\n * Compute a safe absolute path for a device artifact.\n *\n * - When no `outputPath` is given, returns an unpredictable file name inside a\n * private per-process temp directory. This replaces the previous predictable\n * `/tmp/device-mcp-*-${Date.now()}` names, which were vulnerable to symlink\n * and information-disclosure attacks in the shared temp directory.\n * - When an `outputPath` is given, it is rejected if it contains a NUL byte,\n * resolved to an absolute path, and — when `DEVICE_MCP_OUTPUT_DIR` is set —\n * confined to that directory.\n *\n * @param outputPath - Optional caller-supplied destination path.\n * @param kind - The artifact kind, used to pick the default file extension.\n * @returns The resolved, validated absolute path to write to.\n */\nexport function resolveArtifactPath(\n outputPath: string | undefined,\n kind: ArtifactKind,\n): string {\n if (outputPath === undefined) {\n const token = randomBytes(8).toString('hex');\n return join(\n getSessionTempDir(),\n `${kind}-${token}.${ARTIFACT_EXTENSION[kind]}`,\n );\n }\n\n if (outputPath.includes('\\0')) {\n throw new Error('Invalid output path: contains a NUL byte');\n }\n\n const target = resolve(outputPath);\n assertWithinAllowedBase(target);\n return target;\n}\n\n/**\n * Write bytes to an artifact path with hardened permissions and open flags:\n * owner-only (`0600`) and `O_NOFOLLOW`. Use for artifacts we write directly\n * in-process (e.g. the Appium backend decoding a base64 screenshot).\n *\n * @param path - The destination path (should come from `resolveArtifactPath`).\n * @param data - The bytes to write.\n */\nexport async function writeArtifactFile(\n path: string,\n data: Buffer,\n): Promise<void> {\n await writeFile(path, data, {\n mode: ARTIFACT_MODE,\n flag: ARTIFACT_WRITE_FLAGS,\n });\n}\n\n/**\n * Best-effort tightening of permissions on a file written by an external tool\n * (idb/adb create the file themselves, so we cannot pass secure open flags).\n * Silently ignored when unsupported or when the file is absent.\n *\n * @param path - The path of the file to restrict to owner-only access.\n */\nexport function hardenArtifactFile(path: string): void {\n try {\n chmodSync(path, ARTIFACT_MODE);\n } catch {\n // Best-effort: the filesystem may not support chmod (e.g. some mounts on\n // Windows) or the external tool may have failed to create the file.\n }\n}\n"]}
|
package/dist/utils/platform.cjs
CHANGED
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MultipleDevicesError = void 0;
|
|
4
4
|
exports.detectAllDevices = detectAllDevices;
|
|
5
5
|
exports.detectPlatform = detectPlatform;
|
|
6
|
+
exports.idbSupportsApiSelection = idbSupportsApiSelection;
|
|
6
7
|
exports.resolveIdbPath = resolveIdbPath;
|
|
8
|
+
exports.chooseAxApi = chooseAxApi;
|
|
7
9
|
const node_fs_1 = require("node:fs");
|
|
8
10
|
const exec_js_1 = require("./exec.cjs");
|
|
9
11
|
class MultipleDevicesError extends Error {
|
|
@@ -116,26 +118,82 @@ function findPythonBinDirs(base) {
|
|
|
116
118
|
return [];
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Whether an `idb` binary supports the `--api` describe-all backend selector.
|
|
123
|
+
* Only the unified `idb` (>= ~1.5) exposes it; the legacy python `fb-idb`
|
|
124
|
+
* rejects `--api` as a usage error, so we probe the help text.
|
|
125
|
+
*
|
|
126
|
+
* @param idbPath - The `idb` executable to probe.
|
|
127
|
+
* @returns True when `--api` is accepted.
|
|
128
|
+
*/
|
|
129
|
+
async function idbSupportsApiSelection(idbPath) {
|
|
130
|
+
const help = await (0, exec_js_1.exec)(idbPath, ['ui', 'describe-all', '--help']).catch(() => null);
|
|
131
|
+
if (help?.exitCode !== 0) {
|
|
132
|
+
return false;
|
|
123
133
|
}
|
|
134
|
+
return `${help.stdout}\n${help.stderr}`.includes('--api');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the `idb` executable, preferring one that supports `--api`.
|
|
138
|
+
*
|
|
139
|
+
* A machine can have both the legacy python `fb-idb` (often first on `PATH`)
|
|
140
|
+
* and the unified `facebook/fb/idb`; only the latter supports `--api axbridge`,
|
|
141
|
+
* which iOS 17+ simulators require. We return the first `--api`-capable
|
|
142
|
+
* candidate, falling back to the first merely-working one.
|
|
143
|
+
*
|
|
144
|
+
* @returns The path to the best available `idb`.
|
|
145
|
+
* @throws When no `idb` executable is found.
|
|
146
|
+
*/
|
|
147
|
+
async function resolveIdbPath() {
|
|
124
148
|
const home = process.env.HOME ?? '';
|
|
125
149
|
const candidates = [
|
|
126
|
-
'
|
|
150
|
+
'idb',
|
|
127
151
|
'/opt/homebrew/bin/idb',
|
|
152
|
+
'/usr/local/bin/idb',
|
|
128
153
|
...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),
|
|
129
154
|
...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),
|
|
130
155
|
];
|
|
156
|
+
let firstWorking = null;
|
|
131
157
|
for (const candidate of candidates) {
|
|
132
158
|
const probe = await (0, exec_js_1.exec)(candidate, ['--help']).catch(() => null);
|
|
133
|
-
if (probe?.exitCode
|
|
159
|
+
if (probe?.exitCode !== 0) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (firstWorking === null) {
|
|
163
|
+
firstWorking = candidate;
|
|
164
|
+
}
|
|
165
|
+
if (await idbSupportsApiSelection(candidate)) {
|
|
134
166
|
return candidate;
|
|
135
167
|
}
|
|
136
168
|
}
|
|
169
|
+
if (firstWorking !== null) {
|
|
170
|
+
return firstWorking;
|
|
171
|
+
}
|
|
137
172
|
throw new Error('idb not found.\n' +
|
|
138
|
-
'Install: brew tap facebook/fb && brew install idb
|
|
173
|
+
'Install the unified client: brew tap facebook/fb && brew install idb');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The `--api` accessibility backend for a given iOS runtime.
|
|
177
|
+
*
|
|
178
|
+
* The backends are version-inverted: `ax` works on iOS <= 17 but is broken on
|
|
179
|
+
* iOS 26; `axbridge` works on iOS >= 18. So we pick `axbridge` for >= 18 and
|
|
180
|
+
* `ax` otherwise, or `null` (omit `--api`) when the CLI cannot select. The
|
|
181
|
+
* caller still falls back if the chosen backend errors.
|
|
182
|
+
*
|
|
183
|
+
* @param osVersion - The device OS version (e.g. '17.4', '26.1').
|
|
184
|
+
* @param supportsApi - Whether the resolved CLI accepts `--api`.
|
|
185
|
+
* @returns The `--api` value, or `null` to omit it.
|
|
186
|
+
*/
|
|
187
|
+
function chooseAxApi(osVersion, supportsApi) {
|
|
188
|
+
if (!supportsApi) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const major = Number.parseInt(osVersion, 10);
|
|
192
|
+
if (Number.isNaN(major)) {
|
|
193
|
+
// Unknown version - prefer the modern reader and let the caller fall back
|
|
194
|
+
return 'axbridge';
|
|
195
|
+
}
|
|
196
|
+
return major >= 18 ? 'axbridge' : 'ax';
|
|
139
197
|
}
|
|
140
198
|
async function resolveAdbPath() {
|
|
141
199
|
const result = await (0, exec_js_1.exec)('adb', ['version']).catch(() => null);
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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\n/**\n * Whether an `idb` binary supports the `--api` describe-all backend selector.\n * Only the unified `idb` (>= ~1.5) exposes it; the legacy python `fb-idb`\n * rejects `--api` as a usage error, so we probe the help text.\n *\n * @param idbPath - The `idb` executable to probe.\n * @returns True when `--api` is accepted.\n */\nexport async function idbSupportsApiSelection(\n idbPath: string,\n): Promise<boolean> {\n const help = await exec(idbPath, ['ui', 'describe-all', '--help']).catch(\n () => null,\n );\n if (help?.exitCode !== 0) {\n return false;\n }\n return `${help.stdout}\\n${help.stderr}`.includes('--api');\n}\n\n/**\n * Resolve the `idb` executable, preferring one that supports `--api`.\n *\n * A machine can have both the legacy python `fb-idb` (often first on `PATH`)\n * and the unified `facebook/fb/idb`; only the latter supports `--api axbridge`,\n * which iOS 17+ simulators require. We return the first `--api`-capable\n * candidate, falling back to the first merely-working one.\n *\n * @returns The path to the best available `idb`.\n * @throws When no `idb` executable is found.\n */\nexport async function resolveIdbPath(): Promise<string> {\n const home = process.env.HOME ?? '';\n const candidates = [\n 'idb',\n '/opt/homebrew/bin/idb',\n '/usr/local/bin/idb',\n ...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),\n ...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),\n ];\n\n let firstWorking: string | null = null;\n for (const candidate of candidates) {\n const probe = await exec(candidate, ['--help']).catch(() => null);\n if (probe?.exitCode !== 0) {\n continue;\n }\n if (firstWorking === null) {\n firstWorking = candidate;\n }\n if (await idbSupportsApiSelection(candidate)) {\n return candidate;\n }\n }\n\n if (firstWorking !== null) {\n return firstWorking;\n }\n\n throw new Error(\n 'idb not found.\\n' +\n 'Install the unified client: brew tap facebook/fb && brew install idb',\n );\n}\n\n/**\n * The `--api` accessibility backend for a given iOS runtime.\n *\n * The backends are version-inverted: `ax` works on iOS <= 17 but is broken on\n * iOS 26; `axbridge` works on iOS >= 18. So we pick `axbridge` for >= 18 and\n * `ax` otherwise, or `null` (omit `--api`) when the CLI cannot select. The\n * caller still falls back if the chosen backend errors.\n *\n * @param osVersion - The device OS version (e.g. '17.4', '26.1').\n * @param supportsApi - Whether the resolved CLI accepts `--api`.\n * @returns The `--api` value, or `null` to omit it.\n */\nexport function chooseAxApi(\n osVersion: string,\n supportsApi: boolean,\n): 'ax' | 'axbridge' | null {\n if (!supportsApi) {\n return null;\n }\n const major = Number.parseInt(osVersion, 10);\n if (Number.isNaN(major)) {\n // Unknown version - prefer the modern reader and let the caller fall back\n return 'axbridge';\n }\n return major >= 18 ? 'axbridge' : 'ax';\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"]}
|
|
@@ -15,5 +15,38 @@ export declare class MultipleDevicesError extends Error {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function detectAllDevices(platform?: Platform): Promise<DetectedDevice[]>;
|
|
17
17
|
export declare function detectPlatform(explicitDeviceId?: string, explicitPlatform?: Platform): Promise<DetectedDevice>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether an `idb` binary supports the `--api` describe-all backend selector.
|
|
20
|
+
* Only the unified `idb` (>= ~1.5) exposes it; the legacy python `fb-idb`
|
|
21
|
+
* rejects `--api` as a usage error, so we probe the help text.
|
|
22
|
+
*
|
|
23
|
+
* @param idbPath - The `idb` executable to probe.
|
|
24
|
+
* @returns True when `--api` is accepted.
|
|
25
|
+
*/
|
|
26
|
+
export declare function idbSupportsApiSelection(idbPath: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the `idb` executable, preferring one that supports `--api`.
|
|
29
|
+
*
|
|
30
|
+
* A machine can have both the legacy python `fb-idb` (often first on `PATH`)
|
|
31
|
+
* and the unified `facebook/fb/idb`; only the latter supports `--api axbridge`,
|
|
32
|
+
* which iOS 17+ simulators require. We return the first `--api`-capable
|
|
33
|
+
* candidate, falling back to the first merely-working one.
|
|
34
|
+
*
|
|
35
|
+
* @returns The path to the best available `idb`.
|
|
36
|
+
* @throws When no `idb` executable is found.
|
|
37
|
+
*/
|
|
18
38
|
export declare function resolveIdbPath(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* The `--api` accessibility backend for a given iOS runtime.
|
|
41
|
+
*
|
|
42
|
+
* The backends are version-inverted: `ax` works on iOS <= 17 but is broken on
|
|
43
|
+
* iOS 26; `axbridge` works on iOS >= 18. So we pick `axbridge` for >= 18 and
|
|
44
|
+
* `ax` otherwise, or `null` (omit `--api`) when the CLI cannot select. The
|
|
45
|
+
* caller still falls back if the chosen backend errors.
|
|
46
|
+
*
|
|
47
|
+
* @param osVersion - The device OS version (e.g. '17.4', '26.1').
|
|
48
|
+
* @param supportsApi - Whether the resolved CLI accepts `--api`.
|
|
49
|
+
* @returns The `--api` value, or `null` to omit it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function chooseAxApi(osVersion: string, supportsApi: boolean): 'ax' | 'axbridge' | null;
|
|
19
52
|
//# sourceMappingURL=platform.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAgCtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,OAAO,GACnB,IAAI,GAAG,UAAU,GAAG,IAAI,CAU1B"}
|
|
@@ -15,5 +15,38 @@ export declare class MultipleDevicesError extends Error {
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function detectAllDevices(platform?: Platform): Promise<DetectedDevice[]>;
|
|
17
17
|
export declare function detectPlatform(explicitDeviceId?: string, explicitPlatform?: Platform): Promise<DetectedDevice>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether an `idb` binary supports the `--api` describe-all backend selector.
|
|
20
|
+
* Only the unified `idb` (>= ~1.5) exposes it; the legacy python `fb-idb`
|
|
21
|
+
* rejects `--api` as a usage error, so we probe the help text.
|
|
22
|
+
*
|
|
23
|
+
* @param idbPath - The `idb` executable to probe.
|
|
24
|
+
* @returns True when `--api` is accepted.
|
|
25
|
+
*/
|
|
26
|
+
export declare function idbSupportsApiSelection(idbPath: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the `idb` executable, preferring one that supports `--api`.
|
|
29
|
+
*
|
|
30
|
+
* A machine can have both the legacy python `fb-idb` (often first on `PATH`)
|
|
31
|
+
* and the unified `facebook/fb/idb`; only the latter supports `--api axbridge`,
|
|
32
|
+
* which iOS 17+ simulators require. We return the first `--api`-capable
|
|
33
|
+
* candidate, falling back to the first merely-working one.
|
|
34
|
+
*
|
|
35
|
+
* @returns The path to the best available `idb`.
|
|
36
|
+
* @throws When no `idb` executable is found.
|
|
37
|
+
*/
|
|
18
38
|
export declare function resolveIdbPath(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* The `--api` accessibility backend for a given iOS runtime.
|
|
41
|
+
*
|
|
42
|
+
* The backends are version-inverted: `ax` works on iOS <= 17 but is broken on
|
|
43
|
+
* iOS 26; `axbridge` works on iOS >= 18. So we pick `axbridge` for >= 18 and
|
|
44
|
+
* `ax` otherwise, or `null` (omit `--api`) when the CLI cannot select. The
|
|
45
|
+
* caller still falls back if the chosen backend errors.
|
|
46
|
+
*
|
|
47
|
+
* @param osVersion - The device OS version (e.g. '17.4', '26.1').
|
|
48
|
+
* @param supportsApi - Whether the resolved CLI accepts `--api`.
|
|
49
|
+
* @returns The `--api` value, or `null` to omit it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function chooseAxApi(osVersion: string, supportsApi: boolean): 'ax' | 'axbridge' | null;
|
|
19
52
|
//# sourceMappingURL=platform.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAgCtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,OAAO,GACnB,IAAI,GAAG,UAAU,GAAG,IAAI,CAU1B"}
|
package/dist/utils/platform.mjs
CHANGED
|
@@ -109,26 +109,82 @@ function findPythonBinDirs(base) {
|
|
|
109
109
|
return [];
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Whether an `idb` binary supports the `--api` describe-all backend selector.
|
|
114
|
+
* Only the unified `idb` (>= ~1.5) exposes it; the legacy python `fb-idb`
|
|
115
|
+
* rejects `--api` as a usage error, so we probe the help text.
|
|
116
|
+
*
|
|
117
|
+
* @param idbPath - The `idb` executable to probe.
|
|
118
|
+
* @returns True when `--api` is accepted.
|
|
119
|
+
*/
|
|
120
|
+
export async function idbSupportsApiSelection(idbPath) {
|
|
121
|
+
const help = await exec(idbPath, ['ui', 'describe-all', '--help']).catch(() => null);
|
|
122
|
+
if (help?.exitCode !== 0) {
|
|
123
|
+
return false;
|
|
116
124
|
}
|
|
125
|
+
return `${help.stdout}\n${help.stderr}`.includes('--api');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the `idb` executable, preferring one that supports `--api`.
|
|
129
|
+
*
|
|
130
|
+
* A machine can have both the legacy python `fb-idb` (often first on `PATH`)
|
|
131
|
+
* and the unified `facebook/fb/idb`; only the latter supports `--api axbridge`,
|
|
132
|
+
* which iOS 17+ simulators require. We return the first `--api`-capable
|
|
133
|
+
* candidate, falling back to the first merely-working one.
|
|
134
|
+
*
|
|
135
|
+
* @returns The path to the best available `idb`.
|
|
136
|
+
* @throws When no `idb` executable is found.
|
|
137
|
+
*/
|
|
138
|
+
export async function resolveIdbPath() {
|
|
117
139
|
const home = process.env.HOME ?? '';
|
|
118
140
|
const candidates = [
|
|
119
|
-
'
|
|
141
|
+
'idb',
|
|
120
142
|
'/opt/homebrew/bin/idb',
|
|
143
|
+
'/usr/local/bin/idb',
|
|
121
144
|
...findPythonBinDirs(`${home}/Library/Python`).map((d) => `${d}/idb`),
|
|
122
145
|
...findPythonBinDirs('/usr/local/lib').map((d) => `${d}/idb`),
|
|
123
146
|
];
|
|
147
|
+
let firstWorking = null;
|
|
124
148
|
for (const candidate of candidates) {
|
|
125
149
|
const probe = await exec(candidate, ['--help']).catch(() => null);
|
|
126
|
-
if (probe?.exitCode
|
|
150
|
+
if (probe?.exitCode !== 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (firstWorking === null) {
|
|
154
|
+
firstWorking = candidate;
|
|
155
|
+
}
|
|
156
|
+
if (await idbSupportsApiSelection(candidate)) {
|
|
127
157
|
return candidate;
|
|
128
158
|
}
|
|
129
159
|
}
|
|
160
|
+
if (firstWorking !== null) {
|
|
161
|
+
return firstWorking;
|
|
162
|
+
}
|
|
130
163
|
throw new Error('idb not found.\n' +
|
|
131
|
-
'Install: brew tap facebook/fb && brew install idb
|
|
164
|
+
'Install the unified client: brew tap facebook/fb && brew install idb');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The `--api` accessibility backend for a given iOS runtime.
|
|
168
|
+
*
|
|
169
|
+
* The backends are version-inverted: `ax` works on iOS <= 17 but is broken on
|
|
170
|
+
* iOS 26; `axbridge` works on iOS >= 18. So we pick `axbridge` for >= 18 and
|
|
171
|
+
* `ax` otherwise, or `null` (omit `--api`) when the CLI cannot select. The
|
|
172
|
+
* caller still falls back if the chosen backend errors.
|
|
173
|
+
*
|
|
174
|
+
* @param osVersion - The device OS version (e.g. '17.4', '26.1').
|
|
175
|
+
* @param supportsApi - Whether the resolved CLI accepts `--api`.
|
|
176
|
+
* @returns The `--api` value, or `null` to omit it.
|
|
177
|
+
*/
|
|
178
|
+
export function chooseAxApi(osVersion, supportsApi) {
|
|
179
|
+
if (!supportsApi) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const major = Number.parseInt(osVersion, 10);
|
|
183
|
+
if (Number.isNaN(major)) {
|
|
184
|
+
// Unknown version - prefer the modern reader and let the caller fall back
|
|
185
|
+
return 'axbridge';
|
|
186
|
+
}
|
|
187
|
+
return major >= 18 ? 'axbridge' : 'ax';
|
|
132
188
|
}
|
|
133
189
|
async function resolveAdbPath() {
|
|
134
190
|
const result = await exec('adb', ['version']).catch(() => null);
|