@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +110 -21
  3. package/dist/android/NOTICE.md +65 -0
  4. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk +0 -0
  5. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk.idsig +0 -0
  6. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.manifest.json +10 -0
  7. package/dist/backends/adb-backend.cjs +326 -17
  8. package/dist/backends/adb-backend.cjs.map +1 -1
  9. package/dist/backends/adb-backend.d.cts +1 -0
  10. package/dist/backends/adb-backend.d.cts.map +1 -1
  11. package/dist/backends/adb-backend.d.mts +1 -0
  12. package/dist/backends/adb-backend.d.mts.map +1 -1
  13. package/dist/backends/adb-backend.mjs +327 -18
  14. package/dist/backends/adb-backend.mjs.map +1 -1
  15. package/dist/backends/android-instrumentation/adb.cjs +28 -0
  16. package/dist/backends/android-instrumentation/adb.cjs.map +1 -0
  17. package/dist/backends/android-instrumentation/adb.d.cts +24 -0
  18. package/dist/backends/android-instrumentation/adb.d.cts.map +1 -0
  19. package/dist/backends/android-instrumentation/adb.d.mts +24 -0
  20. package/dist/backends/android-instrumentation/adb.d.mts.map +1 -0
  21. package/dist/backends/android-instrumentation/adb.mjs +25 -0
  22. package/dist/backends/android-instrumentation/adb.mjs.map +1 -0
  23. package/dist/backends/android-instrumentation/artifact.cjs +139 -0
  24. package/dist/backends/android-instrumentation/artifact.cjs.map +1 -0
  25. package/dist/backends/android-instrumentation/artifact.d.cts +28 -0
  26. package/dist/backends/android-instrumentation/artifact.d.cts.map +1 -0
  27. package/dist/backends/android-instrumentation/artifact.d.mts +28 -0
  28. package/dist/backends/android-instrumentation/artifact.d.mts.map +1 -0
  29. package/dist/backends/android-instrumentation/artifact.mjs +154 -0
  30. package/dist/backends/android-instrumentation/artifact.mjs.map +1 -0
  31. package/dist/backends/android-instrumentation/errors.cjs +79 -0
  32. package/dist/backends/android-instrumentation/errors.cjs.map +1 -0
  33. package/dist/backends/android-instrumentation/errors.d.cts +68 -0
  34. package/dist/backends/android-instrumentation/errors.d.cts.map +1 -0
  35. package/dist/backends/android-instrumentation/errors.d.mts +68 -0
  36. package/dist/backends/android-instrumentation/errors.d.mts.map +1 -0
  37. package/dist/backends/android-instrumentation/errors.mjs +73 -0
  38. package/dist/backends/android-instrumentation/errors.mjs.map +1 -0
  39. package/dist/backends/android-instrumentation/installer.cjs +235 -0
  40. package/dist/backends/android-instrumentation/installer.cjs.map +1 -0
  41. package/dist/backends/android-instrumentation/installer.d.cts +123 -0
  42. package/dist/backends/android-instrumentation/installer.d.cts.map +1 -0
  43. package/dist/backends/android-instrumentation/installer.d.mts +123 -0
  44. package/dist/backends/android-instrumentation/installer.d.mts.map +1 -0
  45. package/dist/backends/android-instrumentation/installer.mjs +225 -0
  46. package/dist/backends/android-instrumentation/installer.mjs.map +1 -0
  47. package/dist/backends/android-instrumentation/signer.cjs +435 -0
  48. package/dist/backends/android-instrumentation/signer.cjs.map +1 -0
  49. package/dist/backends/android-instrumentation/signer.d.cts +40 -0
  50. package/dist/backends/android-instrumentation/signer.d.cts.map +1 -0
  51. package/dist/backends/android-instrumentation/signer.d.mts +40 -0
  52. package/dist/backends/android-instrumentation/signer.d.mts.map +1 -0
  53. package/dist/backends/android-instrumentation/signer.mjs +431 -0
  54. package/dist/backends/android-instrumentation/signer.mjs.map +1 -0
  55. package/dist/backends/android-instrumentation/snapshot.cjs +159 -0
  56. package/dist/backends/android-instrumentation/snapshot.cjs.map +1 -0
  57. package/dist/backends/android-instrumentation/snapshot.d.cts +67 -0
  58. package/dist/backends/android-instrumentation/snapshot.d.cts.map +1 -0
  59. package/dist/backends/android-instrumentation/snapshot.d.mts +67 -0
  60. package/dist/backends/android-instrumentation/snapshot.d.mts.map +1 -0
  61. package/dist/backends/android-instrumentation/snapshot.mjs +154 -0
  62. package/dist/backends/android-instrumentation/snapshot.mjs.map +1 -0
  63. package/dist/backends/appium-backend.cjs +19 -1
  64. package/dist/backends/appium-backend.cjs.map +1 -1
  65. package/dist/backends/appium-backend.d.cts +1 -0
  66. package/dist/backends/appium-backend.d.cts.map +1 -1
  67. package/dist/backends/appium-backend.d.mts +1 -0
  68. package/dist/backends/appium-backend.d.mts.map +1 -1
  69. package/dist/backends/appium-backend.mjs +19 -1
  70. package/dist/backends/appium-backend.mjs.map +1 -1
  71. package/dist/backends/idb-backend.cjs +128 -31
  72. package/dist/backends/idb-backend.cjs.map +1 -1
  73. package/dist/backends/idb-backend.d.cts +1 -0
  74. package/dist/backends/idb-backend.d.cts.map +1 -1
  75. package/dist/backends/idb-backend.d.mts +1 -0
  76. package/dist/backends/idb-backend.d.mts.map +1 -1
  77. package/dist/backends/idb-backend.mjs +129 -32
  78. package/dist/backends/idb-backend.mjs.map +1 -1
  79. package/dist/backends/index.cjs +3 -0
  80. package/dist/backends/index.cjs.map +1 -1
  81. package/dist/backends/index.d.cts.map +1 -1
  82. package/dist/backends/index.d.mts.map +1 -1
  83. package/dist/backends/index.mjs +3 -0
  84. package/dist/backends/index.mjs.map +1 -1
  85. package/dist/backends/types.cjs.map +1 -1
  86. package/dist/backends/types.d.cts +6 -0
  87. package/dist/backends/types.d.cts.map +1 -1
  88. package/dist/backends/types.d.mts +6 -0
  89. package/dist/backends/types.d.mts.map +1 -1
  90. package/dist/backends/types.mjs.map +1 -1
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.d.cts +1 -1
  93. package/dist/index.d.cts.map +1 -1
  94. package/dist/index.d.mts +1 -1
  95. package/dist/index.d.mts.map +1 -1
  96. package/dist/index.mjs.map +1 -1
  97. package/dist/tools/swipe.cjs +3 -1
  98. package/dist/tools/swipe.cjs.map +1 -1
  99. package/dist/tools/swipe.d.cts.map +1 -1
  100. package/dist/tools/swipe.d.mts.map +1 -1
  101. package/dist/tools/swipe.mjs +3 -1
  102. package/dist/tools/swipe.mjs.map +1 -1
  103. package/dist/utils/output-path.cjs +13 -0
  104. package/dist/utils/output-path.cjs.map +1 -1
  105. package/dist/utils/output-path.d.cts +10 -0
  106. package/dist/utils/output-path.d.cts.map +1 -1
  107. package/dist/utils/output-path.d.mts +10 -0
  108. package/dist/utils/output-path.d.mts.map +1 -1
  109. package/dist/utils/output-path.mjs +12 -0
  110. package/dist/utils/output-path.mjs.map +1 -1
  111. package/dist/utils/platform.cjs +65 -7
  112. package/dist/utils/platform.cjs.map +1 -1
  113. package/dist/utils/platform.d.cts +33 -0
  114. package/dist/utils/platform.d.cts.map +1 -1
  115. package/dist/utils/platform.d.mts +33 -0
  116. package/dist/utils/platform.d.mts.map +1 -1
  117. package/dist/utils/platform.mjs +63 -7
  118. package/dist/utils/platform.mjs.map +1 -1
  119. 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,CAoCN"}
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"}
@@ -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'])
@@ -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,oDAAoD;QACtD,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.',\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
+ {"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":";;AA6FA,kDAmBC;AAUD,8CAQC;AASD,gDAOC;AAlJD,6CAA0C;AAC1C,qCAA2E;AAC3E,+CAA6C;AAC7C,qCAAiC;AACjC,yCAAgE;AAEhE,iEAAiE;AACjE,MAAM,kBAAkB,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;CACR,CAAC;AAIX,wGAAwG;AACxG,MAAM,aAAa,GAAG,KAAK,CAAC;AAE5B;;;;;GAKG;AACH,mFAAmF;AACnF,MAAM,oBAAoB,GACxB,mBAAW,CAAC,QAAQ;IACpB,mBAAW,CAAC,OAAO;IACnB,mBAAW,CAAC,OAAO;IACnB,CAAC,mBAAW,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;AAChC,8BAA8B;AAE9B,IAAI,cAAc,GAAkB,IAAI,CAAC;AAEzC;;;;;;;;GAQG;AACH,SAAS,iBAAiB;IACxB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,cAAc,GAAG,IAAA,qBAAW,EAAC,IAAA,gBAAI,EAAC,IAAA,gBAAM,GAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc;IACrB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAC;IAC7D,OAAO,UAAU,CAAC,CAAC,CAAC,IAAA,mBAAO,EAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAAC,MAAc;IAC7C,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAG,IAAA,oBAAQ,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAA,sBAAU,EAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,MAAM,MAAM,EAAE,CACvE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,mBAAmB,CACjC,UAA8B,EAC9B,IAAkB;IAElB,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAA,yBAAW,EAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,IAAA,gBAAI,EACT,iBAAiB,EAAE,EACnB,GAAG,IAAI,IAAI,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAC/C,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,mBAAO,EAAC,UAAU,CAAC,CAAC;IACnC,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,IAAY;IAEZ,MAAM,IAAA,oBAAS,EAAC,IAAI,EAAE,IAAI,EAAE;QAC1B,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,oBAAoB;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,IAAA,mBAAS,EAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,oEAAoE;IACtE,CAAC;AACH,CAAC","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.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;AAkE3D;;;;;;;;;;;;;;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"}
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;AAkE3D;;;;;;;;;;;;;;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"}
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":"AAAA,OAAO,EAAE,WAAW,EAAE,oBAAoB;AAC1C,OAAO,EAAE,SAAS,EAAE,SAAS,IAAI,WAAW,EAAE,WAAW,EAAE,gBAAgB;AAC3E,OAAO,EAAE,SAAS,EAAE,yBAAyB;AAC7C,OAAO,EAAE,MAAM,EAAE,gBAAgB;AACjC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,kBAAkB;AAEhE,iEAAiE;AACjE,MAAM,kBAAkB,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;CACR,CAAC;AAIX,wGAAwG;AACxG,MAAM,aAAa,GAAG,KAAK,CAAC;AAE5B;;;;;GAKG;AACH,mFAAmF;AACnF,MAAM,oBAAoB,GACxB,WAAW,CAAC,QAAQ;IACpB,WAAW,CAAC,OAAO;IACnB,WAAW,CAAC,OAAO;IACnB,CAAC,WAAW,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;AAChC,8BAA8B;AAE9B,IAAI,cAAc,GAAkB,IAAI,CAAC;AAEzC;;;;;;;;GAQG;AACH,SAAS,iBAAiB;IACxB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,cAAc,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc;IACrB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAC;IAC7D,OAAO,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAAC,MAAc;IAC7C,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,MAAM,MAAM,EAAE,CACvE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAA8B,EAC9B,IAAkB;IAElB,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,IAAI,CACT,iBAAiB,EAAE,EACnB,GAAG,IAAI,IAAI,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAC/C,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACnC,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,IAAY;IAEZ,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE;QAC1B,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,oBAAoB;KAC3B,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,oEAAoE;IACtE,CAAC;AACH,CAAC","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"]}
@@ -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
- async function resolveIdbPath() {
120
- const result = await (0, exec_js_1.exec)('idb', ['--help']).catch(() => null);
121
- if (result?.exitCode === 0) {
122
- return 'idb';
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
- '/usr/local/bin/idb',
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 === 0) {
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-companion && pip3 install fb-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":";;;AA6BA,4CAcC;AAED,wCA4BC;AAyFD,wCAyBC;AA3LD,qCAAsC;AAEtC,wCAAqD;AAQrD,MAAa,oBAAqB,SAAQ,KAAK;IAG7C,YAAY,OAAyB;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QACnE,KAAK,CACH,wDAAwD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3E,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAXD,oDAWC;AAED;;;;;GAKG;AACI,KAAK,UAAU,gBAAgB,CACpC,QAAmB;IAEnB,MAAM,QAAQ,GAAgC,EAAE,CAAC;IAEjD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,8BAA8B,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC;AAEM,KAAK,UAAU,cAAc,CAClC,gBAAyB,EACzB,gBAA2B;IAE3B,IAAI,gBAAgB,EAAE,CAAC;QACrB,OAAO,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,gBAAgB,CAAC,CAAC;IAEzD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,MAAM,GACV,gBAAgB,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAA,4BAAkB,EAAC,KAAK,CAAC,CAAC;IAEvE,MAAM,aAAa,GAAG,gBAAgB,IAAI,KAAK,CAAC;IAChD,MAAM,IAAI,KAAK,CACb,gBAAgB,aAAa,kBAAkB;QAC7C,uCAAuC;QACvC,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,IAAA,4BAAkB,EAAC,KAAK,CAAC;QACzB,IAAA,4BAAkB,EAAC,KAAK,CAAC;KAC1B,CAAC,CAAC;IAEH,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,MAAM,IAAA,cAAI,EAAC,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,IAAA,cAAI,EAAC,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;AASD,KAAK,UAAU,0BAA0B;IACvC,MAAM,MAAM,GAAG,MAAM,IAAA,cAAI,EAAC,OAAO,EAAE;QACjC,QAAQ;QACR,MAAM;QACN,SAAS;QACT,QAAQ;QACR,QAAQ;KACT,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAEtC,CAAC;QACF,MAAM,OAAO,GAAqB,EAAE,CAAC;QACrC,KAAK,MAAM,cAAc,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3D,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;oBACpD,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC;QACH,OAAO,IAAA,qBAAW,EAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC9C,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAChD,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAC7B;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,cAAc;IAClC,MAAM,MAAM,GAAG,MAAM,IAAA,cAAI,EAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,MAAM,EAAE,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACpC,MAAM,UAAU,GAAG;QACjB,oBAAoB;QACpB,uBAAuB;QACvB,GAAG,iBAAiB,CAAC,GAAG,IAAI,iBAAiB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC;QACrE,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC;KAC9D,CAAC;IAEF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,MAAM,IAAA,cAAI,EAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAClE,IAAI,KAAK,EAAE,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,kBAAkB;QAChB,oFAAoF,CACvF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,cAAc;IAC3B,MAAM,MAAM,GAAG,MAAM,IAAA,cAAI,EAAC,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAChE,IAAI,MAAM,EAAE,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACpC,MAAM,UAAU,GAAG;QACjB,OAAO,CAAC,GAAG,CAAC,YAAY;YACtB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,qBAAqB;QAClD,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAC1B,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,qBAAqB;QACtD,GAAG,IAAI,yCAAyC;QAChD,GAAG,IAAI,iCAAiC;KACzC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC;IAE9B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,MAAM,IAAA,cAAI,EAAC,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACnE,IAAI,KAAK,EAAE,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,8BAA8B;IAC3C,MAAM,GAAG,GAAG,MAAM,cAAc,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,IAAA,cAAI,EAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,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,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","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,CAyBtD"}
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,CAyBtD"}
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"}
@@ -109,26 +109,82 @@ function findPythonBinDirs(base) {
109
109
  return [];
110
110
  }
111
111
  }
112
- export async function resolveIdbPath() {
113
- const result = await exec('idb', ['--help']).catch(() => null);
114
- if (result?.exitCode === 0) {
115
- return 'idb';
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
- '/usr/local/bin/idb',
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 === 0) {
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-companion && pip3 install fb-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);