@mcesystems/usbmuxd-instance-manager 1.0.73 → 1.0.75

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/dist/cli.js CHANGED
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
9
12
  var __copyProps = (to, from, except, desc) => {
10
13
  if (from && typeof from === "object" || typeof from === "function") {
11
14
  for (let key of __getOwnPropNames(from))
@@ -23,192 +26,860 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
26
  mod
24
27
  ));
25
28
 
29
+ // ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
30
+ var require_package = __commonJS({
31
+ "../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json"(exports2, module2) {
32
+ module2.exports = {
33
+ name: "dotenv",
34
+ version: "17.2.3",
35
+ description: "Loads environment variables from .env file",
36
+ main: "lib/main.js",
37
+ types: "lib/main.d.ts",
38
+ exports: {
39
+ ".": {
40
+ types: "./lib/main.d.ts",
41
+ require: "./lib/main.js",
42
+ default: "./lib/main.js"
43
+ },
44
+ "./config": "./config.js",
45
+ "./config.js": "./config.js",
46
+ "./lib/env-options": "./lib/env-options.js",
47
+ "./lib/env-options.js": "./lib/env-options.js",
48
+ "./lib/cli-options": "./lib/cli-options.js",
49
+ "./lib/cli-options.js": "./lib/cli-options.js",
50
+ "./package.json": "./package.json"
51
+ },
52
+ scripts: {
53
+ "dts-check": "tsc --project tests/types/tsconfig.json",
54
+ lint: "standard",
55
+ pretest: "npm run lint && npm run dts-check",
56
+ test: "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
57
+ "test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
58
+ prerelease: "npm test",
59
+ release: "standard-version"
60
+ },
61
+ repository: {
62
+ type: "git",
63
+ url: "git://github.com/motdotla/dotenv.git"
64
+ },
65
+ homepage: "https://github.com/motdotla/dotenv#readme",
66
+ funding: "https://dotenvx.com",
67
+ keywords: [
68
+ "dotenv",
69
+ "env",
70
+ ".env",
71
+ "environment",
72
+ "variables",
73
+ "config",
74
+ "settings"
75
+ ],
76
+ readmeFilename: "README.md",
77
+ license: "BSD-2-Clause",
78
+ devDependencies: {
79
+ "@types/node": "^18.11.3",
80
+ decache: "^4.6.2",
81
+ sinon: "^14.0.1",
82
+ standard: "^17.0.0",
83
+ "standard-version": "^9.5.0",
84
+ tap: "^19.2.0",
85
+ typescript: "^4.8.4"
86
+ },
87
+ engines: {
88
+ node: ">=12"
89
+ },
90
+ browser: {
91
+ fs: false
92
+ }
93
+ };
94
+ }
95
+ });
96
+
97
+ // ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js
98
+ var require_main = __commonJS({
99
+ "../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js"(exports2, module2) {
100
+ var fs = require("fs");
101
+ var path = require("path");
102
+ var os = require("os");
103
+ var crypto = require("crypto");
104
+ var packageJson = require_package();
105
+ var version = packageJson.version;
106
+ var TIPS = [
107
+ "\u{1F510} encrypt with Dotenvx: https://dotenvx.com",
108
+ "\u{1F510} prevent committing .env to code: https://dotenvx.com/precommit",
109
+ "\u{1F510} prevent building .env in docker: https://dotenvx.com/prebuild",
110
+ "\u{1F4E1} add observability to secrets: https://dotenvx.com/ops",
111
+ "\u{1F465} sync secrets across teammates & machines: https://dotenvx.com/ops",
112
+ "\u{1F5C2}\uFE0F backup and recover secrets: https://dotenvx.com/ops",
113
+ "\u2705 audit secrets and track compliance: https://dotenvx.com/ops",
114
+ "\u{1F504} add secrets lifecycle management: https://dotenvx.com/ops",
115
+ "\u{1F511} add access controls to secrets: https://dotenvx.com/ops",
116
+ "\u{1F6E0}\uFE0F run anywhere with `dotenvx run -- yourcommand`",
117
+ "\u2699\uFE0F specify custom .env file path with { path: '/custom/path/.env' }",
118
+ "\u2699\uFE0F enable debug logging with { debug: true }",
119
+ "\u2699\uFE0F override existing env vars with { override: true }",
120
+ "\u2699\uFE0F suppress all logs with { quiet: true }",
121
+ "\u2699\uFE0F write to custom object with { processEnv: myObject }",
122
+ "\u2699\uFE0F load multiple .env files with { path: ['.env.local', '.env'] }"
123
+ ];
124
+ function _getRandomTip() {
125
+ return TIPS[Math.floor(Math.random() * TIPS.length)];
126
+ }
127
+ function parseBoolean(value) {
128
+ if (typeof value === "string") {
129
+ return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
130
+ }
131
+ return Boolean(value);
132
+ }
133
+ function supportsAnsi() {
134
+ return process.stdout.isTTY;
135
+ }
136
+ function dim(text) {
137
+ return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
138
+ }
139
+ var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
140
+ function parse(src) {
141
+ const obj = {};
142
+ let lines = src.toString();
143
+ lines = lines.replace(/\r\n?/mg, "\n");
144
+ let match;
145
+ while ((match = LINE.exec(lines)) != null) {
146
+ const key = match[1];
147
+ let value = match[2] || "";
148
+ value = value.trim();
149
+ const maybeQuote = value[0];
150
+ value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
151
+ if (maybeQuote === '"') {
152
+ value = value.replace(/\\n/g, "\n");
153
+ value = value.replace(/\\r/g, "\r");
154
+ }
155
+ obj[key] = value;
156
+ }
157
+ return obj;
158
+ }
159
+ function _parseVault(options2) {
160
+ options2 = options2 || {};
161
+ const vaultPath = _vaultPath(options2);
162
+ options2.path = vaultPath;
163
+ const result = DotenvModule.configDotenv(options2);
164
+ if (!result.parsed) {
165
+ const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
166
+ err.code = "MISSING_DATA";
167
+ throw err;
168
+ }
169
+ const keys = _dotenvKey(options2).split(",");
170
+ const length = keys.length;
171
+ let decrypted;
172
+ for (let i = 0; i < length; i++) {
173
+ try {
174
+ const key = keys[i].trim();
175
+ const attrs = _instructions(result, key);
176
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
177
+ break;
178
+ } catch (error) {
179
+ if (i + 1 >= length) {
180
+ throw error;
181
+ }
182
+ }
183
+ }
184
+ return DotenvModule.parse(decrypted);
185
+ }
186
+ function _warn(message) {
187
+ console.error(`[dotenv@${version}][WARN] ${message}`);
188
+ }
189
+ function _debug(message) {
190
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
191
+ }
192
+ function _log(message) {
193
+ console.log(`[dotenv@${version}] ${message}`);
194
+ }
195
+ function _dotenvKey(options2) {
196
+ if (options2 && options2.DOTENV_KEY && options2.DOTENV_KEY.length > 0) {
197
+ return options2.DOTENV_KEY;
198
+ }
199
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
200
+ return process.env.DOTENV_KEY;
201
+ }
202
+ return "";
203
+ }
204
+ function _instructions(result, dotenvKey) {
205
+ let uri;
206
+ try {
207
+ uri = new URL(dotenvKey);
208
+ } catch (error) {
209
+ if (error.code === "ERR_INVALID_URL") {
210
+ const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
211
+ err.code = "INVALID_DOTENV_KEY";
212
+ throw err;
213
+ }
214
+ throw error;
215
+ }
216
+ const key = uri.password;
217
+ if (!key) {
218
+ const err = new Error("INVALID_DOTENV_KEY: Missing key part");
219
+ err.code = "INVALID_DOTENV_KEY";
220
+ throw err;
221
+ }
222
+ const environment = uri.searchParams.get("environment");
223
+ if (!environment) {
224
+ const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
225
+ err.code = "INVALID_DOTENV_KEY";
226
+ throw err;
227
+ }
228
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
229
+ const ciphertext = result.parsed[environmentKey];
230
+ if (!ciphertext) {
231
+ const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
232
+ err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
233
+ throw err;
234
+ }
235
+ return { ciphertext, key };
236
+ }
237
+ function _vaultPath(options2) {
238
+ let possibleVaultPath = null;
239
+ if (options2 && options2.path && options2.path.length > 0) {
240
+ if (Array.isArray(options2.path)) {
241
+ for (const filepath of options2.path) {
242
+ if (fs.existsSync(filepath)) {
243
+ possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
244
+ }
245
+ }
246
+ } else {
247
+ possibleVaultPath = options2.path.endsWith(".vault") ? options2.path : `${options2.path}.vault`;
248
+ }
249
+ } else {
250
+ possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
251
+ }
252
+ if (fs.existsSync(possibleVaultPath)) {
253
+ return possibleVaultPath;
254
+ }
255
+ return null;
256
+ }
257
+ function _resolveHome(envPath) {
258
+ return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath;
259
+ }
260
+ function _configVault(options2) {
261
+ const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options2 && options2.debug);
262
+ const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options2 && options2.quiet);
263
+ if (debug || !quiet) {
264
+ _log("Loading env from encrypted .env.vault");
265
+ }
266
+ const parsed = DotenvModule._parseVault(options2);
267
+ let processEnv = process.env;
268
+ if (options2 && options2.processEnv != null) {
269
+ processEnv = options2.processEnv;
270
+ }
271
+ DotenvModule.populate(processEnv, parsed, options2);
272
+ return { parsed };
273
+ }
274
+ function configDotenv(options2) {
275
+ const dotenvPath = path.resolve(process.cwd(), ".env");
276
+ let encoding = "utf8";
277
+ let processEnv = process.env;
278
+ if (options2 && options2.processEnv != null) {
279
+ processEnv = options2.processEnv;
280
+ }
281
+ let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options2 && options2.debug);
282
+ let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options2 && options2.quiet);
283
+ if (options2 && options2.encoding) {
284
+ encoding = options2.encoding;
285
+ } else {
286
+ if (debug) {
287
+ _debug("No encoding is specified. UTF-8 is used by default");
288
+ }
289
+ }
290
+ let optionPaths = [dotenvPath];
291
+ if (options2 && options2.path) {
292
+ if (!Array.isArray(options2.path)) {
293
+ optionPaths = [_resolveHome(options2.path)];
294
+ } else {
295
+ optionPaths = [];
296
+ for (const filepath of options2.path) {
297
+ optionPaths.push(_resolveHome(filepath));
298
+ }
299
+ }
300
+ }
301
+ let lastError;
302
+ const parsedAll = {};
303
+ for (const path2 of optionPaths) {
304
+ try {
305
+ const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
306
+ DotenvModule.populate(parsedAll, parsed, options2);
307
+ } catch (e) {
308
+ if (debug) {
309
+ _debug(`Failed to load ${path2} ${e.message}`);
310
+ }
311
+ lastError = e;
312
+ }
313
+ }
314
+ const populated = DotenvModule.populate(processEnv, parsedAll, options2);
315
+ debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
316
+ quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
317
+ if (debug || !quiet) {
318
+ const keysCount = Object.keys(populated).length;
319
+ const shortPaths = [];
320
+ for (const filePath of optionPaths) {
321
+ try {
322
+ const relative = path.relative(process.cwd(), filePath);
323
+ shortPaths.push(relative);
324
+ } catch (e) {
325
+ if (debug) {
326
+ _debug(`Failed to load ${filePath} ${e.message}`);
327
+ }
328
+ lastError = e;
329
+ }
330
+ }
331
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
332
+ }
333
+ if (lastError) {
334
+ return { parsed: parsedAll, error: lastError };
335
+ } else {
336
+ return { parsed: parsedAll };
337
+ }
338
+ }
339
+ function config2(options2) {
340
+ if (_dotenvKey(options2).length === 0) {
341
+ return DotenvModule.configDotenv(options2);
342
+ }
343
+ const vaultPath = _vaultPath(options2);
344
+ if (!vaultPath) {
345
+ _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
346
+ return DotenvModule.configDotenv(options2);
347
+ }
348
+ return DotenvModule._configVault(options2);
349
+ }
350
+ function decrypt(encrypted, keyStr) {
351
+ const key = Buffer.from(keyStr.slice(-64), "hex");
352
+ let ciphertext = Buffer.from(encrypted, "base64");
353
+ const nonce = ciphertext.subarray(0, 12);
354
+ const authTag = ciphertext.subarray(-16);
355
+ ciphertext = ciphertext.subarray(12, -16);
356
+ try {
357
+ const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
358
+ aesgcm.setAuthTag(authTag);
359
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
360
+ } catch (error) {
361
+ const isRange = error instanceof RangeError;
362
+ const invalidKeyLength = error.message === "Invalid key length";
363
+ const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
364
+ if (isRange || invalidKeyLength) {
365
+ const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
366
+ err.code = "INVALID_DOTENV_KEY";
367
+ throw err;
368
+ } else if (decryptionFailed) {
369
+ const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
370
+ err.code = "DECRYPTION_FAILED";
371
+ throw err;
372
+ } else {
373
+ throw error;
374
+ }
375
+ }
376
+ }
377
+ function populate(processEnv, parsed, options2 = {}) {
378
+ const debug = Boolean(options2 && options2.debug);
379
+ const override = Boolean(options2 && options2.override);
380
+ const populated = {};
381
+ if (typeof parsed !== "object") {
382
+ const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
383
+ err.code = "OBJECT_REQUIRED";
384
+ throw err;
385
+ }
386
+ for (const key of Object.keys(parsed)) {
387
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
388
+ if (override === true) {
389
+ processEnv[key] = parsed[key];
390
+ populated[key] = parsed[key];
391
+ }
392
+ if (debug) {
393
+ if (override === true) {
394
+ _debug(`"${key}" is already defined and WAS overwritten`);
395
+ } else {
396
+ _debug(`"${key}" is already defined and was NOT overwritten`);
397
+ }
398
+ }
399
+ } else {
400
+ processEnv[key] = parsed[key];
401
+ populated[key] = parsed[key];
402
+ }
403
+ }
404
+ return populated;
405
+ }
406
+ var DotenvModule = {
407
+ configDotenv,
408
+ _configVault,
409
+ _parseVault,
410
+ config: config2,
411
+ decrypt,
412
+ parse,
413
+ populate
414
+ };
415
+ module2.exports.configDotenv = DotenvModule.configDotenv;
416
+ module2.exports._configVault = DotenvModule._configVault;
417
+ module2.exports._parseVault = DotenvModule._parseVault;
418
+ module2.exports.config = DotenvModule.config;
419
+ module2.exports.decrypt = DotenvModule.decrypt;
420
+ module2.exports.parse = DotenvModule.parse;
421
+ module2.exports.populate = DotenvModule.populate;
422
+ module2.exports = DotenvModule;
423
+ }
424
+ });
425
+
26
426
  // src/cli.ts
