appium-ios-remotexpc 0.37.0 → 0.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.37.2](https://github.com/appium/appium-ios-remotexpc/compare/v0.37.1...v0.37.2) (2026-03-22)
2
+
3
+ ### Miscellaneous Chores
4
+
5
+ * Migrate scripts to ESM/.mjs ([#173](https://github.com/appium/appium-ios-remotexpc/issues/173)) ([8d6053b](https://github.com/appium/appium-ios-remotexpc/commit/8d6053b6bfde353296905daf345b8e15d4307873))
6
+
7
+ ## [0.37.1](https://github.com/appium/appium-ios-remotexpc/compare/v0.37.0...v0.37.1) (2026-03-21)
8
+
1
9
  ## [0.37.0](https://github.com/appium/appium-ios-remotexpc/compare/v0.36.0...v0.37.0) (2026-03-21)
2
10
 
3
11
  ### Features
package/README.md CHANGED
@@ -11,7 +11,7 @@ This library provides functionality for:
11
11
 
12
12
  - Remote XPC (Cross Process Communication) with iOS devices
13
13
  - Lockdown communication
14
- - USB device multiplexing (usbmux)
14
+ - Device multiplexing via **usbmuxd** (lists USB **and** WiFi-attached devices; see below)
15
15
  - Property list (plist) handling
16
16
  - IPv6 tunneling services to iOS devices using TUN/TAP interfaces
17
17
  - System log access
@@ -32,7 +32,7 @@ npm install appium-ios-remotexpc
32
32
  ## Features
33
33
 
34
34
  - **Plist Handling**: Encode, decode, parse, and create property lists for iOS device communication.
35
- - **USB Device Communication**: Connect to iOS devices over USB using the usbmux protocol.
35
+ - **Device communication over usbmux / usbmuxd**: The system **usbmuxd** daemon exposes a single device list that includes machines plugged in over **USB** and, when pairing and wireless sync are set up, the same iPhone/iPad **over WiFi**. WiFi entries are marked **`ConnectionType: Network`** (USB entries use `ConnectionType: USB`). This library connects through usbmuxd the same way for both; the tunnel path is unchanged (lockdown → CoreDeviceProxy → TUN/TAP → Remote XPC).
36
36
  - **Remote XPC**: Establish Remote XPC connections with iOS devices.
37
37
  - **Service Architecture**: Connect to various iOS services:
38
38
  - System Log Service: Access device logs
@@ -130,6 +130,20 @@ const remoteXPC = await TunnelManager.createRemoteXPCConnection(
130
130
  const services = remoteXPC.getServices();
131
131
  ```
132
132
 
133
+ ### iPhone / iPad over WiFi (usbmuxd “network” devices)
134
+
135
+ **usbmuxd** (the multiplexer daemon, e.g. on macOS) does **not** only list USB devices: once a device is paired with the host and wireless sync / lockdown-over-WiFi is enabled, **the same daemon’s device list includes that device as attached over WiFi**. In plist responses from `ListDevices`, those rows carry **`ConnectionType: Network`** (and a distinct `DeviceID` from any USB listing for the same physical device).
136
+
137
+ There is no separate “WiFi API” in this library: call `createUsbmux()` → `listDevices()` (or any other client that queries **usbmuxd**) and use the returned **`DeviceID`** and UDID with `createLockdownServiceByUDID` and the tunnel steps in the previous section—identical to USB.
138
+
139
+ **Typical host-side setup:**
140
+
141
+ 1. Pair the device with this Mac and tap **Trust** on the device if prompted.
142
+ 2. Allow the device to connect over WiFi (e.g. in Finder under the device, enable **Show this [device] when on WiFi**, or use Xcode **Devices and Simulators** with the equivalent option so lockdown can reach the device without USB).
143
+ 3. Confirm **usbmuxd** reports the device with **`ConnectionType: Network`**—for example by logging the result of `listDevices()` from this library, or by checking another usbmuxd client’s device list while the device is on the same network and not on USB.
144
+
145
+ For an end-to-end tunnel smoke test with the tunnel registry HTTP API, use `npm run tunnel-creation` or `npm run test:tunnel-creation` (see `scripts/test-tunnel-creation.ts`), usually with **sudo** for TUN/TAP.
146
+
133
147
  ### Apple TV / tvOS over WiFi
134
148
 
135
149
  Apple TV and tvOS devices over WiFi are supported. The following symbols are part of the public API and are intended for external use (e.g. by the Appium XCUITest driver):
@@ -182,10 +196,19 @@ All pull requests must pass these checks before merging. The workflows are defin
182
196
  - `npm run format` - Run prettier
183
197
  - `npm run lint:fix` - Run ESLint with auto-fix
184
198
  - `npm test` - Run tests (requires sudo privileges for tunneling)
185
- - `npm run test:tunnel-creation` - Create tunnels for testing (requires sudo)
199
+
200
+ CLI helpers under `scripts/` are ESM (`.mjs`) and load the library via the package entrypoint. Run `npm run build` before using them so `appium-ios-remotexpc` resolves to `build/`.
201
+
202
+ - `npm run tunnel-creation` / `npm run test:tunnel-creation` — Create USB tunnels and start the tunnel registry HTTP API (requires `sudo`)
203
+ - `npm run test:tunnel-creation:lsof` — Same as above with `--keep-open` (for inspecting open sockets)
204
+ - `npm run pair-appletv` — Pair an Apple TV over WiFi for Remote XPC (requires `sudo`)
205
+ - `npm run start-appletv-tunnel` — Start an Apple TV WiFi tunnel and tunnel registry (requires `sudo`)
206
+
207
+ Pass `--help` after `--` to any of these npm scripts to see CLI flags (for example: `npm run pair-appletv -- --help`).
186
208
 
187
209
  ## Project Structure
188
210
 
211
+ - `/scripts` - Optional CLI helpers (ESM `.mjs`) for tunnels and Apple TV pairing; use via `npm run` entries under [Scripts](#scripts)
189
212
  - `/src` - Source code
190
213
  - `/lib` - Core libraries
191
214
  - `/lockdown` - Device lockdown protocol
@@ -193,7 +216,7 @@ All pull requests must pass these checks before merging. The workflows are defin
193
216
  - `/plist` - Property list processing
194
217
  - `/remote-xpc` - XPC connection handling
195
218
  - `/tunnel` - Tunneling implementation with tuntap integration
196
- - `/usbmux` - USB multiplexing protocol
219
+ - `/usbmux` - usbmuxd client (USB and WiFi-listed devices)
197
220
  - `/services` - Service implementations
198
221
  - `/ios`
199
222
  - `/diagnostic-service` - Device diagnostics
@@ -208,7 +231,7 @@ npm test
208
231
  ```
209
232
 
210
233
  Note: Integration tests require:
211
- - Physical iOS devices connected
234
+ - Physical iOS devices connected (USB and/or **WiFi** if the device is paired and visible to usbmux as `Network`)
212
235
  - Sudo privileges for tunnel creation
213
236
  - Device trust established
214
237
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-remotexpc",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
4
4
  "main": "build/src/index.js",
5
5
  "types": "build/src/index.d.ts",
6
6
  "type": "module",
@@ -18,11 +18,11 @@
18
18
  "clean:build": "rimraf ./build",
19
19
  "build:es": "tsc",
20
20
  "build": "run-s clean:* build:*",
21
- "lint": "eslint src --ext .ts --quiet",
21
+ "lint": "eslint src scripts --quiet",
22
22
  "prepare": "husky && npm run build",
23
23
  "format": "prettier --write \"{src,test}/**/*.{ts,tsx}\"",
24
24
  "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx}\"",
25
- "lint:fix": "eslint src --ext .ts --fix",
25
+ "lint:fix": "eslint src scripts --fix",
26
26
  "test": "mocha test/integration/**/*.ts",
27
27
  "test:all": "mocha -r tsx/cjs test/run-integration-tests.ts",
28
28
  "test:tunnel": "mocha test/integration/tunnel-test.ts --exit --timeout 1m",
@@ -33,9 +33,9 @@
33
33
  "test:mobile-config": "mocha test/integration/mobile-config-test.ts --exit --timeout 1m",
34
34
  "test:springboard": "mocha test/integration/springboard-service-test.ts --exit --timeout 1m",
35
35
  "test:unit": "NODE_ENV=test mocha 'test/unit/**/*.ts' --exit --timeout 2m",
36
- "tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
37
- "pair-appletv": "sudo tsx scripts/pair-appletv.ts",
38
- "start-appletv-tunnel": "sudo tsx scripts/start-appletv-tunnel.ts",
36
+ "tunnel-creation": "sudo node scripts/tunnel-creation.mjs",
37
+ "pair-appletv": "sudo node scripts/pair-appletv.mjs",
38
+ "start-appletv-tunnel": "sudo node scripts/start-appletv-tunnel.mjs",
39
39
  "test:webinspector": "mocha test/integration/webinspector-test.ts --exit --timeout 1m",
40
40
  "test:misagent": "mocha test/integration/misagent-service-test.ts --exit --timeout 1m",
41
41
  "test:afc": "mocha test/integration/afc-test.ts --exit --timeout 1m",
@@ -55,8 +55,8 @@
55
55
  "test:dvt:network-monitor": "mocha test/integration/dvt_instruments/network-monitor-test.ts --exit --timeout 1m",
56
56
  "test:dvt:process-control": "mocha test/integration/process-control-test.ts --exit --timeout 1m",
57
57
  "test:testmanagerd": "mocha test/integration/testmanagerd-test.ts --exit --timeout 2m",
58
- "test:tunnel-creation": "sudo tsx scripts/test-tunnel-creation.ts",
59
- "test:tunnel-creation:lsof": "sudo tsx scripts/test-tunnel-creation.ts --keep-open"
58
+ "test:tunnel-creation": "sudo node scripts/tunnel-creation.mjs",
59
+ "test:tunnel-creation:lsof": "sudo node scripts/tunnel-creation.mjs --keep-open"
60
60
  },
61
61
  "keywords": [],
62
62
  "author": "Appium Contributors",
@@ -102,6 +102,7 @@
102
102
  "@xmldom/xmldom": "^0.9.8",
103
103
  "appium-ios-tuntap": "^0.x",
104
104
  "axios": "^1.12.0",
105
+ "commander": "^14.0.1",
105
106
  "dnssd": "^0.x",
106
107
  "minimatch": "^10.1.1",
107
108
  "node-devicectl": "^1.2.0",
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pair Apple TV / tvOS devices over WiFi for Remote XPC tunnels.
4
+ *
5
+ * Usage:
6
+ * npm run pair-appletv -- [options]
7
+ */
8
+
9
+ import { logger } from '@appium/support';
10
+ import { Command } from 'commander';
11
+ import { AppleTVPairingService, UserInputService } from 'appium-ios-remotexpc';
12
+
13
+ const log = logger.getLogger('AppleTVPairing');
14
+
15
+ async function main() {
16
+ const program = new Command();
17
+ program
18
+ .name('pair-appletv')
19
+ .description('Pair Apple TV / tvOS devices over WiFi for Remote XPC tunnels')
20
+ .option(
21
+ '-d, --device <selector>',
22
+ 'Device selector: name, identifier (e.g. AA:BB:CC:DD:EE:FF), or index (0, 1, …)',
23
+ );
24
+
25
+ program.parse(process.argv);
26
+ const options = program.opts();
27
+
28
+ const userInput = new UserInputService();
29
+ const pairingService = new AppleTVPairingService(userInput);
30
+ const result = await pairingService.discoverAndPair(options.device);
31
+
32
+ if (result.success) {
33
+ log.info(`Pairing successful! Record saved to: ${result.pairingFile}`);
34
+ } else {
35
+ throw result.error ?? new Error('Pairing failed');
36
+ }
37
+ }
38
+
39
+ await main();
@@ -1,43 +1,70 @@
1
- #!/usr/bin/env tsx
2
- import * as tls from 'node:tls';
3
-
4
- import { PacketStreamServer, TunnelManager } from '../src/index.js';
5
- import type { TunnelRegistry } from '../src/index.js';
6
- import { AppleTVTunnelService } from '../src/lib/apple-tv/tunnel/index.js';
7
- import type { AppleTVDevice } from '../src/lib/bonjour/bonjour-discovery.js';
8
- import { getLogger } from '../src/lib/logger.js';
9
- import type { TunnelConnection } from '../src/lib/tunnel/index.js';
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Start an Apple TV Remote XPC tunnel and expose the tunnel registry API.
4
+ */
5
+
6
+ import { logger } from '@appium/support';
7
+ import { Command } from 'commander';
10
8
  import {
11
- DEFAULT_TUNNEL_REGISTRY_PORT,
9
+ AppleTVTunnelService,
10
+ PacketStreamServer,
11
+ TunnelManager,
12
12
  startTunnelRegistryServer,
13
- } from '../src/lib/tunnel/tunnel-registry-server.js';
13
+ } from 'appium-ios-remotexpc';
14
+ import { DEFAULT_TUNNEL_REGISTRY_PORT } from '../build/src/lib/tunnel/tunnel-registry-server.js';
14
15
 
15
- const log = getLogger('WiFiTunnel');
16
+ const log = logger.getLogger('WiFiTunnel');
16
17
  const PACKET_STREAM_PORT = 50100;
17
18
 
18
- async function main(): Promise<void> {
19
- const args = process.argv.slice(2);
20
- const specificDeviceIdentifier = args.find((arg) => !arg.startsWith('-'));
19
+ function parsePort(value) {
20
+ const port = Number.parseInt(value, 10);
21
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
22
+ throw new Error(
23
+ `Invalid port: ${value}. Expected an integer between 1 and 65535.`,
24
+ );
25
+ }
26
+ return port;
27
+ }
28
+
29
+ async function main() {
30
+ const program = new Command();
31
+ program
32
+ .name('start-appletv-tunnel')
33
+ .description('Start an Apple TV WiFi tunnel and tunnel registry HTTP API')
34
+ .argument(
35
+ '[deviceIdentifier]',
36
+ 'Optional Apple TV device identifier to target',
37
+ )
38
+ .option(
39
+ '--tunnel-registry-port <port>',
40
+ `Port for tunnel registry API (default: ${DEFAULT_TUNNEL_REGISTRY_PORT})`,
41
+ parsePort,
42
+ );
43
+
44
+ program.parse(process.argv);
45
+ const options = program.opts();
46
+ const deviceIdentifier = program.args[0];
47
+ const registryPort =
48
+ options.tunnelRegistryPort ?? DEFAULT_TUNNEL_REGISTRY_PORT;
21
49
 
22
- if (specificDeviceIdentifier) {
50
+ if (deviceIdentifier) {
23
51
  log.info(
24
- `Starting Apple TV tunnel for specific device identifier: ${specificDeviceIdentifier}`,
52
+ `Starting Apple TV tunnel for specific device identifier: ${deviceIdentifier}`,
25
53
  );
26
54
  } else {
27
55
  log.info('Starting Apple TV tunnel (will try all discovered devices)');
28
56
  }
29
57
 
30
58
  const tunnelService = new AppleTVTunnelService();
31
- let tunnel: TunnelConnection | null = null;
32
- let tlsSocket: tls.TLSSocket | null = null;
33
- let deviceInfo: AppleTVDevice | null = null;
34
- let packetStreamServer: PacketStreamServer | null = null;
59
+ let tunnel = null;
60
+ let tlsSocket = null;
61
+ let deviceInfo = null;
62
+ let packetStreamServer = null;
35
63
 
36
- const cleanup = async (signal: string): Promise<void> => {
64
+ const cleanup = async (signal) => {
37
65
  log.warn(`\nCleaning up (${signal})...`);
38
66
 
39
67
  try {
40
- // Close packet stream server first
41
68
  if (packetStreamServer) {
42
69
  log.info('Closing packet stream server...');
43
70
  await packetStreamServer.stop();
@@ -89,7 +116,7 @@ async function main(): Promise<void> {
89
116
  log.info('Starting Apple TV tunnel...');
90
117
  const result = await tunnelService.startTunnel(
91
118
  undefined,
92
- specificDeviceIdentifier,
119
+ deviceIdentifier,
93
120
  );
94
121
  tlsSocket = result.socket;
95
122
  deviceInfo = result.device;
@@ -101,13 +128,11 @@ async function main(): Promise<void> {
101
128
  log.info('Creating tunnel with TunnelManager...');
102
129
  tunnel = await TunnelManager.getTunnel(tlsSocket);
103
130
 
104
- // Start packet stream server (same as iPhone tunnel)
105
131
  let packetStreamPort = 0;
106
132
  try {
107
133
  packetStreamServer = new PacketStreamServer(PACKET_STREAM_PORT);
108
134
  await packetStreamServer.start();
109
135
 
110
- // Attach packet consumer to tunnel to receive packet data
111
136
  const consumer = packetStreamServer.getPacketConsumer();
112
137
  if (consumer && tunnel.addPacketConsumer) {
113
138
  tunnel.addPacketConsumer(consumer);
@@ -123,7 +148,7 @@ async function main(): Promise<void> {
123
148
  const now = Date.now();
124
149
  const nowISOString = new Date().toISOString();
125
150
 
126
- const registry: TunnelRegistry = {
151
+ const registry = {
127
152
  tunnels: {
128
153
  [deviceInfo.identifier]: {
129
154
  udid: deviceInfo.identifier,
@@ -144,7 +169,7 @@ async function main(): Promise<void> {
144
169
  },
145
170
  };
146
171
 
147
- await startTunnelRegistryServer(registry);
172
+ await startTunnelRegistryServer(registry, registryPort);
148
173
 
149
174
  log.info('=== TUNNEL ESTABLISHED ===');
150
175
  log.info(`Tunnel Address: ${tunnel.Address}`);
@@ -154,7 +179,7 @@ async function main(): Promise<void> {
154
179
 
155
180
  log.info('\n📁 Tunnel registry API:');
156
181
  log.info(
157
- ` http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels`,
182
+ ` http://localhost:${registryPort}/remotexpc/tunnels`,
158
183
  );
159
184
  log.info(' - GET /remotexpc/tunnels - List all tunnels');
160
185
  log.info(
@@ -169,9 +194,7 @@ async function main(): Promise<void> {
169
194
  } catch (error) {
170
195
  log.error('Tunnel failed:', error);
171
196
  throw error;
172
- } finally {
173
- await cleanup('Shutdown');
174
197
  }
175
198
  }
176
199
 
177
- main();
200
+ await main();
@@ -1,10 +1,10 @@
1
- #!/usr/bin/env tsx
1
+ #!/usr/bin/env node
2
2
  /**
3
- * Test script for creating lockdown service, starting CoreDeviceProxy, and creating tunnel
4
- * This script demonstrates the tunnel creation workflow for all connected devices
3
+ * Create lockdown + CoreDeviceProxy tunnels for connected USB devices and expose the tunnel registry API.
5
4
  */
5
+
6
6
  import { logger } from '@appium/support';
7
- import type { ConnectionOptions } from 'tls';
7
+ import { Command } from 'commander';
8
8
 
9
9
  import {
10
10
  PacketStreamServer,
@@ -12,26 +12,27 @@ import {
12
12
  createLockdownServiceByUDID,
13
13
  createUsbmux,
14
14
  startCoreDeviceProxy,
15
- } from '../src/index.js';
16
- import type { SocketInfo, TunnelRegistry } from '../src/index.js';
17
- import {
18
- DEFAULT_TUNNEL_REGISTRY_PORT,
19
15
  startTunnelRegistryServer,
20
- } from '../src/lib/tunnel/tunnel-registry-server.js';
21
- import type { Device } from '../src/lib/usbmux/index.js';
16
+ } from 'appium-ios-remotexpc';
17
+ import { DEFAULT_TUNNEL_REGISTRY_PORT } from '../build/src/lib/tunnel/tunnel-registry-server.js';
22
18
 
23
19
  const log = logger.getLogger('TunnelCreation');
24
- /**
25
- * Update tunnel registry with new tunnel information
26
- */
27
- async function updateTunnelRegistry(
28
- results: TunnelResult[],
29
- ): Promise<TunnelRegistry> {
20
+
21
+ function parsePort(value) {
22
+ const port = Number.parseInt(value, 10);
23
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) {
24
+ throw new Error(
25
+ `Invalid port: ${value}. Expected an integer between 1 and 65535.`,
26
+ );
27
+ }
28
+ return port;
29
+ }
30
+
31
+ async function updateTunnelRegistry(results) {
30
32
  const now = Date.now();
31
33
  const nowISOString = new Date().toISOString();
32
34
 
33
- // Initialize registry if it doesn't exist
34
- const registry: TunnelRegistry = {
35
+ const registry = {
35
36
  tunnels: {},
36
37
  metadata: {
37
38
  lastUpdated: nowISOString,
@@ -40,7 +41,6 @@ async function updateTunnelRegistry(
40
41
  },
41
42
  };
42
43
 
43
- // Update tunnels
44
44
  for (const result of results) {
45
45
  if (result.success) {
46
46
  const udid = result.device.Properties.SerialNumber;
@@ -58,39 +58,21 @@ async function updateTunnelRegistry(
58
58
  }
59
59
  }
60
60
 
61
- // Update metadata
62
61
  registry.metadata = {
63
62
  lastUpdated: nowISOString,
64
63
  totalTunnels: Object.keys(registry.tunnels).length,
65
- activeTunnels: Object.keys(registry.tunnels).length, // Assuming all are active for now
64
+ activeTunnels: Object.keys(registry.tunnels).length,
66
65
  };
67
66
 
68
67
  return registry;
69
68
  }
70
69
 
71
- const activeServers: Array<{ server: any; port: number }> = [];
72
- const packetStreamServers: Map<string, PacketStreamServer> = new Map();
73
-
74
- const deviceInfoMap: Map<
75
- string,
76
- {
77
- udid: string;
78
- address: string;
79
- rsdPort?: number;
80
- connectionType: string;
81
- productId: number;
82
- }
83
- > = new Map();
70
+ const packetStreamServers = new Map();
84
71
 
85
- let PACKET_STREAM_BASE_PORT = 50000;
86
- /**
87
- * Setup cleanup handlers for graceful shutdown
88
- */
89
- function setupCleanupHandlers(): void {
90
- const cleanup = async (signal: string) => {
72
+ function setupCleanupHandlers() {
73
+ const cleanup = async (signal) => {
91
74
  log.warn(`\nCleaning up (${signal})...`);
92
75
 
93
- // Close all packet stream servers
94
76
  if (packetStreamServers.size > 0) {
95
77
  log.info(
96
78
  `Closing ${packetStreamServers.size} packet stream server(s)...`,
@@ -111,7 +93,6 @@ function setupCleanupHandlers(): void {
111
93
  log.info('Cleanup completed.');
112
94
  };
113
95
 
114
- // Handle various termination signals
115
96
  process.on('SIGINT', async () => {
116
97
  await cleanup('SIGINT (Ctrl+C)');
117
98
  process.exit(0);
@@ -125,7 +106,6 @@ function setupCleanupHandlers(): void {
125
106
  process.exit(0);
126
107
  });
127
108
 
128
- // Handle uncaught exceptions and unhandled rejections
129
109
  process.on('uncaughtException', async (error) => {
130
110
  log.error('Uncaught Exception:', error);
131
111
  await cleanup('Uncaught Exception');
@@ -137,24 +117,7 @@ function setupCleanupHandlers(): void {
137
117
  });
138
118
  }
139
119
 
140
- /**
141
- * Interface for tunnel result
142
- */
143
- interface TunnelResult {
144
- device: Device;
145
- tunnel: {
146
- Address: string;
147
- RsdPort?: number;
148
- };
149
- packetStreamPort?: number;
150
- success: boolean;
151
- error?: string;
152
- }
153
-
154
- async function createTunnelForDevice(
155
- device: Device,
156
- tlsOptions: Partial<ConnectionOptions>,
157
- ): Promise<TunnelResult & { socket?: any; socketInfo?: SocketInfo }> {
120
+ async function createTunnelForDevice(device, tlsOptions, packetStreamBaseRef) {
158
121
  const udid = device.Properties.SerialNumber;
159
122
 
160
123
  try {
@@ -185,9 +148,9 @@ async function createTunnelForDevice(
185
148
  `Tunnel created for address: ${tunnel.Address} with RsdPort: ${tunnel.RsdPort}`,
186
149
  );
187
150
 
188
- let packetStreamPort: number | undefined;
151
+ let packetStreamPort;
189
152
  try {
190
- packetStreamPort = PACKET_STREAM_BASE_PORT++;
153
+ packetStreamPort = packetStreamBaseRef.value++;
191
154
  const packetStreamServer = new PacketStreamServer(packetStreamPort);
192
155
  await packetStreamServer.start();
193
156
 
@@ -215,16 +178,6 @@ async function createTunnelForDevice(
215
178
  socket.setNoDelay(true);
216
179
  }
217
180
 
218
- const deviceInfo = {
219
- udid: device.Properties.SerialNumber,
220
- address: tunnel.Address,
221
- rsdPort: tunnel.RsdPort,
222
- connectionType: device.Properties.ConnectionType,
223
- productId: device.Properties.ProductID,
224
- };
225
-
226
- deviceInfoMap.set(device.Properties.SerialNumber, deviceInfo);
227
-
228
181
  return {
229
182
  device,
230
183
  tunnel: {
@@ -261,14 +214,32 @@ async function createTunnelForDevice(
261
214
  }
262
215
  }
263
216
 
264
- /**
265
- */
266
- async function main(): Promise<void> {
217
+ async function main() {
267
218
  setupCleanupHandlers();
268
219
 
269
- const args = process.argv.slice(2);
270
- const keepOpenFlag = args.includes('--keep-open') || args.includes('-k');
271
- const specificUdid = args.find((arg) => !arg.startsWith('-'));
220
+ const program = new Command();
221
+ program
222
+ .name('tunnel-creation')
223
+ .description(
224
+ 'Create tunnels for connected USB devices (lockdown + CoreDeviceProxy)',
225
+ )
226
+ .argument('[udid]', 'Optional device UDID (omit for all devices)')
227
+ .option('--udid <udid>', 'UDID of the device to create tunnel for')
228
+ .option('-k, --keep-open', 'Keep connections open for lsof inspection')
229
+ .option(
230
+ '--packet-stream-base-port <port>',
231
+ 'Base port for packet stream servers (1-65535)',
232
+ parsePort,
233
+ )
234
+ .option(
235
+ '--tunnel-registry-port <port>',
236
+ `Port for tunnel registry API (default: ${DEFAULT_TUNNEL_REGISTRY_PORT})`,
237
+ parsePort,
238
+ );
239
+
240
+ program.parse(process.argv);
241
+ const options = program.opts();
242
+ const specificUdid = options.udid ?? program.args[0] ?? undefined;
272
243
 
273
244
  if (specificUdid) {
274
245
  log.info(
@@ -278,16 +249,22 @@ async function main(): Promise<void> {
278
249
  log.info('Starting tunnel creation test for all connected devices');
279
250
  }
280
251
 
281
- if (keepOpenFlag) {
252
+ if (options.keepOpen) {
282
253
  log.info('Running in "keep connections open" mode for lsof inspection');
283
254
  }
284
255
 
285
- try {
286
- const tlsOptions: Partial<ConnectionOptions> = {
287
- rejectUnauthorized: false,
288
- minVersion: 'TLSv1.2',
289
- };
256
+ const tlsOptions = {
257
+ rejectUnauthorized: false,
258
+ minVersion: 'TLSv1.2',
259
+ };
260
+
261
+ const packetStreamBaseRef = {
262
+ value: options.packetStreamBasePort ?? 50000,
263
+ };
264
+ const registryPort =
265
+ options.tunnelRegistryPort ?? DEFAULT_TUNNEL_REGISTRY_PORT;
290
266
 
267
+ try {
291
268
  log.info('Connecting to usbmuxd...');
292
269
  const usbmux = await createUsbmux();
293
270
 
@@ -331,10 +308,14 @@ async function main(): Promise<void> {
331
308
 
332
309
  log.info(`\nProcessing ${devicesToProcess.length} device(s)...`);
333
310
 
334
- const results: TunnelResult[] = [];
311
+ const results = [];
335
312
 
336
313
  for (const device of devicesToProcess) {
337
- const result = await createTunnelForDevice(device, tlsOptions);
314
+ const result = await createTunnelForDevice(
315
+ device,
316
+ tlsOptions,
317
+ packetStreamBaseRef,
318
+ );
338
319
  results.push(result);
339
320
 
340
321
  if (devicesToProcess.length > 1) {
@@ -353,11 +334,11 @@ async function main(): Promise<void> {
353
334
  if (successful.length > 0) {
354
335
  log.info('\n✅ Successful tunnels:');
355
336
  const registry = await updateTunnelRegistry(results);
356
- await startTunnelRegistryServer(registry);
337
+ await startTunnelRegistryServer(registry, registryPort);
357
338
 
358
339
  log.info('\n📁 Tunnel registry API:');
359
340
  log.info(' The tunnel registry is now available through the API at:');
360
- log.info(' http://localhost:42314/remotexpc/tunnels');
341
+ log.info(` http://localhost:${registryPort}/remotexpc/tunnels`);
361
342
  log.info('\n Available endpoints:');
362
343
  log.info(' - GET /remotexpc/tunnels - List all tunnels');
363
344
  log.info(' - GET /remotexpc/tunnels/:udid - Get tunnel by UDID');
@@ -365,15 +346,15 @@ async function main(): Promise<void> {
365
346
 
366
347
  log.info('\n💡 Example usage:');
367
348
  log.info(
368
- ` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels`,
349
+ ` curl http://localhost:${registryPort}/remotexpc/tunnels`,
369
350
  );
370
351
  log.info(
371
- ` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels/metadata`,
352
+ ` curl http://localhost:${registryPort}/remotexpc/tunnels/metadata`,
372
353
  );
373
354
  if (successful.length > 0) {
374
355
  const firstUdid = successful[0].device.Properties.SerialNumber;
375
356
  log.info(
376
- ` curl http://localhost:${DEFAULT_TUNNEL_REGISTRY_PORT}/remotexpc/tunnels/${firstUdid}`,
357
+ ` curl http://localhost:${registryPort}/remotexpc/tunnels/${firstUdid}`,
377
358
  );
378
359
  }
379
360
  }
@@ -383,5 +364,4 @@ async function main(): Promise<void> {
383
364
  }
384
365
  }
385
366
 
386
- // Run the main function
387
- main();
367
+ await main();
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env tsx
2
- import {
3
- AppleTVPairingService,
4
- UserInputService,
5
- } from '../src/lib/apple-tv/index.js';
6
- import { getLogger } from '../src/lib/logger.js';
7
-
8
- interface CLIArgs {
9
- device?: string;
10
- help?: boolean;
11
- }
12
-
13
- function parseArgs(): CLIArgs {
14
- const args: CLIArgs = {};
15
- const cliArgs = process.argv.slice(2);
16
-
17
- for (let i = 0; i < cliArgs.length; i++) {
18
- const arg = cliArgs[i];
19
- if (arg === '--device' || arg === '-d') {
20
- args.device = cliArgs[++i];
21
- } else if (arg === '--help' || arg === '-h') {
22
- args.help = true;
23
- }
24
- }
25
-
26
- return args;
27
- }
28
-
29
- function printHelp(): void {
30
- // eslint-disable-next-line no-console
31
- console.log(`
32
- Apple TV Pairing Script
33
-
34
- Usage: pair-appletv [options]
35
-
36
- Options:
37
- -d, --device <selector> Specify device to pair with. Can be:
38
- - Device name (e.g., "Living Room")
39
- - Device identifier (e.g., "AA:BB:CC:DD:EE:FF")
40
- - Device index (e.g., "0", "1", "2")
41
- If not specified and multiple devices are found,
42
- you will be prompted to choose one.
43
- -h, --help Show this help message
44
-
45
- Examples:
46
- pair-appletv # Discover and select device interactively
47
- pair-appletv --device "Living Room" # Pair with device named "Living Room"
48
- pair-appletv --device 0 # Pair with first discovered device
49
- pair-appletv -d AA:BB:CC:DD:EE:FF # Pair with device by identifier
50
- `);
51
- }
52
-
53
- // CLI interface
54
- async function main(): Promise<void> {
55
- const log = getLogger('AppleTVPairing');
56
- const args = parseArgs();
57
-
58
- if (args.help) {
59
- printHelp();
60
- return;
61
- }
62
-
63
- const userInput = new UserInputService();
64
- const pairingService = new AppleTVPairingService(userInput);
65
- const result = await pairingService.discoverAndPair(args.device);
66
-
67
- if (result.success) {
68
- log.info(`Pairing successful! Record saved to: ${result.pairingFile}`);
69
- } else {
70
- const error = result.error ?? new Error('Pairing failed');
71
- log.error(`Pairing failed: ${error.message}`);
72
- throw error;
73
- }
74
- }
75
-
76
- main().catch(() => {
77
- process.exit(1);
78
- });