@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.
- package/dist/device-ctl.d.ts +128 -0
- package/dist/device-ctl.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5365 -0
- package/dist/index.js.map +22 -0
- package/dist/lib-imobiledevice.d.ts +162 -0
- package/dist/lib-imobiledevice.d.ts.map +1 -0
- package/dist/sim-ctl.d.ts +216 -0
- package/dist/sim-ctl.d.ts.map +1 -0
- package/dist/types.d.ts +487 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/unified-api.d.ts +122 -0
- package/dist/unified-api.d.ts.map +1 -0
- package/dist/utils.d.ts +47 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +49 -0
- package/src/device-ctl.ts +547 -0
- package/src/index.ts +8 -0
- package/src/lib-imobiledevice.ts +404 -0
- package/src/shell-quote.d.ts +5 -0
- package/src/sim-ctl.ts +502 -0
- package/src/types.ts +315 -0
- package/src/unified-api.ts +578 -0
- package/src/utils.ts +174 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/ios-devices",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "iOS device control library wrapping xcrun devicectl, libimobiledevice, and simctl",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --sourcemap --external shell-quote",
|
|
20
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"prepublishOnly": "bun run build && bun run build:types"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ios",
|
|
27
|
+
"iphone",
|
|
28
|
+
"ipad",
|
|
29
|
+
"device",
|
|
30
|
+
"devicectl",
|
|
31
|
+
"simctl",
|
|
32
|
+
"libimobiledevice",
|
|
33
|
+
"xcode"
|
|
34
|
+
],
|
|
35
|
+
"author": "ebowwa",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/ebowwa/codespaces"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"typescript": "^5.7.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"shell-quote": "^1.8.0",
|
|
47
|
+
"zod": "^3.24.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceCtl - Wrapper for xcrun devicectl (iOS 17+)
|
|
3
|
+
* Primary interface for modern iOS device control
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
execDeviceCtl,
|
|
8
|
+
exec,
|
|
9
|
+
buildDeviceArg,
|
|
10
|
+
type ExecOptions,
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
import {
|
|
13
|
+
type IOSDevice,
|
|
14
|
+
type DeviceDetails,
|
|
15
|
+
type ProcessInfo,
|
|
16
|
+
type InstalledApp,
|
|
17
|
+
type FileInfo,
|
|
18
|
+
type DisplayInfo,
|
|
19
|
+
type CommandResult,
|
|
20
|
+
type ProcessSignal,
|
|
21
|
+
type DarwinNotification,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
export interface DeviceCtlOptions {
|
|
25
|
+
timeout?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DeviceCtl {
|
|
29
|
+
private options: DeviceCtlOptions;
|
|
30
|
+
|
|
31
|
+
constructor(options: DeviceCtlOptions = {}) {
|
|
32
|
+
this.options = { timeout: 60000, ...options };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private execOpts(): ExecOptions {
|
|
36
|
+
return { timeout: this.options.timeout };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ==========================================================================
|
|
40
|
+
// Device Listing & Management
|
|
41
|
+
// ==========================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* List all connected devices
|
|
45
|
+
*/
|
|
46
|
+
async listDevices(): Promise<IOSDevice[]> {
|
|
47
|
+
const result = await execDeviceCtl(['list', 'devices'], this.execOpts());
|
|
48
|
+
|
|
49
|
+
if (!result.success || !result.json) {
|
|
50
|
+
return this.parseDevicesFromText(result.stdout);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = result.json as { devices?: Array<Record<string, unknown>> };
|
|
54
|
+
return (data.devices || []).map(this.parseDeviceFromJson);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private parseDeviceFromJson(device: Record<string, unknown>): IOSDevice {
|
|
58
|
+
return {
|
|
59
|
+
identifier: device.udid as string || device.identifier as string,
|
|
60
|
+
name: device.name as string || device.deviceName as string || 'Unknown',
|
|
61
|
+
model: device.model as string || '',
|
|
62
|
+
modelCode: device.modelCode as string || '',
|
|
63
|
+
productType: device.productType as string || '',
|
|
64
|
+
osVersion: device.osVersion as string || device.productVersion as string || '',
|
|
65
|
+
osBuildVersion: device.buildVersion as string || '',
|
|
66
|
+
architecture: device.architecture as string || 'arm64e',
|
|
67
|
+
platform: this.detectPlatform(device.productType as string),
|
|
68
|
+
connectionType: this.detectConnectionType(device),
|
|
69
|
+
isTrusted: device.trusted as boolean ?? true,
|
|
70
|
+
isAvailable: device.available as boolean ?? true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private parseDevicesFromText(text: string): IOSDevice[] {
|
|
75
|
+
const lines = text.split('\n');
|
|
76
|
+
const devices: IOSDevice[] = [];
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
// Parse format: "Device Name (iOS 18.1) [UDID]"
|
|
80
|
+
const match = line.match(/^(.+?)\s*\((.+?)\)\s*\[([A-F0-9-]+)\]/i);
|
|
81
|
+
if (match) {
|
|
82
|
+
devices.push({
|
|
83
|
+
identifier: match[3],
|
|
84
|
+
name: match[1].trim(),
|
|
85
|
+
model: '',
|
|
86
|
+
modelCode: '',
|
|
87
|
+
productType: '',
|
|
88
|
+
osVersion: match[2],
|
|
89
|
+
osBuildVersion: '',
|
|
90
|
+
architecture: 'arm64e',
|
|
91
|
+
platform: 'iOS',
|
|
92
|
+
connectionType: 'USB',
|
|
93
|
+
isTrusted: true,
|
|
94
|
+
isAvailable: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return devices;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private detectPlatform(productType?: string): 'iOS' | 'iPadOS' | 'tvOS' | 'watchOS' | 'visionOS' {
|
|
103
|
+
if (!productType) return 'iOS';
|
|
104
|
+
if (productType.includes('iPad')) return 'iPadOS';
|
|
105
|
+
if (productType.includes('AppleTV') || productType.includes('TV')) return 'tvOS';
|
|
106
|
+
if (productType.includes('Watch')) return 'watchOS';
|
|
107
|
+
if (productType.includes('Vision') || productType.includes('Reality')) return 'visionOS';
|
|
108
|
+
return 'iOS';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private detectConnectionType(device: Record<string, unknown>): 'USB' | 'Network' | 'Wireless' {
|
|
112
|
+
if (device.connectionType) return device.connectionType as 'USB' | 'Network' | 'Wireless';
|
|
113
|
+
if (device.networkDevice === true) return 'Network';
|
|
114
|
+
if (device.wirelessEnabled === true) return 'Wireless';
|
|
115
|
+
return 'USB';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get detailed device information
|
|
120
|
+
*/
|
|
121
|
+
async getDeviceInfo(deviceId: string): Promise<DeviceDetails | null> {
|
|
122
|
+
const result = await execDeviceCtl(
|
|
123
|
+
['device', 'info', 'details', ...buildDeviceArg(deviceId)],
|
|
124
|
+
this.execOpts()
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!result.success) return null;
|
|
128
|
+
|
|
129
|
+
if (result.json) {
|
|
130
|
+
const d = result.json as Record<string, unknown>;
|
|
131
|
+
return {
|
|
132
|
+
udid: d.udid as string || deviceId,
|
|
133
|
+
name: d.name as string || d.deviceName as string || '',
|
|
134
|
+
model: d.model as string || '',
|
|
135
|
+
productType: d.productType as string || '',
|
|
136
|
+
productVersion: d.productVersion as string || d.osVersion as string || '',
|
|
137
|
+
buildVersion: d.buildVersion as string || '',
|
|
138
|
+
serialNumber: d.serialNumber as string,
|
|
139
|
+
cpuArchitecture: d.architecture as string || 'arm64e',
|
|
140
|
+
deviceName: d.deviceName as string || d.name as string || '',
|
|
141
|
+
timeZone: d.timeZone as string,
|
|
142
|
+
locale: d.locale as string,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return this.parseDeviceDetailsFromText(result.stdout);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private parseDeviceDetailsFromText(text: string): DeviceDetails | null {
|
|
150
|
+
const lines = text.split('\n');
|
|
151
|
+
const info: Record<string, string> = {};
|
|
152
|
+
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const match = line.match(/^([^:]+):\s*(.+)$/);
|
|
155
|
+
if (match) {
|
|
156
|
+
info[match[1].trim().toLowerCase().replace(/\s+/g, '_')] = match[2].trim();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
udid: info.udid || info.identifier || '',
|
|
162
|
+
name: info.name || info.device_name || '',
|
|
163
|
+
model: info.model || '',
|
|
164
|
+
productType: info.product_type || '',
|
|
165
|
+
productVersion: info.product_version || info.os_version || '',
|
|
166
|
+
buildVersion: info.build_version || '',
|
|
167
|
+
serialNumber: info.serial_number,
|
|
168
|
+
cpuArchitecture: info.architecture || info.cpu_architecture || 'arm64e',
|
|
169
|
+
deviceName: info.device_name || info.name || '',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get device lock state
|
|
175
|
+
*/
|
|
176
|
+
async getLockState(deviceId: string): Promise<'locked' | 'unlocked'> {
|
|
177
|
+
const result = await execDeviceCtl(
|
|
178
|
+
['device', 'info', 'lockState', ...buildDeviceArg(deviceId)],
|
|
179
|
+
this.execOpts()
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (result.stdout.toLowerCase().includes('unlocked')) return 'unlocked';
|
|
183
|
+
return 'locked';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get display information
|
|
188
|
+
*/
|
|
189
|
+
async getDisplays(deviceId: string): Promise<DisplayInfo[]> {
|
|
190
|
+
const result = await execDeviceCtl(
|
|
191
|
+
['device', 'info', 'displays', ...buildDeviceArg(deviceId)],
|
|
192
|
+
this.execOpts()
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!result.success || !result.json) return [];
|
|
196
|
+
|
|
197
|
+
const displays = (result.json as { displays?: unknown[] }).displays || [];
|
|
198
|
+
return displays.map((d: unknown) => {
|
|
199
|
+
const display = d as Record<string, unknown>;
|
|
200
|
+
return {
|
|
201
|
+
id: display.id as number || 0,
|
|
202
|
+
width: display.width as number || display.resolutionWidth as number || 0,
|
|
203
|
+
height: display.height as number || display.resolutionHeight as number || 0,
|
|
204
|
+
scale: display.scale as number || 1,
|
|
205
|
+
refreshRate: display.refreshRate as number,
|
|
206
|
+
colorSpace: display.colorSpace as string,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
// Pairing
|
|
213
|
+
// ==========================================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Pair with a device
|
|
217
|
+
*/
|
|
218
|
+
async pair(deviceId: string): Promise<CommandResult> {
|
|
219
|
+
return execDeviceCtl(['manage', 'pair', ...buildDeviceArg(deviceId)], this.execOpts());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Unpair from a device
|
|
224
|
+
*/
|
|
225
|
+
async unpair(deviceId: string): Promise<CommandResult> {
|
|
226
|
+
return execDeviceCtl(['manage', 'unpair', ...buildDeviceArg(deviceId)], this.execOpts());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ==========================================================================
|
|
230
|
+
// Device Control
|
|
231
|
+
// ==========================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reboot a device
|
|
235
|
+
*/
|
|
236
|
+
async reboot(deviceId: string): Promise<CommandResult> {
|
|
237
|
+
return execDeviceCtl(['device', 'reboot', ...buildDeviceArg(deviceId)], this.execOpts());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Set device orientation
|
|
242
|
+
*/
|
|
243
|
+
async setOrientation(deviceId: string, orientation: 'portrait' | 'landscape' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right'): Promise<CommandResult> {
|
|
244
|
+
return execDeviceCtl(
|
|
245
|
+
['device', 'orientation', '--orientation', orientation, ...buildDeviceArg(deviceId)],
|
|
246
|
+
this.execOpts()
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
// Process Management
|
|
252
|
+
// ==========================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* List running processes
|
|
256
|
+
*/
|
|
257
|
+
async listProcesses(deviceId: string): Promise<ProcessInfo[]> {
|
|
258
|
+
const result = await execDeviceCtl(
|
|
259
|
+
['device', 'info', 'processes', ...buildDeviceArg(deviceId)],
|
|
260
|
+
this.execOpts()
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!result.success) return [];
|
|
264
|
+
|
|
265
|
+
return this.parseProcessesFromText(result.stdout);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private parseProcessesFromText(text: string): ProcessInfo[] {
|
|
269
|
+
const lines = text.split('\n');
|
|
270
|
+
const processes: ProcessInfo[] = [];
|
|
271
|
+
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
// Try to parse PID and process name
|
|
274
|
+
const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
275
|
+
if (match) {
|
|
276
|
+
processes.push({
|
|
277
|
+
pid: parseInt(match[1], 10),
|
|
278
|
+
name: match[2].trim(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return processes;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Launch an app
|
|
288
|
+
*/
|
|
289
|
+
async launchApp(deviceId: string, bundleId: string, options: { arguments?: string[]; environment?: Record<string, string> } = {}): Promise<CommandResult> {
|
|
290
|
+
const args = ['device', 'process', 'launch', ...buildDeviceArg(deviceId), bundleId];
|
|
291
|
+
|
|
292
|
+
if (options.arguments?.length) {
|
|
293
|
+
args.push('--arguments', ...options.arguments);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (options.environment) {
|
|
297
|
+
for (const [key, value] of Object.entries(options.environment)) {
|
|
298
|
+
args.push('--environment', `${key}=${value}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return execDeviceCtl(args, this.execOpts());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Terminate a process
|
|
307
|
+
*/
|
|
308
|
+
async terminateProcess(deviceId: string, processNameOrPid: string | number): Promise<CommandResult> {
|
|
309
|
+
const args = ['device', 'process', 'terminate', ...buildDeviceArg(deviceId)];
|
|
310
|
+
|
|
311
|
+
if (typeof processNameOrPid === 'number') {
|
|
312
|
+
args.push('--pid', String(processNameOrPid));
|
|
313
|
+
} else {
|
|
314
|
+
args.push('--name', processNameOrPid);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return execDeviceCtl(args, this.execOpts());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Send signal to a process
|
|
322
|
+
*/
|
|
323
|
+
async signalProcess(deviceId: string, pid: number, signal: ProcessSignal): Promise<CommandResult> {
|
|
324
|
+
return execDeviceCtl(
|
|
325
|
+
['device', 'process', 'signal', '--pid', String(pid), '--signal', signal, ...buildDeviceArg(deviceId)],
|
|
326
|
+
this.execOpts()
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Suspend a process
|
|
332
|
+
*/
|
|
333
|
+
async suspendProcess(deviceId: string, pid: number): Promise<CommandResult> {
|
|
334
|
+
return execDeviceCtl(
|
|
335
|
+
['device', 'process', 'suspend', '--pid', String(pid), ...buildDeviceArg(deviceId)],
|
|
336
|
+
this.execOpts()
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Resume a process
|
|
342
|
+
*/
|
|
343
|
+
async resumeProcess(deviceId: string, pid: number): Promise<CommandResult> {
|
|
344
|
+
return execDeviceCtl(
|
|
345
|
+
['device', 'process', 'resume', '--pid', String(pid), ...buildDeviceArg(deviceId)],
|
|
346
|
+
this.execOpts()
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Send memory warning to a process
|
|
352
|
+
*/
|
|
353
|
+
async sendMemoryWarning(deviceId: string, pid: number): Promise<CommandResult> {
|
|
354
|
+
return execDeviceCtl(
|
|
355
|
+
['device', 'process', 'sendMemoryWarning', '--pid', String(pid), ...buildDeviceArg(deviceId)],
|
|
356
|
+
this.execOpts()
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Respring the device (restart SpringBoard)
|
|
362
|
+
*/
|
|
363
|
+
async respring(deviceId: string): Promise<CommandResult> {
|
|
364
|
+
// Find SpringBoard PID and send SIGHUP
|
|
365
|
+
const processes = await this.listProcesses(deviceId);
|
|
366
|
+
const springBoard = processes.find((p) => p.name === 'SpringBoard' || p.name.includes('SpringBoard'));
|
|
367
|
+
|
|
368
|
+
if (!springBoard) {
|
|
369
|
+
return { success: false, stdout: '', stderr: 'SpringBoard process not found', exitCode: 1 };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return this.signalProcess(deviceId, springBoard.pid, 'SIGHUP');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ==========================================================================
|
|
376
|
+
// App Management
|
|
377
|
+
// ==========================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* List installed apps
|
|
381
|
+
*/
|
|
382
|
+
async listApps(deviceId: string): Promise<InstalledApp[]> {
|
|
383
|
+
const result = await execDeviceCtl(
|
|
384
|
+
['device', 'info', 'apps', ...buildDeviceArg(deviceId)],
|
|
385
|
+
this.execOpts()
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (!result.success) return [];
|
|
389
|
+
|
|
390
|
+
return this.parseAppsFromText(result.stdout);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private parseAppsFromText(text: string): InstalledApp[] {
|
|
394
|
+
const lines = text.split('\n');
|
|
395
|
+
const apps: InstalledApp[] = [];
|
|
396
|
+
|
|
397
|
+
for (const line of lines) {
|
|
398
|
+
// Try parsing bundle identifier and name
|
|
399
|
+
const trimmed = line.trim();
|
|
400
|
+
if (trimmed && !trimmed.startsWith('[') && trimmed.includes('.')) {
|
|
401
|
+
const parts = trimmed.split(/\s+/);
|
|
402
|
+
if (parts.length >= 1) {
|
|
403
|
+
apps.push({
|
|
404
|
+
bundleIdentifier: parts[0],
|
|
405
|
+
bundleName: parts[1] || parts[0].split('.').pop() || '',
|
|
406
|
+
bundleVersion: '',
|
|
407
|
+
installPath: '',
|
|
408
|
+
platform: 'iOS',
|
|
409
|
+
type: 'User',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return apps;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Install an app
|
|
420
|
+
*/
|
|
421
|
+
async installApp(deviceId: string, appPath: string): Promise<CommandResult> {
|
|
422
|
+
return execDeviceCtl(
|
|
423
|
+
['device', 'install', 'app', ...buildDeviceArg(deviceId), appPath],
|
|
424
|
+
this.execOpts()
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Uninstall an app
|
|
430
|
+
*/
|
|
431
|
+
async uninstallApp(deviceId: string, bundleId: string): Promise<CommandResult> {
|
|
432
|
+
return execDeviceCtl(
|
|
433
|
+
['device', 'uninstall', ...buildDeviceArg(deviceId), bundleId],
|
|
434
|
+
this.execOpts()
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get app icon
|
|
440
|
+
*/
|
|
441
|
+
async getAppIcon(deviceId: string, bundleId: string, outputPath?: string): Promise<CommandResult> {
|
|
442
|
+
const args = ['device', 'info', 'appIcon', ...buildDeviceArg(deviceId), bundleId];
|
|
443
|
+
if (outputPath) {
|
|
444
|
+
args.push('--output', outputPath);
|
|
445
|
+
}
|
|
446
|
+
return execDeviceCtl(args, this.execOpts());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ==========================================================================
|
|
450
|
+
// File System
|
|
451
|
+
// ==========================================================================
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* List files in a directory
|
|
455
|
+
*/
|
|
456
|
+
async listFiles(deviceId: string, path: string): Promise<FileInfo[]> {
|
|
457
|
+
const result = await execDeviceCtl(
|
|
458
|
+
['device', 'info', 'files', ...buildDeviceArg(deviceId), '--path', path],
|
|
459
|
+
this.execOpts()
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (!result.success) return [];
|
|
463
|
+
|
|
464
|
+
return this.parseFilesFromText(result.stdout);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private parseFilesFromText(text: string): FileInfo[] {
|
|
468
|
+
const lines = text.split('\n');
|
|
469
|
+
const files: FileInfo[] = [];
|
|
470
|
+
|
|
471
|
+
for (const line of lines) {
|
|
472
|
+
const trimmed = line.trim();
|
|
473
|
+
if (!trimmed) continue;
|
|
474
|
+
|
|
475
|
+
const isDirectory = trimmed.endsWith('/');
|
|
476
|
+
const name = isDirectory ? trimmed.slice(0, -1) : trimmed;
|
|
477
|
+
|
|
478
|
+
if (name && name !== '.' && name !== '..') {
|
|
479
|
+
files.push({
|
|
480
|
+
path: name,
|
|
481
|
+
name: name.split('/').pop() || name,
|
|
482
|
+
isDirectory,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return files;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Copy file to device
|
|
492
|
+
*/
|
|
493
|
+
async copyToDevice(deviceId: string, source: string, destination: string): Promise<CommandResult> {
|
|
494
|
+
return execDeviceCtl(
|
|
495
|
+
['device', 'copy', 'to-device', ...buildDeviceArg(deviceId), '--source', source, '--destination', destination],
|
|
496
|
+
this.execOpts()
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Copy file from device
|
|
502
|
+
*/
|
|
503
|
+
async copyFromDevice(deviceId: string, source: string, destination: string): Promise<CommandResult> {
|
|
504
|
+
return execDeviceCtl(
|
|
505
|
+
['device', 'copy', 'from-device', ...buildDeviceArg(deviceId), '--source', source, '--destination', destination],
|
|
506
|
+
this.execOpts()
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ==========================================================================
|
|
511
|
+
// Notifications
|
|
512
|
+
// ==========================================================================
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Post a Darwin notification
|
|
516
|
+
*/
|
|
517
|
+
async postNotification(deviceId: string, name: string, userInfo?: Record<string, unknown>): Promise<CommandResult> {
|
|
518
|
+
const args = ['device', 'notification', 'post', ...buildDeviceArg(deviceId), '--name', name];
|
|
519
|
+
|
|
520
|
+
if (userInfo) {
|
|
521
|
+
args.push('--user-info', JSON.stringify(userInfo));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return execDeviceCtl(args, this.execOpts());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ==========================================================================
|
|
528
|
+
// Diagnostics
|
|
529
|
+
// ==========================================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Collect diagnostics
|
|
533
|
+
*/
|
|
534
|
+
async collectDiagnostics(options: { devices?: string; archiveDestination?: string } = {}): Promise<CommandResult> {
|
|
535
|
+
const args = ['diagnose'];
|
|
536
|
+
|
|
537
|
+
if (options.devices) {
|
|
538
|
+
args.push('--devices', options.devices);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (options.archiveDestination) {
|
|
542
|
+
args.push('--archive-destination', options.archiveDestination);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return execDeviceCtl(args, { ...this.execOpts(), timeout: 300000 }); // 5 min timeout
|
|
546
|
+
}
|
|
547
|
+
}
|
package/src/index.ts
ADDED