27
- var import_tool_debug_g44 = require("@mcesystems/tool-debug-g4");
427
+ var import_tool_debug_g45 = require("@mcesystems/tool-debug-g4");
428
+ var import_dotenv = __toESM(require_main());
28
429
 
29
430
  // src/UsbmuxdService.ts
30
431
  var import_node_events2 = require("node:events");
31
- var import_tool_debug_g43 = require("@mcesystems/tool-debug-g4");
432
+ var import_tool_debug_g44 = require("@mcesystems/tool-debug-g4");
32
433
  var import_usb_device_listener = __toESM(require("@mcesystems/usb-device-listener"));
33
434
 
34
435
  // src/InstanceManager.ts
35
- var import_node_child_process2 = require("node:child_process");
36
436
  var import_node_events = require("node:events");
37
- var import_node_util2 = require("node:util");
38
- var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
437
+ var import_tool_debug_g43 = require("@mcesystems/tool-debug-g4");
39
438
 
40
- // src/LockdownSync.ts
439
+ // src/usbipd.ts
41
440
  var import_node_child_process = require("node:child_process");
42
- var import_node_fs = require("node:fs");
43
- var import_node_path = require("node:path");
44
441
  var import_node_util = require("node:util");
45
442
  var import_tool_debug_g4 = require("@mcesystems/tool-debug-g4");
46
- var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbmuxd-instance-manager");
443
+ var { logInfo, logWarning } = (0, import_tool_debug_g4.createLoggers)("usbipd");
47
444
  var execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
