@ebowwa/ios-devices 1.0.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.
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Unified API - Single interface for iOS device control
3
+ * Automatically selects best available tool (devicectl > libimobiledevice)
4
+ */
5
+
6
+ import { DeviceCtl } from './device-ctl.js';
7
+ import { LibIMobileDevice } from './lib-imobiledevice.js';
8
+ import { SimCtl } from './sim-ctl.js';
9
+ import { checkAvailableTools, type ExecOptions } from './utils.js';
10
+ import {
11
+ type IOSDevice,
12
+ type DeviceDetails,
13
+ type ProcessInfo,
14
+ type InstalledApp,
15
+ type FileInfo,
16
+ type LogEntry,
17
+ type LogFilter,
18
+ type CommandResult,
19
+ type SimulatorDevice,
20
+ type Location,
21
+ } from './types.js';
22
+
23
+ export interface IOSDevicesOptions {
24
+ timeout?: number;
25
+ preferDeviceCtl?: boolean; // Default true for iOS 17+
26
+ }
27
+
28
+ export class IOSDevices {
29
+ private deviceCtl: DeviceCtl | null = null;
30
+ private libIMobile: LibIMobileDevice | null = null;
31
+ private simCtl: SimCtl;
32
+ private availableTools: {
33
+ devicectl: boolean;
34
+ simctl: boolean;
35
+ libimobiledevice: boolean;
36
+ } | null = null;
37
+
38
+ constructor(private options: IOSDevicesOptions = {}) {
39
+ this.simCtl = new SimCtl(options);
40
+ }
41
+
42
+ /**
43
+ * Initialize and check available tools
44
+ */
45
+ async initialize(): Promise<void> {
46
+ this.availableTools = await checkAvailableTools();
47
+
48
+ if (this.availableTools.devicectl && this.options.preferDeviceCtl !== false) {
49
+ this.deviceCtl = new DeviceCtl(this.options);
50
+ }
51
+
52
+ if (this.availableTools.libimobiledevice) {
53
+ this.libIMobile = new LibIMobileDevice(this.options);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get available tools status
59
+ */
60
+ getToolsStatus(): { devicectl: boolean; simctl: boolean; libimobiledevice: boolean } | null {
61
+ return this.availableTools;
62
+ }
63
+
64
+ // ==========================================================================
65
+ // Device Management (Physical Devices)
66
+ // ==========================================================================
67
+
68
+ /**
69
+ * List all connected devices
70
+ */
71
+ async listDevices(): Promise<IOSDevice[]> {
72
+ if (!this.availableTools) await this.initialize();
73
+
74
+ if (this.deviceCtl) {
75
+ // DeviceCtl.listDevices() returns IOSDevice[] directly
76
+ return await this.deviceCtl.listDevices();
77
+ }
78
+
79
+ if (this.libIMobile) {
80
+ const result = await this.libIMobile.listDevices();
81
+ if (result.success) {
82
+ return this.parseDeviceListLegacy(result.stdout);
83
+ }
84
+ }
85
+
86
+ return [];
87
+ }
88
+
89
+ /**
90
+ * Get device details
91
+ */
92
+ async getDeviceDetails(deviceId: string): Promise<DeviceDetails | null> {
93
+ if (!this.availableTools) await this.initialize();
94
+
95
+ if (this.deviceCtl) {
96
+ const details = await this.deviceCtl.getDeviceInfo(deviceId);
97
+ if (details) {
98
+ return details;
99
+ }
100
+ }
101
+
102
+ if (this.libIMobile) {
103
+ const libIMobile = new LibIMobileDevice({ udid: deviceId, ...this.options });
104
+ const result = await libIMobile.getDeviceInfo();
105
+ if (result.success) {
106
+ return this.parseDeviceDetailsLegacy(result.stdout);
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Reboot device
115
+ */
116
+ async rebootDevice(deviceId: string): Promise<CommandResult> {
117
+ if (!this.availableTools) await this.initialize();
118
+
119
+ if (this.deviceCtl) {
120
+ return this.deviceCtl.reboot(deviceId);
121
+ }
122
+
123
+ if (this.libIMobile) {
124
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
125
+ return libIMobile.restart();
126
+ }
127
+
128
+ return { success: false, stdout: '', stderr: 'No device control tools available', exitCode: 1 };
129
+ }
130
+
131
+ // ==========================================================================
132
+ // Process Management
133
+ // ==========================================================================
134
+
135
+ /**
136
+ * List processes on device
137
+ */
138
+ async listProcesses(deviceId: string): Promise<ProcessInfo[]> {
139
+ if (!this.availableTools) await this.initialize();
140
+
141
+ if (this.deviceCtl) {
142
+ return this.deviceCtl.listProcesses(deviceId);
143
+ }
144
+
145
+ return [];
146
+ }
147
+
148
+ /**
149
+ * Launch app
150
+ */
151
+ async launchApp(deviceId: string, bundleId: string): Promise<CommandResult> {
152
+ if (!this.availableTools) await this.initialize();
153
+
154
+ if (this.deviceCtl) {
155
+ return this.deviceCtl.launchApp(deviceId, bundleId);
156
+ }
157
+
158
+ return { success: false, stdout: '', stderr: 'devicectl required for launch', exitCode: 1 };
159
+ }
160
+
161
+ /**
162
+ * Terminate app/process
163
+ */
164
+ async terminateProcess(deviceId: string, pidOrName: number | string): Promise<CommandResult> {
165
+ if (!this.availableTools) await this.initialize();
166
+
167
+ if (this.deviceCtl) {
168
+ if (typeof pidOrName === 'number') {
169
+ return this.deviceCtl.terminateProcess(deviceId, pidOrName);
170
+ } else {
171
+ // Find PID first
172
+ const processes = await this.listProcesses(deviceId);
173
+ const proc = processes.find((p) => p.name === pidOrName || p.bundleIdentifier === pidOrName);
174
+ if (proc) {
175
+ return this.deviceCtl.terminateProcess(deviceId, proc.pid);
176
+ }
177
+ return { success: false, stdout: '', stderr: 'Process not found', exitCode: 1 };
178
+ }
179
+ }
180
+
181
+ return { success: false, stdout: '', stderr: 'devicectl required', exitCode: 1 };
182
+ }
183
+
184
+ // ==========================================================================
185
+ // App Management
186
+ // ==========================================================================
187
+
188
+ /**
189
+ * List installed apps
190
+ */
191
+ async listApps(deviceId: string): Promise<InstalledApp[]> {
192
+ if (!this.availableTools) await this.initialize();
193
+
194
+ if (this.deviceCtl) {
195
+ return this.deviceCtl.listApps(deviceId);
196
+ }
197
+
198
+ return [];
199
+ }
200
+
201
+ /**
202
+ * Install app
203
+ */
204
+ async installApp(deviceId: string, appPath: string): Promise<CommandResult> {
205
+ if (!this.availableTools) await this.initialize();
206
+
207
+ if (this.deviceCtl) {
208
+ return this.deviceCtl.installApp(deviceId, appPath);
209
+ }
210
+
211
+ return { success: false, stdout: '', stderr: 'devicectl required', exitCode: 1 };
212
+ }
213
+
214
+ /**
215
+ * Uninstall app
216
+ */
217
+ async uninstallApp(deviceId: string, bundleId: string): Promise<CommandResult> {
218
+ if (!this.availableTools) await this.initialize();
219
+
220
+ if (this.deviceCtl) {
221
+ return this.deviceCtl.uninstallApp(deviceId, bundleId);
222
+ }
223
+
224
+ return { success: false, stdout: '', stderr: 'devicectl required', exitCode: 1 };
225
+ }
226
+
227
+ // ==========================================================================
228
+ // Logging
229
+ // ==========================================================================
230
+
231
+ /**
232
+ * Stream syslog
233
+ */
234
+ async streamSyslog(deviceId: string, filter?: LogFilter): Promise<CommandResult> {
235
+ if (!this.availableTools) await this.initialize();
236
+
237
+ if (this.libIMobile) {
238
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
239
+ return libIMobile.streamSyslog(filter);
240
+ }
241
+
242
+ return { success: false, stdout: '', stderr: 'libimobiledevice required for syslog', exitCode: 1 };
243
+ }
244
+
245
+ /**
246
+ * Get syslog archive
247
+ */
248
+ async getSyslogArchive(deviceId: string, outputPath: string): Promise<CommandResult> {
249
+ if (!this.availableTools) await this.initialize();
250
+
251
+ if (this.libIMobile) {
252
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
253
+ return libIMobile.getSyslogArchive(outputPath);
254
+ }
255
+
256
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
257
+ }
258
+
259
+ // ==========================================================================
260
+ // Screenshot
261
+ // ==========================================================================
262
+
263
+ /**
264
+ * Take screenshot
265
+ */
266
+ async takeScreenshot(deviceId: string, outputPath?: string): Promise<CommandResult> {
267
+ if (!this.availableTools) await this.initialize();
268
+
269
+ if (this.libIMobile) {
270
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
271
+ return libIMobile.takeScreenshot(outputPath);
272
+ }
273
+
274
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
275
+ }
276
+
277
+ // ==========================================================================
278
+ // Location
279
+ // ==========================================================================
280
+
281
+ /**
282
+ * Set simulated location
283
+ */
284
+ async setLocation(deviceId: string, latitude: number, longitude: number): Promise<CommandResult> {
285
+ if (!this.availableTools) await this.initialize();
286
+
287
+ if (this.libIMobile) {
288
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
289
+ return libIMobile.setLocation(latitude, longitude);
290
+ }
291
+
292
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
293
+ }
294
+
295
+ /**
296
+ * Reset location
297
+ */
298
+ async resetLocation(deviceId: string): Promise<CommandResult> {
299
+ if (!this.availableTools) await this.initialize();
300
+
301
+ if (this.libIMobile) {
302
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
303
+ return libIMobile.resetLocation();
304
+ }
305
+
306
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
307
+ }
308
+
309
+ // ==========================================================================
310
+ // Backup
311
+ // ==========================================================================
312
+
313
+ /**
314
+ * Create backup
315
+ */
316
+ async createBackup(deviceId: string, directory: string, encrypt?: boolean, password?: string): Promise<CommandResult> {
317
+ if (!this.availableTools) await this.initialize();
318
+
319
+ if (this.libIMobile) {
320
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
321
+ return libIMobile.createBackup(directory, encrypt, password);
322
+ }
323
+
324
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
325
+ }
326
+
327
+ /**
328
+ * Restore backup
329
+ */
330
+ async restoreBackup(deviceId: string, directory: string, password?: string): Promise<CommandResult> {
331
+ if (!this.availableTools) await this.initialize();
332
+
333
+ if (this.libIMobile) {
334
+ const libIMobile = new LibIMobileDevice({ udid: deviceId });
335
+ return libIMobile.restoreBackup(directory, password);
336
+ }
337
+
338
+ return { success: false, stdout: '', stderr: 'libimobiledevice required', exitCode: 1 };
339
+ }
340
+
341
+ // ==========================================================================
342
+ // Simulator (direct access)
343
+ // ==========================================================================
344
+
345
+ /**
346
+ * Get SimCtl instance for simulator control
347
+ */
348
+ getSimulator(): SimCtl {
349
+ return this.simCtl;
350
+ }
351
+
352
+ /**
353
+ * List simulators
354
+ */
355
+ async listSimulators(): Promise<SimulatorDevice[]> {
356
+ const result = await this.simCtl.list({ devices: true, available: true });
357
+ if (result.success) {
358
+ return this.parseSimulatorList(result.stdout);
359
+ }
360
+ return [];
361
+ }
362
+
363
+ /**
364
+ * Boot simulator
365
+ */
366
+ async bootSimulator(udid: string): Promise<CommandResult> {
367
+ return this.simCtl.boot(udid);
368
+ }
369
+
370
+ /**
371
+ * Shutdown simulator
372
+ */
373
+ async shutdownSimulator(udid: string): Promise<CommandResult> {
374
+ return this.simCtl.shutdown(udid);
375
+ }
376
+
377
+ // ==========================================================================
378
+ // Parsing Helpers
379
+ // ==========================================================================
380
+
381
+ private parseDeviceList(json: unknown): IOSDevice[] {
382
+ // Parse devicectl JSON output
383
+ const devices: IOSDevice[] = [];
384
+ try {
385
+ const data = json as { devices?: Array<{
386
+ identifier?: string;
387
+ name?: string;
388
+ modelCode?: string;
389
+ productType?: string;
390
+ osVersion?: string;
391
+ osBuildVersion?: string;
392
+ architecture?: string;
393
+ platform?: string;
394
+ connectionProperties?: { connectionType?: string };
395
+ }> };
396
+
397
+ if (data?.devices) {
398
+ for (const d of data.devices) {
399
+ devices.push({
400
+ identifier: d.identifier || '',
401
+ name: d.name || '',
402
+ model: d.modelCode || '',
403
+ modelCode: d.modelCode || '',
404
+ productType: d.productType || '',
405
+ osVersion: d.osVersion || '',
406
+ osBuildVersion: d.osBuildVersion || '',
407
+ architecture: d.architecture || '',
408
+ platform: (d.platform as IOSDevice['platform']) || 'iOS',
409
+ connectionType: d.connectionProperties?.connectionType === 'Wired' ? 'USB' : 'Network',
410
+ isTrusted: true,
411
+ isAvailable: true,
412
+ });
413
+ }
414
+ }
415
+ } catch {
416
+ // Parse error
417
+ }
418
+ return devices;
419
+ }
420
+
421
+ private parseDeviceListLegacy(text: string): IOSDevice[] {
422
+ // Parse idevice_id -l output
423
+ const devices: IOSDevice[] = [];
424
+ const lines = text.split('\n');
425
+ for (const line of lines) {
426
+ const match = line.match(/^([A-F0-9-]+)\s+(.+)$/);
427
+ if (match) {
428
+ devices.push({
429
+ identifier: match[1],
430
+ name: match[2].trim(),
431
+ model: '',
432
+ modelCode: '',
433
+ productType: '',
434
+ osVersion: '',
435
+ osBuildVersion: '',
436
+ architecture: '',
437
+ platform: 'iOS',
438
+ connectionType: 'USB',
439
+ isTrusted: true,
440
+ isAvailable: true,
441
+ });
442
+ }
443
+ }
444
+ return devices;
445
+ }
446
+
447
+ private parseDeviceDetails(json: unknown): DeviceDetails | null {
448
+ try {
449
+ const d = json as {
450
+ identifier?: string;
451
+ name?: string;
452
+ modelCode?: string;
453
+ productType?: string;
454
+ productVersion?: string;
455
+ buildVersion?: string;
456
+ serialNumber?: string;
457
+ architecture?: string;
458
+ };
459
+
460
+ return {
461
+ udid: d.identifier || '',
462
+ name: d.name || '',
463
+ model: d.modelCode || '',
464
+ productType: d.productType || '',
465
+ productVersion: d.productVersion || '',
466
+ buildVersion: d.buildVersion || '',
467
+ serialNumber: d.serialNumber,
468
+ cpuArchitecture: d.architecture || '',
469
+ deviceName: d.name || '',
470
+ };
471
+ } catch {
472
+ return null;
473
+ }
474
+ }
475
+
476
+ private parseDeviceDetailsLegacy(text: string): DeviceDetails | null {
477
+ // Parse ideviceinfo output
478
+ const details: Partial<DeviceDetails> = {};
479
+ const lines = text.split('\n');
480
+ for (const line of lines) {
481
+ const match = line.match(/^(.+?):\s*(.+)$/);
482
+ if (match) {
483
+ const key = match[1].trim().toLowerCase().replace(/\s+/g, '_');
484
+ const value = match[2].trim();
485
+ if (key === 'unique_device_id') details.udid = value;
486
+ if (key === 'device_name') details.name = value;
487
+ if (key === 'model_name') details.model = value;
488
+ if (key === 'product_type') details.productType = value;
489
+ if (key === 'product_version') details.productVersion = value;
490
+ if (key === 'build_version') details.buildVersion = value;
491
+ if (key === 'serial_number') details.serialNumber = value;
492
+ if (key === 'cpu_architecture') details.cpuArchitecture = value;
493
+ }
494
+ }
495
+ return details as DeviceDetails;
496
+ }
497
+
498
+ private parseProcessList(json: unknown): ProcessInfo[] {
499
+ const processes: ProcessInfo[] = [];
500
+ try {
501
+ const data = json as { processes?: Array<{
502
+ pid?: number;
503
+ name?: string;
504
+ user?: string;
505
+ bundleIdentifier?: string;
506
+ }> };
507
+
508
+ if (data?.processes) {
509
+ for (const p of data.processes) {
510
+ processes.push({
511
+ pid: p.pid || 0,
512
+ name: p.name || '',
513
+ user: p.user,
514
+ bundleIdentifier: p.bundleIdentifier,
515
+ });
516
+ }
517
+ }
518
+ } catch {
519
+ // Parse error
520
+ }
521
+ return processes;
522
+ }
523
+
524
+ private parseAppList(json: unknown): InstalledApp[] {
525
+ const apps: InstalledApp[] = [];
526
+ try {
527
+ const data = json as { apps?: Array<{
528
+ bundleIdentifier?: string;
529
+ bundleName?: string;
530
+ bundleVersion?: string;
531
+ installPath?: string;
532
+ platform?: string;
533
+ type?: string;
534
+ }> };
535
+
536
+ if (data?.apps) {
537
+ for (const a of data.apps) {
538
+ apps.push({
539
+ bundleIdentifier: a.bundleIdentifier || '',
540
+ bundleName: a.bundleName || '',
541
+ bundleVersion: a.bundleVersion || '',
542
+ installPath: a.installPath || '',
543
+ platform: (a.platform as InstalledApp['platform']) || 'iOS',
544
+ type: (a.type as InstalledApp['type']) || 'User',
545
+ });
546
+ }
547
+ }
548
+ } catch {
549
+ // Parse error
550
+ }
551
+ return apps;
552
+ }
553
+
554
+ private parseSimulatorList(text: string): SimulatorDevice[] {
555
+ const simulators: SimulatorDevice[] = [];
556
+ const lines = text.split('\n');
557
+ for (const line of lines) {
558
+ const match = line.match(/([A-F0-9-]+)\s+\((.+?)\)\s+\((.+?)\)/);
559
+ if (match) {
560
+ simulators.push({
561
+ udid: match[1],
562
+ name: match[2],
563
+ state: line.includes('Booted') ? 'Booted' : 'Shutdown',
564
+ runtime: match[3],
565
+ deviceType: '',
566
+ availability: 'available',
567
+ });
568
+ }
569
+ }
570
+ return simulators;
571
+ }
572
+ }
573
+
574
+ // Re-export classes for direct access
575
+ export { DeviceCtl } from './device-ctl.js';
576
+ export { LibIMobileDevice } from './lib-imobiledevice.js';
577
+ export { SimCtl } from './sim-ctl.js';
578
+ export { checkAvailableTools } from './utils.js';