48
- var ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
49
- var SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
50
- function windowsPathToWsl(windowsPath) {
51
- const normalized = windowsPath.replace(/\\/g, "/").trim();
52
- const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
53
- if (driveMatch) {
54
- const drive = driveMatch[1].toLowerCase();
55
- const rest = driveMatch[2] || "";
56
- return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
57
- }
58
- return normalized;
59
- }
60
- async function syncToAlpine(udid, config2) {
61
- if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
62
- return;
63
- }
64
- const windowsDir = config2.lockdownWindowsPath.trim();
65
- const windowsFile = (0, import_node_path.join)(windowsDir, `${udid}.plist`);
66
- if (!(0, import_node_fs.existsSync)(windowsFile)) {
67
- return;
68
- }
69
- const distro = config2.wslDistribution || "alpine-usbmuxd-build";
70
- const wslSource = windowsPathToWsl(windowsFile);
71
- const wslDestDir = ALPINE_LOCKDOWN_DIR;
72
- try {
73
- await execAsync(
74
- `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
75
- );
76
- logInfo(`Lockdown sync: copied ${udid}.plist to Alpine`);
77
- } catch (error) {
445
+ var Usbipd = class {
446
+ constructor(usbipdPath) {
447
+ this.usbipdPath = usbipdPath;
448
+ }
449
+ async unbindAllDevicesFromWsl() {
450
+ try {
451
+ await execAsync(`"${this.usbipdPath}" unbind -a`);
452
+ logInfo("All devices unbound from WSL");
453
+ } catch (error) {
454
+ logWarning(`Failed to unbind all devices from WSL: ${error}`);
455
+ }
456
+ }
457
+ async detachAllDevicesFromWsl() {
458
+ try {
459
+ await execAsync(`"${this.usbipdPath}" detach -a`);
460
+ logInfo("All devices detached from WSL");
461
+ } catch (error) {
462
+ logWarning(`Failed to detach all devices from WSL: ${error}`);
463
+ }
464
+ }
465
+ /**
466
+ * Detach a device from WSL via usbipd
467
+ */
468
+ async detachDeviceFromWsl(busId) {
469
+ try {
470
+ await execAsync(`"${this.usbipdPath}" detach --busid=${busId}`);
471
+ logInfo(`Device ${busId} detached from WSL`);
472
+ } catch (error) {
473
+ const message = error instanceof Error ? error.message : String(error);
474
+ if (message.includes("no device with busid") || message.includes("There is no device")) {
475
+ logInfo(`Device ${busId} already detached`);
476
+ } else {
477
+ logWarning(`Failed to detach device ${busId}: ${error}`);
478
+ }
479
+ }
480
+ }
481
+ /**
482
+ * Attach a device to WSL via usbipd
483
+ * Note: This requires administrator privileges
484
+ */
485
+ async attachDeviceToWsl(busId, wsl, distro) {
486
+ try {
487
+ await wsl.ensureWslRunning();
488
+ logInfo(`Binding device ${busId}...`);
489
+ await execAsync(`"${this.usbipdPath}" bind --busid ${busId} --force`);
490
+ if (distro) {
491
+ logInfo(`Attaching device ${busId} to WSL distribution ${distro}...`);
492
+ await execAsync(`"${this.usbipdPath}" attach --wsl=${distro} --busid=${busId}`);
493
+ } else {
494
+ logInfo(`Attaching device ${busId} to default WSL...`);
495
+ await execAsync(`"${this.usbipdPath}" attach --wsl --busid=${busId}`);
496
+ }
497
+ logInfo(`Device ${busId} attached to WSL successfully`);
498
+ return true;
499
+ } catch (error) {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ if (message.includes("already attached to a client")) {
502
+ logInfo(`Device ${busId} is already attached to WSL, continuing`);
503
+ return true;
504
+ }
505
+ logWarning(`Failed to attach device ${busId} to WSL: ${error}`);
506
+ return false;
507
+ }
508
+ }
509
+ /**
510
+ * Parse usbipd list output to extract device information
511
+ */
512
+ parseUsbipdList(output) {
513
+ const devices = [];
514
+ const lines = output.split(/\r?\n/);
515
+ for (const line of lines) {
516
+ const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
517
+ if (match) {
518
+ const rest = match[4].trim();
519
+ const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
520
+ const description = stateMatch ? stateMatch[1].trim() : rest;
521
+ const state = stateMatch ? stateMatch[2].trim() : "Unknown";
522
+ devices.push({
523
+ busId: match[1],
524
+ vid: match[2].toUpperCase(),
525
+ pid: match[3].toUpperCase(),
526
+ description,
527
+ state
528
+ });
529
+ }
530
+ }
531
+ return devices;
532
+ }
533
+ /**
534
+ * Find the usbipd bus ID for a device by matching VID/PID
535
+ */
536
+ async findBusIdForDevice(device) {
537
+ try {
538
+ const { stdout } = await execAsync(`"${this.usbipdPath}" list`);
539
+ const usbipdDevices = this.parseUsbipdList(stdout);
540
+ const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
541
+ const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
542
+ logInfo(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
543
+ logInfo(`Found ${usbipdDevices.length} devices from usbipd list`);
544
+ const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
545
+ if (match) {
546
+ logInfo(
547
+ `Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
548
+ );
549
+ return match.busId;
550
+ }
551
+ throw new Error(
552
+ `Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
553
+ );
554
+ } catch (error) {
555
+ throw new Error(`Failed to run usbipd list: ${error}`);
556
+ }
557
+ }
558
+ };
559
+
560
+ // src/wsl.ts
561
+ var import_node_child_process2 = require("node:child_process");
562
+ var import_node_fs = require("node:fs");
563
+ var import_node_path = require("node:path");
564
+ var import_node_util2 = require("node:util");
565
+ var import_tool_debug_g42 = require("@mcesystems/tool-debug-g4");
566
+ var { logInfo: logInfo2, logWarning: logWarning2, logDetail } = (0, import_tool_debug_g42.createLoggers)("wsl");
567
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
568
+ var Wsl2 = class {
569
+ constructor(wslDistribution) {
570
+ this.wslDistribution = wslDistribution;
571
+ }
572
+ ALPINE_LOCKDOWN_DIR = "/var/lib/lockdown";
573
+ SYSTEM_CONFIG_PLIST = "SystemConfiguration.plist";
574
+ wslIpAddress = null;
575
+ /**
576
+ * Detect the WSL2 IP address for the configured distribution
577
+ * This IP is needed to connect from Windows to services inside WSL
578
+ */
579
+ async detectWslIpAddress() {
580
+ if (this.wslIpAddress) {
581
+ return this.wslIpAddress;
582
+ }
583
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
584
+ try {
585
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
586
+ const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
587
+ if (match) {
588
+ this.wslIpAddress = match[1];
589
+ logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
590
+ return this.wslIpAddress;
591
+ }
592
+ } catch (error) {
593
+ logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
594
+ }
595
+ try {
596
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
597
+ const ip = stdout.trim().split(/\s+/)[0];
598
+ if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
599
+ this.wslIpAddress = ip;
600
+ logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
601
+ return this.wslIpAddress;
602
+ }
603
+ } catch (error) {
604
+ logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
605
+ }
606
+ logWarning2("Could not detect WSL IP, falling back to localhost");
607
+ this.wslIpAddress = "127.0.0.1";
608
+ return this.wslIpAddress;
609
+ }
610
+ /**
611
+ * Ensure the WSL distribution is running
612
+ */
613
+ async ensureWslRunning() {
614
+ const distro = this.wslDistribution;
615
+ try {
616
+ if (distro) {
617
+ logInfo2(`Starting WSL distribution: ${distro}...`);
618
+ await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
619
+ } else {
620
+ logInfo2("Starting default WSL distribution...");
621
+ await execAsync2(`wsl -- echo "WSL started"`);
622
+ }
623
+ return true;
624
+ } catch (error) {
625
+ logWarning2(`Failed to start WSL: ${error}`);
626
+ return false;
627
+ }
628
+ }
629
+ /**
630
+ * Create a new usbmuxd instance
631
+ */
632
+ async createInstance({
633
+ id,
634
+ basePort,
635
+ verboseLogging,
636
+ usbmuxdPath,
637
+ onLog,
638
+ onExit
639
+ }) {
640
+ const port = basePort + id - 1;
641
+ logInfo2(`Creating instance ${id} on port ${port}`);
642
+ const host = await this.detectWslIpAddress();
643
+ const usbmuxdArgs = [
644
+ "-f",
645
+ // Foreground
646
+ "-v",
647
+ // Verbose (if enabled)
648
+ "-S",
649
+ `0.0.0.0:${port}`,
650
+ // Listen on all interfaces (for Windows → WSL2)
651
+ "--pidfile",
652
+ "NONE"
653
+ ];
654
+ if (!verboseLogging) {
655
+ usbmuxdArgs.splice(1, 1);
656
+ }
657
+ const wslArgs = [
658
+ "-d",
659
+ this.wslDistribution || "alpine-usbmuxd-build",
660
+ usbmuxdPath,
661
+ ...usbmuxdArgs
662
+ ];
663
+ const process2 = (0, import_node_child_process2.spawn)("wsl", wslArgs, {
664
+ stdio: ["ignore", "pipe", "pipe"],
665
+ detached: true,
666
+ windowsHide: false
667
+ });
668
+ process2.stdout?.on("data", (data) => {
669
+ onLog(data.toString().trim());
670
+ });
671
+ process2.stderr?.on("data", (data) => {
672
+ onLog(data.toString().trim());
673
+ });
674
+ process2.on("exit", onExit);
675
+ const pid = process2.pid;
676
+ if (pid === void 0) {
677
+ process2.kill("SIGKILL");
678
+ throw new Error("Failed to get PID for usbmuxd instance");
679
+ }
680
+ const instance = {
681
+ id,
682
+ host,
683
+ port,
684
+ pid,
685
+ deviceUdids: [],
686
+ startedAt: /* @__PURE__ */ new Date()
687
+ };
688
+ return { instance, process: process2 };
689
+ }
690
+ /**
691
+ * Convert a Windows path to the equivalent path inside WSL (e.g. C:\foo\bar -> /mnt/c/foo/bar).
692
+ */
693
+ windowsPathToWsl(windowsPath) {
694
+ const normalized = windowsPath.replace(/\\/g, "/").trim();
695
+ const driveMatch = normalized.match(/^([a-zA-Z]):\/?(.*)$/);
696
+ if (driveMatch) {
697
+ const drive = driveMatch[1].toLowerCase();
698
+ const rest = driveMatch[2] || "";
699
+ return `/mnt/${drive}${rest ? `/${rest}` : ""}`;
700
+ }
701
+ return normalized;
702
+ }
703
+ /**
704
+ * Get the native Apple lockdown directory path for the current platform.
705
+ * This is where Apple/iTunes stores pairing records natively.
706
+ */
707
+ getAppleLockdownPath() {
708
+ if (process.platform === "win32") {
709
+ return (0, import_node_path.join)(process.env.ProgramData ?? "C:\\ProgramData", "Apple", "Lockdown");
710
+ }
711
+ if (process.platform === "darwin") {
712
+ return "/var/db/lockdown";
713
+ }
714
+ if (process.platform === "linux") {
715
+ return "/var/lib/lockdown";
716
+ }
717
+ return null;
718
+ }
719
+ /**
720
+ * Copy a single device's lockdown plist from the native Apple lockdown directory
721
+ * to Alpine so usbmuxd can use it (skip pairing).
722
+ *
723
+ * Uses the platform-specific Apple/iTunes lockdown directory
724
+ * (e.g. C:\ProgramData\Apple\Lockdown on Windows, /var/db/lockdown on macOS).
725
+ *
726
+ * No-op if the lockdown directory cannot be determined or files don't exist.
727
+ */
728
+ async syncToAlpine(udid) {
729
+ const lockdownDir = this.getAppleLockdownPath();
730
+ if (!lockdownDir) {
731
+ logWarning2("Lockdown sync to Alpine: unsupported platform, skipping");
732
+ return;
733
+ }
734
+ logDetail(`Lockdown sync to Alpine: source dir = ${lockdownDir}, udid = ${udid}`);
735
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
736
+ const wslDestDir = this.ALPINE_LOCKDOWN_DIR;
737
+ const devicePlistFile = `${udid}.plist`;
738
+ const devicePlistPath = (0, import_node_path.join)(lockdownDir, devicePlistFile);
739
+ if ((0, import_node_fs.existsSync)(devicePlistPath)) {
740
+ await this.copyFileToAlpine(
741
+ this.windowsPathToWsl(devicePlistPath),
742
+ wslDestDir,
743
+ distro,
744
+ devicePlistFile
745
+ );
746
+ } else {
747
+ logDetail(
748
+ `Lockdown sync to Alpine: ${devicePlistFile} not found at ${devicePlistPath}, skipping`
749
+ );
750
+ }
751
+ const systemConfigPath = (0, import_node_path.join)(lockdownDir, this.SYSTEM_CONFIG_PLIST);
752
+ if ((0, import_node_fs.existsSync)(systemConfigPath)) {
753
+ await this.copyFileToAlpine(
754
+ this.windowsPathToWsl(systemConfigPath),
755
+ wslDestDir,
756
+ distro,
757
+ this.SYSTEM_CONFIG_PLIST
758
+ );
759
+ } else {
760
+ logDetail(
761
+ `Lockdown sync to Alpine: ${this.SYSTEM_CONFIG_PLIST} not found at ${systemConfigPath}, skipping`
762
+ );
763
+ }
764
+ }
765
+ /**
766
+ * Copy a single file into the Alpine lockdown directory.
767
+ * Falls back to sudo if the initial copy fails.
768
+ */
769
+ async copyFileToAlpine(wslSource, wslDestDir, distro, fileName) {
770
+ try {
771
+ await execAsync2(
772
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && cp '${wslSource}' '${wslDestDir}/'"`
773
+ );
774
+ logInfo2(`Lockdown sync: copied ${fileName} to Alpine`);
775
+ } catch (error) {
776
+ try {
777
+ await execAsync2(
778
+ `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
779
+ );
780
+ logInfo2(`Lockdown sync: copied ${fileName} to Alpine (via sudo)`);
781
+ } catch (sudoError) {
782
+ logWarning2(
783
+ `Lockdown sync to Alpine failed for ${fileName}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
784
+ );
785
+ }
786
+ }
787
+ }
788
+ /**
789
+ * Copy lockdown plists from Alpine back to the native Apple lockdown directory
790
+ * (for devices that were assigned this session).
791
+ * Also copies SystemConfiguration.plist if present in Alpine.
792
+ *
793
+ * No-op if the lockdown directory cannot be determined.
794
+ */
795
+ async syncFromAlpine(udids) {
796
+ logInfo2(`Lockdown sync from Alpine: starting (${udids.length} device(s))`);
797
+ const lockdownDir = this.getAppleLockdownPath();
798
+ if (!lockdownDir) {
799
+ logWarning2("Lockdown sync: unsupported platform, skipping");
800
+ return;
801
+ }
802
+ logDetail(`Lockdown sync: target dir = ${lockdownDir}`);
803
+ try {
804
+ (0, import_node_fs.mkdirSync)(lockdownDir, { recursive: true });
805
+ } catch (error) {
806
+ logWarning2(`Lockdown sync: could not create lockdown dir ${lockdownDir}: ${error}`);
807
+ return;
808
+ }
809
+ const distro = this.wslDistribution || "alpine-usbmuxd-build";
810
+ const wslDestDir = this.windowsPathToWsl(lockdownDir);
78
811
  try {
79
- await execAsync(
80
- `wsl -d ${distro} -- sh -c "mkdir -p ${wslDestDir} && sudo cp '${wslSource}' '${wslDestDir}/'"`
81
- );
82
- logInfo(`Lockdown sync: copied ${udid}.plist to Alpine (via sudo)`);
83
- } catch (sudoError) {
84
- logWarning(
85
- `Lockdown sync to Alpine failed for ${udid}: ${error}. Sudo fallback failed: ${sudoError}. Ensure /var/lib/lockdown is writable or use passwordless sudo.`
86
- );
812
+ const { stdout } = await execAsync2(`wsl -d ${distro} -- ls -la ${this.ALPINE_LOCKDOWN_DIR}/`);
813
+ logDetail(`Lockdown sync: Alpine ${this.ALPINE_LOCKDOWN_DIR} contents:
814
+ ${stdout}`);
815
+ } catch (error) {
816
+ logWarning2(`Lockdown sync: could not list Alpine lockdown dir: ${error}`);
87
817
  }
818
+ const filesToSync = [...udids.map((udid) => `${udid}.plist`), this.SYSTEM_CONFIG_PLIST];
819
+ logDetail(`Lockdown sync: files to sync = [${filesToSync.join(", ")}]`);
820
+ for (const fileName of filesToSync) {
821
+ await this.copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir);
822
+ }
823
+ logInfo2("Lockdown sync from Alpine: done");
88
824
  }
89
- }
90
- async function syncFromAlpine(udids, config2) {
91
- if (!config2.lockdownSyncEnabled || !config2.lockdownWindowsPath?.trim()) {
92
- return;
93
- }
94
- const windowsDir = config2.lockdownWindowsPath.trim();
95
- try {
96
- (0, import_node_fs.mkdirSync)(windowsDir, { recursive: true });
97
- } catch (error) {
98
- logWarning(`Lockdown sync: could not create Windows dir ${windowsDir}: ${error}`);
99
- return;
100
- }
101
- const distro = config2.wslDistribution || "alpine-usbmuxd-build";
102
- const wslDestDir = windowsPathToWsl(windowsDir);
103
- for (const udid of udids) {
104
- const plist = `${udid}.plist`;
825
+ /**
826
+ * Copy a single file from the Alpine lockdown directory back to Windows.
827
+ * Skips if the file does not exist in Alpine; logs a warning if the copy fails.
828
+ */
829
+ async copyFileFromAlpine(fileName, wslDestDir, distro, lockdownDir) {
830
+ const alpinePath = `${this.ALPINE_LOCKDOWN_DIR}/${fileName}`;
105
831
  try {
106
- await execAsync(
107
- `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${plist}' && cp '${ALPINE_LOCKDOWN_DIR}/${plist}' '${wslDestDir}/'"`
108
- );
109
- logInfo(`Lockdown sync: copied ${plist} from Alpine to Windows`);
832
+ await execAsync2(`wsl -d ${distro} -- test -f '${alpinePath}'`);
110
833
  } catch {
834
+ logDetail(`Lockdown sync: ${fileName} not found in Alpine (${alpinePath}), skipping`);
835
+ return;
836
+ }
837
+ logDetail(`Lockdown sync: copying ${fileName} \u2192 ${wslDestDir}/`);
838
+ try {
839
+ await execAsync2(`wsl -d ${distro} -- cp -f '${alpinePath}' '${wslDestDir}/'`);
840
+ logInfo2(`Lockdown sync: copied ${fileName} from Alpine to ${lockdownDir}`);
841
+ } catch (error) {
842
+ logWarning2(`Lockdown sync: failed to copy ${fileName} from Alpine to ${lockdownDir}: ${error}`);
111
843
  }
112
844
  }
113
- try {
114
- await execAsync(
115
- `wsl -d ${distro} -- sh -c "test -f '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' && cp '${ALPINE_LOCKDOWN_DIR}/${SYSTEM_CONFIG_PLIST}' '${wslDestDir}/'"`
116
- );
117
- logInfo(`Lockdown sync: copied ${SYSTEM_CONFIG_PLIST} from Alpine to Windows`);
118
- } catch {
119
- }
120
- }
845
+ };
121
846
 
122
847
  // src/InstanceManager.ts
123
- var { logInfo: logInfo2, logWarning: logWarning2 } = (0, import_tool_debug_g42.createLoggers)("usbmuxd-instance-manager");
124
- var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.exec);
125
- var USBIPD_PATH = '"C:\\Program Files\\usbipd-win\\usbipd.exe"';
126
- function parseUsbipdList(output) {
127
- const devices = [];
128
- const lines = output.split(/\r?\n/);
129
- for (const line of lines) {
130
- const match = line.match(/^(\d+-\d+)\s+([0-9a-f]{4}):([0-9a-f]{4})\s+(.+)$/i);
131
- if (match) {
132
- const rest = match[4].trim();
133
- const stateMatch = rest.match(/^(.+?)\s{2,}(\S.*)$/);
134
- const description = stateMatch ? stateMatch[1].trim() : rest;
135
- const state = stateMatch ? stateMatch[2].trim() : "Unknown";
136
- devices.push({
137
- busId: match[1],
138
- vid: match[2].toUpperCase(),
139
- pid: match[3].toUpperCase(),
140
- description,
141
- state
142
- });
143
- }
144
- }
145
- return devices;
146
- }
848
+ var { logWarning: logWarning3, logDetail: logDetail2, logDataObject, logTask } = (0, import_tool_debug_g43.createLoggers)("instance-manager");
147
849
  var DEFAULT_CONFIG = {
148
850
  batchSize: 4,
149
851
  basePort: 27015,
150
- maxInstances: 20,
852
+ maxInstances: 5,
151
853
  usbmuxdPath: "usbmuxd",
152
854
  // Path inside WSL2
153
855
  wslDistribution: "alpine-usbmuxd-build",
154
856
  // Alpine WSL2 distribution name
155
857
  verboseLogging: true,
156
- appleVendorId: "05AC",
157
- lockdownWindowsPath: "C:\\ProgramData\\mce\\lockdown",
158
- lockdownSyncEnabled: true
858
+ appleVendorId: "05AC"
159
859
  };
160
860
  var InstanceManager = class extends import_node_events.EventEmitter {
161
- config;
861
+ /** Map of usbmuxd instance IDs to instances */
162
862
  instances = /* @__PURE__ */ new Map();
863
+ /** Map of device IDs to device mappings host:port */
163
864
  deviceMappings = /* @__PURE__ */ new Map();
865
+ /** Map of usbmuxd instance IDs to child processes running it on wsl2*/
164
866
  processes = /* @__PURE__ */ new Map();
165
- nextInstanceId = 1;
166
- startedAt = null;
167
- isRunning = false;
168
867
  /** Tracks which devices have been attached to WSL */
169
868
  attachedDevices = /* @__PURE__ */ new Set();
170
- /** Device IDs currently in the attach flow (ignore disconnect until attach completes) */
869
+ /** Device IDs currently in the attach flow
870
+ * (ignore disconnect until attach completes) */
171
871
  pendingAttachDevices = /* @__PURE__ */ new Set();
172
- /** Cached WSL IP address for connecting from Windows */
173
- wslIpAddress = null;
872
+ config;
873
+ nextInstanceId = 1;
874
+ startedAt = null;
875
+ isRunning = false;
876
+ wsl;
877
+ usbipd;
174
878
  constructor(config2 = {}) {
175
879
  super();
176
880
  this.config = { ...DEFAULT_CONFIG, ...config2 };
177
- }
178
- /**
179
- * Detect the WSL2 IP address for the configured distribution
180
- * This IP is needed to connect from Windows to services inside WSL
181
- */
182
- async detectWslIpAddress() {
183
- if (this.wslIpAddress) {
184
- return this.wslIpAddress;
185
- }
186
- const distro = this.config.wslDistribution || "alpine-usbmuxd-build";
187
- try {
188
- const { stdout } = await execAsync2(`wsl -d ${distro} -- ip -4 addr show eth0`);
189
- const match = stdout.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
190
- if (match) {
191
- this.wslIpAddress = match[1];
192
- logInfo2(`Detected WSL IP address: ${this.wslIpAddress}`);
193
- return this.wslIpAddress;
194
- }
195
- } catch (error) {
196
- logWarning2(`Failed to detect WSL IP via ip addr: ${error}`);
197
- }
198
- try {
199
- const { stdout } = await execAsync2(`wsl -d ${distro} -- hostname -I`);
200
- const ip = stdout.trim().split(/\s+/)[0];
201
- if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
202
- this.wslIpAddress = ip;
203
- logInfo2(`Detected WSL IP address (hostname): ${this.wslIpAddress}`);
204
- return this.wslIpAddress;
205
- }
206
- } catch (error) {
207
- logWarning2(`Failed to detect WSL IP via hostname: ${error}`);
208
- }
209
- logWarning2("Could not detect WSL IP, falling back to localhost");
210
- this.wslIpAddress = "127.0.0.1";
211
- return this.wslIpAddress;
881
+ this.wsl = new Wsl2(this.config.wslDistribution);
882
+ this.usbipd = new Usbipd(process.env.USBIPD_PATH ?? "usbipd");
212
883
  }
213
884
  /**
214
885
  * Start the instance manager
@@ -219,7 +890,6 @@ var InstanceManager = class extends import_node_events.EventEmitter {
219
890
  }
220
891
  this.isRunning = true;
221
892
  this.startedAt = /* @__PURE__ */ new Date();
222
- this.emit("started");
223
893
  }
224
894
  /**
225
895
  * Stop the instance manager and all instances
@@ -230,147 +900,122 @@ var InstanceManager = class extends import_node_events.EventEmitter {
230
900
  }
231
901
  this.isRunning = false;
232
902
  const udids = Array.from(this.deviceMappings.keys());
233
- await syncFromAlpine(udids, this.config);
234
- const detachPromises = Array.from(this.attachedDevices).map(
235
- (busId) => this.detachDeviceFromWsl(busId)
236
- );
237
- await Promise.all(detachPromises);
903
+ await this.wsl.syncFromAlpine(udids);
904
+ await this.usbipd.detachAllDevicesFromWsl();
905
+ await this.usbipd.unbindAllDevicesFromWsl();
238
906
  this.attachedDevices.clear();
239
907
  const stopPromises = Array.from(this.instances.keys()).map((id) => this.stopInstance(id));
240
908
  await Promise.all(stopPromises);
241
909
  this.instances.clear();
242
910
  this.deviceMappings.clear();
243
911
  this.processes.clear();
244
- this.emit("stopped");
245
912
  }
246
- /**
247
- * Find the usbipd bus ID for a device by matching VID/PID
248
- */
249
- async findBusIdForDevice(device) {
250
- try {
251
- const { stdout } = await execAsync2(`${USBIPD_PATH} list`);
252
- const usbipdDevices = parseUsbipdList(stdout);
253
- const deviceVid = device.vid.toString(16).toUpperCase().padStart(4, "0");
254
- const devicePid = device.pid.toString(16).toUpperCase().padStart(4, "0");
255
- logInfo2(`Looking for device with VID:PID ${deviceVid}:${devicePid}`);
256
- logInfo2(`Found ${usbipdDevices.length} devices from usbipd list`);
257
- const match = usbipdDevices.find((d) => d.vid === deviceVid && d.pid === devicePid);
258
- if (match) {
259
- logInfo2(
260
- `Found usbipd bus ID ${match.busId} for device ${device.deviceId} (${deviceVid}:${devicePid})`
261
- );
262
- return match.busId;
913
+ async attachToWsl({
914
+ busId,
915
+ deviceId
916
+ }) {
917
+ if (!this.attachedDevices.has(busId)) {
918
+ this.pendingAttachDevices.add(deviceId);
919
+ if (!this.config.wslDistribution) {
920
+ throw new Error("WSL distribution not configured");
263
921
  }
264
- for (const d of usbipdDevices) {
265
- logInfo2(` usbipd device: ${d.busId} ${d.vid}:${d.pid} "${d.description}"`);
266
- }
267
- logWarning2(
268
- `Could not find usbipd bus ID for device ${device.deviceId} (${deviceVid}:${devicePid})`
922
+ const attached = await this.usbipd.attachDeviceToWsl(
923
+ busId,
924
+ this.wsl,
925
+ this.config.wslDistribution
269
926
  );
270
- return null;
271
- } catch (error) {
272
- logWarning2(`Failed to run usbipd list: ${error}`);
273
- return null;
927
+ if (attached) {
928
+ this.attachedDevices.add(busId);
929
+ }
274
930
  }
275
931
  }
276
- /**
277
- * Ensure the WSL distribution is running
278
- */
279
- async ensureWslRunning() {
280
- const distro = this.config.wslDistribution;
281
- try {
282
- if (distro) {
283
- logInfo2(`Starting WSL distribution: ${distro}...`);
284
- await execAsync2(`wsl -d ${distro} -- echo "WSL started"`);
285
- } else {
286
- logInfo2("Starting default WSL distribution...");
287
- await execAsync2(`wsl -- echo "WSL started"`);
288
- }
289
- return true;
290
- } catch (error) {
291
- logWarning2(`Failed to start WSL: ${error}`);
292
- return false;
932
+ async removeDevice(mapping) {
933
+ const instance = await this.detachFromWsl({ deviceId: mapping.udid });
934
+ if (!instance) {
935
+ throw new Error(`Instance ${mapping.instanceId} not found`);
936
+ }
937
+ if (mapping.busId) {
938
+ await this.usbipd.detachDeviceFromWsl(mapping.busId);
939
+ this.attachedDevices.delete(mapping.busId);
293
940
  }
941
+ const deviceIndex = instance.deviceUdids.indexOf(mapping.udid);
942
+ if (deviceIndex > -1) {
943
+ instance.deviceUdids.splice(deviceIndex, 1);
944
+ }
945
+ this.deviceMappings.delete(mapping.udid);
946
+ return instance;
294
947
  }
295
- /**
296
- * Attach a device to WSL via usbipd
297
- * Note: This requires administrator privileges
298
- */
299
- async attachDeviceToWsl(busId) {
300
- const distro = this.config.wslDistribution;
301
- try {
302
- await this.ensureWslRunning();
303
- logInfo2(`Binding device ${busId}...`);
304
- await execAsync2(`${USBIPD_PATH} bind --busid ${busId} --force`);
305
- if (distro) {
306
- logInfo2(`Attaching device ${busId} to WSL distribution ${distro}...`);
307
- await execAsync2(`${USBIPD_PATH} attach --wsl=${distro} --busid=${busId}`);
308
- } else {
309
- logInfo2(`Attaching device ${busId} to default WSL...`);
310
- await execAsync2(`${USBIPD_PATH} attach --wsl --busid=${busId}`);
311
- }
312
- logInfo2(`Device ${busId} attached to WSL successfully`);
313
- return true;
314
- } catch (error) {
315
- const message = error instanceof Error ? error.message : String(error);
316
- if (message.includes("already attached to a client")) {
317
- logInfo2(`Device ${busId} is already attached to WSL, continuing`);
318
- return true;
948
+ async onUsbmuxdInstanceEnd({
949
+ instanceId,
950
+ code,
951
+ signal
952
+ }) {
953
+ this.emit("instance-exited", {
954
+ instanceId,
955
+ code,
956
+ signal
957
+ });
958
+ for (const [_, mapping] of this.deviceMappings.entries()) {
959
+ if (mapping.instanceId === instanceId) {
960
+ this.removeDevice(mapping);
319
961
  }
320
- logWarning2(`Failed to attach device ${busId} to WSL: ${error}`);
321
- return false;
322
962
  }
963
+ this.instances.delete(instanceId);
964
+ this.processes.get(instanceId)?.kill();
965
+ this.processes.delete(instanceId);
323
966
  }
324
- /**
325
- * Detach a device from WSL via usbipd
326
- */
327
- async detachDeviceFromWsl(busId) {
328
- try {
329
- await execAsync2(`${USBIPD_PATH} detach --busid=${busId}`);
330
- logInfo2(`Device ${busId} detached from WSL`);
331
- } catch (error) {
332
- const message = error instanceof Error ? error.message : String(error);
333
- if (message.includes("no device with busid") || message.includes("There is no device")) {
334
- logInfo2(`Device ${busId} already detached`);
335
- } else {
336
- logWarning2(`Failed to detach device ${busId}: ${error}`);
967
+ async onUsbmuxdInstanceStart({
968
+ instance,
969
+ process: process2
970
+ }) {
971
+ this.instances.set(instance.id, instance);
972
+ this.processes.set(instance.id, process2);
973
+ this.emit("instance-started", {
974
+ instanceId: instance.id
975
+ });
976
+ }
977
+ async getInstance() {
978
+ let targetInstance = this.findInstanceWithCapacity();
979
+ if (!targetInstance) {
980
+ if (this.instances.size >= this.config.maxInstances) {
981
+ throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
337
982
  }
983
+ const newInstanceId = this.nextInstanceId++;
984
+ let onExit = (code, signal) => {
985
+ this.onUsbmuxdInstanceEnd({ instanceId: newInstanceId, code, signal });
986
+ };
987
+ onExit = onExit.bind(this);
988
+ const { instance, process: process2 } = await this.wsl.createInstance({
989
+ id: newInstanceId,
990
+ basePort: this.config.basePort,
991
+ verboseLogging: this.config.verboseLogging,
992
+ usbmuxdPath: this.config.usbmuxdPath,
993
+ onLog: (message) => this.emit("instance-log", {
994
+ instanceId: newInstanceId,
995
+ level: "info",
996
+ message
997
+ }),
998
+ onExit
999
+ });
1000
+ this.onUsbmuxdInstanceStart({ instance, process: process2 });
1001
+ targetInstance = instance;
338
1002
  }
1003
+ return targetInstance;
339
1004
  }
340
1005
  /**
341
- * Handle device connection
1006
+ * Handle device connection only for not attached devices
342
1007
  * Attaches device to WSL, then assigns to an existing instance or creates a new one
343
1008
  */
344
1009
  async onDeviceConnected(device) {
345
- if (this.deviceMappings.has(device.deviceId)) {
346
- logWarning2(`Device ${device.deviceId} is already connected`);
1010
+ const busId = await this.usbipd.findBusIdForDevice(device);
1011
+ if (this.attachedDevices.has(busId)) {
1012
+ logTask(`Device ${device.deviceId} is already attached. skipping onDeviceConnected...`);
347
1013
  return;
348
1014
  }
349
- await syncToAlpine(device.deviceId, this.config);
350
- const busId = await this.findBusIdForDevice(device);
351
- if (!busId) {
352
- logWarning2(`Cannot attach device ${device.deviceId} - bus ID not found`);
353
- }
354
- if (busId && !this.attachedDevices.has(busId)) {
355
- this.pendingAttachDevices.add(device.deviceId);
356
- try {
357
- const attached = await this.attachDeviceToWsl(busId);
358
- if (attached) {
359
- this.attachedDevices.add(busId);
360
- await new Promise((resolve) => setTimeout(resolve, 1e3));
361
- }
362
- } finally {
363
- this.pendingAttachDevices.delete(device.deviceId);
364
- }
365
- }
366
- let targetInstance = this.findInstanceWithCapacity();
367
- if (!targetInstance) {
368
- if (this.instances.size >= this.config.maxInstances) {
369
- throw new Error(`Maximum number of instances (${this.config.maxInstances}) reached`);
370
- }
371
- targetInstance = await this.createInstance();
372
- }
373
- targetInstance.deviceUdids.push(device.deviceId);
1015
+ logDataObject("Device connected", { device });
1016
+ await this.wsl.syncToAlpine(device.deviceId);
1017
+ await this.attachToWsl({ busId, deviceId: device.deviceId });
1018
+ const targetInstance = await this.getInstance();
374
1019
  const mapping = {
375
1020
  udid: device.deviceId,
376
1021
  instanceId: targetInstance.id,
@@ -380,77 +1025,52 @@ var InstanceManager = class extends import_node_events.EventEmitter {
380
1025
  busId: busId ?? void 0
381
1026
  };
382
1027
  this.deviceMappings.set(device.deviceId, mapping);
1028
+ targetInstance.deviceUdids.push(device.deviceId);
383
1029
  this.emit("device-assigned", {
384
1030
  device,
385
1031
  instance: targetInstance,
386
1032
  mapping
387
1033
  });
1034
+ this.pendingAttachDevices.delete(device.deviceId);
388
1035
  }
389
- /**
390
- * Pair a device with the usbmuxd host.
391
- * This is required once per device before most commands will work.
392
- * The pairing record is stored in WSL and persists across restarts.
393
- *
394
- * @param udid Device UDID to pair
395
- * @param goIosPath Optional path to go-ios binary (defaults to "ios")
396
- * @returns true if pairing succeeded, false otherwise
397
- */
398
- async pairDevice(udid, goIosPath = "ios") {
399
- const mapping = this.deviceMappings.get(udid);
1036
+ async detachFromWsl({ deviceId }) {
1037
+ const mapping = this.deviceMappings.get(deviceId);
400
1038
  if (!mapping) {
401
- logWarning2(`Cannot pair device ${udid} - not found in mappings`);
402
- return false;
1039
+ throw new Error(`Device ${deviceId} was not tracked`);
403
1040
  }
404
- try {
405
- const socketAddress = `${mapping.host}:${mapping.port}`;
406
- logInfo2(`Pairing device ${udid} via ${socketAddress}...`);
407
- const { stderr } = await execAsync2(`"${goIosPath}" pair --udid=${udid}`, {
408
- env: { ...process.env, USBMUXD_SOCKET_ADDRESS: socketAddress }
409
- });
410
- if (stderr?.includes("error")) {
411
- logWarning2(`Pairing warning for ${udid}: ${stderr}`);
412
- }
413
- logInfo2(`Device ${udid} paired successfully`);
414
- this.emit("device-paired", { udid, mapping });
415
- return true;
416
- } catch (error) {
417
- logWarning2(`Failed to pair device ${udid}: ${error}`);
418
- return false;
1041
+ if (mapping.busId) {
1042
+ await this.usbipd.detachDeviceFromWsl(mapping.busId);
1043
+ this.attachedDevices.delete(mapping.busId);
419
1044
  }
1045
+ const instance = this.instances.get(mapping.instanceId);
1046
+ if (!instance) {
1047
+ logWarning3(`Instance ${mapping.instanceId} not found`);
1048
+ this.deviceMappings.delete(deviceId);
1049
+ throw new Error(`Instance ${mapping.instanceId} not found`);
1050
+ }
1051
+ return instance;
420
1052
  }
421
1053
  /**
422
- * Handle device disconnection
1054
+ * Handle device disconnection only for attached devices
423
1055
  * Detaches from WSL, removes device from instance, and stops instance if empty
424
1056
  */
425
1057
  async onDeviceDisconnected(device) {
426
- if (this.pendingAttachDevices.has(device.deviceId)) {
1058
+ if (!this.attachedDevices.has(device.deviceId)) {
1059
+ logTask(`Device ${device.deviceId} is not attached. skipping onDeviceDisconnected...`);
427
1060
  return;
428
1061
  }
1062
+ logDataObject("Device disconnected", { device });
429
1063
  const mapping = this.deviceMappings.get(device.deviceId);
430
1064
  if (!mapping) {
431
- logWarning2(`Device ${device.deviceId} was not tracked`);
432
- return;
433
- }
434
- if (mapping.busId) {
435
- await this.detachDeviceFromWsl(mapping.busId);
436
- this.attachedDevices.delete(mapping.busId);
437
- }
438
- const instance = this.instances.get(mapping.instanceId);
439
- if (!instance) {
440
- logWarning2(`Instance ${mapping.instanceId} not found`);
441
- this.deviceMappings.delete(device.deviceId);
442
- return;
1065
+ throw new Error(`Device ${device.deviceId} was not tracked`);
443
1066
  }
444
- const deviceIndex = instance.deviceUdids.indexOf(device.deviceId);
445
- if (deviceIndex > -1) {
446
- instance.deviceUdids.splice(deviceIndex, 1);
447
- }
448
- this.deviceMappings.delete(device.deviceId);
1067
+ const instance = await this.removeDevice(mapping);
449
1068
  this.emit("device-removed", {
450
1069
  device,
451
1070
  instance
452
1071
  });
453
1072
  if (instance.deviceUdids.length === 0) {
1073
+ logDetail2(`Instance ${instance.id} is empty, stopping...`);
454
1074
  await this.stopInstance(instance.id);
455
1075
  }
456
1076
  }
@@ -465,104 +1085,18 @@ var InstanceManager = class extends import_node_events.EventEmitter {
465
1085
  }
466
1086
  return null;
467
1087
  }
468
- /**
469
- * Create a new usbmuxd instance
470
- */
471
- async createInstance() {
472
- const instanceId = this.nextInstanceId++;
473
- const port = this.config.basePort + instanceId - 1;
474
- const host = await this.detectWslIpAddress();
475
- const usbmuxdArgs = [
476
- "-f",
477
- // Foreground
478
- "-v",
479
- // Verbose (if enabled)
480
- "-S",
481
- `0.0.0.0:${port}`,
482
- // Listen on all interfaces (for Windows → WSL2)
483
- "--pidfile",
484
- "NONE"
485
- ];
486
- if (!this.config.verboseLogging) {
487
- usbmuxdArgs.splice(1, 1);
488
- }
489
- const wslArgs = [
490
- "-d",
491
- this.config.wslDistribution || "alpine-usbmuxd-build",
492
- this.config.usbmuxdPath,
493
- ...usbmuxdArgs
494
- ];
495
- const process2 = (0, import_node_child_process2.spawn)("wsl", wslArgs, {
496
- stdio: ["ignore", "pipe", "pipe"],
497
- windowsHide: false
498
- // Show console for debugging
499
- });
500
- process2.stdout?.on("data", (data) => {
501
- this.emit("instance-log", {
502
- instanceId,
503
- level: "info",
504
- message: data.toString().trim()
505
- });
506
- });
507
- process2.stderr?.on("data", (data) => {
508
- this.emit("instance-log", {
509
- instanceId,
510
- level: "error",
511
- message: data.toString().trim()
512
- });
513
- });
514
- process2.on("exit", (code, signal) => {
515
- this.emit("instance-exited", {
516
- instanceId,
517
- code,
518
- signal
519
- });
520
- if (this.instances.has(instanceId)) {
521
- this.instances.delete(instanceId);
522
- this.processes.delete(instanceId);
523
- }
524
- });
525
- const pid = process2.pid;
526
- if (pid === void 0) {
527
- process2.kill("SIGKILL");
528
- throw new Error("Failed to get PID for usbmuxd instance");
529
- }
530
- const instance = {
531
- id: instanceId,
532
- host,
533
- port,
534
- pid,
535
- deviceUdids: [],
536
- startedAt: /* @__PURE__ */ new Date()
537
- };
538
- this.instances.set(instanceId, instance);
539
- this.processes.set(instanceId, process2);
540
- this.emit("instance-started", instance);
541
- await new Promise((resolve) => setTimeout(resolve, 500));
542
- return instance;
543
- }
544
1088
  /**
545
1089
  * Stop a specific instance
546
1090
  */
547
1091
  async stopInstance(instanceId) {
548
1092
  const instance = this.instances.get(instanceId);
549
- const process2 = this.processes.get(instanceId);
550
- if (!instance || !process2) {
1093
+ const childProcess = this.processes.get(instanceId);
1094
+ if (!instance || !childProcess) {
551
1095
  return;
552
1096
  }
553
- process2.kill("SIGTERM");
554
- await new Promise((resolve) => {
555
- const timeout = setTimeout(() => {
556
- if (!process2.killed) {
557
- process2.kill("SIGKILL");
558
- }
559
- resolve();
560
- }, 5e3);
561
- process2.once("exit", () => {
562
- clearTimeout(timeout);
563
- resolve();
564
- });
565
- });
1097
+ for (const process2 of this.processes.values()) {
1098
+ process2.kill("SIGKILL");
1099
+ }
566
1100
  this.instances.delete(instanceId);
567
1101
  this.processes.delete(instanceId);
568
1102
  for (const [udid, mapping] of this.deviceMappings.entries()) {
@@ -614,7 +1148,7 @@ var InstanceManager = class extends import_node_events.EventEmitter {
614
1148
  };
615
1149
 
616
1150
  // src/UsbmuxdService.ts
617
- var { logInfo: logInfo3, logError } = (0, import_tool_debug_g43.createLoggers)("usbmuxd-instance-manager");
1151
+ var { logInfo: logInfo3, logError, logDataObject: logDataObject2 } = (0, import_tool_debug_g44.createLoggers)("usbmuxd-service");
618
1152
  var UsbmuxdService = class extends import_node_events2.EventEmitter {
619
1153
  manager;
620
1154
  usbListener;
@@ -638,6 +1172,9 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
638
1172
  this.manager.on(
639
1173
  "device-assigned",
640
1174
  (payload) => {
1175
+ logInfo3(
1176
+ `Device ${payload.device.deviceId} connected to usbmuxd instance at ${payload.mapping.host}:${payload.mapping.port}`
1177
+ );
641
1178
  this.emit("device-assigned", payload);
642
1179
  }
643
1180
  );
@@ -673,12 +1210,7 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
673
1210
  }
674
1211
  const config2 = this.manager.getConfig();
675
1212
  const appleVid = Number.parseInt(config2.appleVendorId, 16);
676
- logInfo3("Starting service...");
677
- logInfo3(`Batch size: ${config2.batchSize} devices per instance`);
678
- logInfo3(`Base port: ${config2.basePort}`);
679
- logInfo3(`Max instances: ${config2.maxInstances}`);
680
- logInfo3(`usbmuxd path: ${config2.usbmuxdPath}`);
681
- logInfo3(`Monitoring Apple devices (VID: ${config2.appleVendorId})`);
1213
+ logDataObject2("Starting service...", { config: config2 });
682
1214
  this.usbListener.onDeviceAdd(async (device) => {
683
1215
  if (device.vid !== appleVid) {
684
1216
  return;
@@ -759,22 +1291,11 @@ var UsbmuxdService = class extends import_node_events2.EventEmitter {
759
1291
  getConfig() {
760
1292
  return this.manager.getConfig();
761
1293
  }
762
- /**
763
- * Pair a device with the usbmuxd host.
764
- * This is required once per device before most commands will work.
765
- * The pairing record is stored in WSL and persists across restarts.
766
- *
767
- * @param udid Device UDID to pair
768
- * @param goIosPath Optional path to go-ios binary
769
- * @returns true if pairing succeeded, false otherwise
770
- */
771
- async pairDevice(udid, goIosPath) {
772
- return this.manager.pairDevice(udid, goIosPath);
773
- }
774
1294
  };
775
1295
 
776
1296
  // src/cli.ts
777
- var { logInfo: logInfo4, logError: logError2, logHeader, logDataObject } = (0, import_tool_debug_g44.createLoggers)("usbmuxd-cli");
1297
+ import_dotenv.default.config();
1298
+ var { logInfo: logInfo4, logError: logError2, logHeader, logDataObject: logDataObject3 } = (0, import_tool_debug_g45.createLoggers)("usbmuxd-cli");
778
1299
  var args = process.argv.slice(2);
779
1300
  var options = {};
780
1301
  for (let i = 0; i < args.length; i++) {
@@ -796,31 +1317,25 @@ var config = {
796
1317
  appleVendorId: options.appleVid || "05AC"
797
1318
  };
798
1319
  logHeader("usbmuxd Instance Manager");
799
- logDataObject("Multi-instance manager for iOS device connections", { config });
1320
+ logDataObject3("Multi-instance manager for iOS device connections", { config });
800
1321
  var service = new UsbmuxdService(config);
801
- process.on("SIGINT", async () => {
1322
+ async function shutdown(eventName) {
802
1323
  logInfo4("");
803
- logInfo4("Received SIGINT, shutting down gracefully...");
1324
+ logInfo4(`Received ${eventName}, shutting down gracefully...`);
804
1325
  try {
805
1326
  await service.stop();
806
1327
  logInfo4("Shutdown complete");
807
1328
  process.exit(0);
808
1329
  } catch (error) {
809
- logError2("Error during shutdown:", error);
1330
+ logError2(`Error during ${eventName}:`, error);
810
1331
  process.exit(1);
811
1332
  }
1333
+ }
1334
+ process.on("SIGINT", async () => {
1335
+ void shutdown("SIGINT");
812
1336
  });
813
1337
  process.on("SIGTERM", async () => {
814
- logInfo4("");
815
- logInfo4("Received SIGTERM, shutting down gracefully...");
816
- try {
817
- await service.stop();
818
- logInfo4("Shutdown complete");
819
- process.exit(0);
820
- } catch (error) {
821
- logError2("Error during shutdown:", error);
822
- process.exit(1);
823
- }
1338
+ void shutdown("SIGTERM");
824
1339
  });
825
1340
  setInterval(() => {
826
1341
  const stats = service.getStats();
@@ -829,7 +1344,7 @@ setInterval(() => {
829
1344
  `Uptime: ${stats.uptimeSeconds}s | Instances: ${stats.instanceCount} | Devices: ${stats.deviceCount}`
830
1345
  );
831
1346
  if (instances.length > 0) {
832
- logDataObject("Active instances:", { instances });
1347
+ logDataObject3("Active instances:", { instances });
833
1348
  }
834
1349
  }, 3e4);
835
1350
  if (args.includes("--help") || args.includes("-h")) {
@@ -853,7 +1368,7 @@ if (args.includes("--help") || args.includes("-h")) {
853
1368
  try {
854
1369
  service.start();
855
1370
  } catch (error) {
856
- logError2("Failed to start service:", error);
857
- process.exit(1);
1371
+ logError2("Failed to start service", error);
1372
+ shutdown("Failed to start service");
858
1373
  }
859
1374
  //# sourceMappingURL=cli.js.map