Skip to content

Commit ffce717

Browse files
feat: Add a possibility to list connected devices (#3)
1 parent f487eda commit ffce717

File tree

4 files changed

+226
-4
lines changed

4 files changed

+226
-4
lines changed

lib/devicectl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {ExecuteOptions, ExecuteResult} from './types';
55
import * as processMixins from './mixins/process';
66
import * as infoMixins from './mixins/info';
77
import * as copyMixins from './mixins/copy';
8+
import * as listMixins from './mixins/list';
89

910
const XCRUN = 'xcrun';
1011
const LOG_TAG = 'Devicectl';
@@ -44,11 +45,12 @@ export class Devicectl {
4445
logStdout = false,
4546
asynchronous = false,
4647
asJson = true,
48+
noDevice = false,
4749
subcommandOptions,
4850
timeout,
4951
} = opts ?? {};
5052

51-
const finalArgs = ['devicectl', ...subcommand, '--device', this.udid];
53+
const finalArgs = ['devicectl', ...subcommand, ...(noDevice ? [] : ['--device', this.udid])];
5254

5355
if (subcommandOptions && !_.isEmpty(subcommandOptions)) {
5456
finalArgs.push(
@@ -85,8 +87,12 @@ export class Devicectl {
8587
sendMemoryWarning = processMixins.sendMemoryWarning;
8688
sendSignalToProcess = processMixins.sendSignalToProcess;
8789
launchApp = processMixins.launchApp;
90+
8891
listProcesses = infoMixins.listProcesses;
8992
listApps = infoMixins.listApps;
93+
9094
listFiles = copyMixins.listFiles;
9195
pullFile = copyMixins.pullFile;
96+
97+
listDevices = listMixins.listDevices;
9298
}

lib/mixins/list.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type {DeviceInfo} from '../types';
2+
import type {Devicectl} from '../devicectl';
3+
4+
/**
5+
* Retrieves the list of connected device infos.
6+
* Might be empty if no devices are connected.
7+
*/
8+
export async function listDevices(this: Devicectl): Promise<DeviceInfo[]> {
9+
const {stdout} = await this.execute(['list', 'devices'], {
10+
noDevice: true,
11+
});
12+
return JSON.parse(stdout).result.devices;
13+
}

lib/types.ts

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,25 @@ export interface AppInfo {
4242
* Options for executing devicectl commands
4343
*/
4444
export interface ExecuteOptions {
45-
/** Whether to log stdout output */
45+
/**
46+
* Whether to drop the --device option from the actual devicectl command
47+
* @default false
48+
*/
49+
noDevice?: boolean;
50+
/**
51+
* Whether to log stdout output
52+
* @default false
53+
*/
4654
logStdout?: boolean;
47-
/** Whether to return JSON output */
55+
/**
56+
* Whether to return JSON output
57+
* @default true
58+
*/
4859
asJson?: boolean;
49-
/** Whether to run the command asynchronously */
60+
/**
61+
* Whether to run the command asynchronously
62+
* @default false
63+
*/
5064
asynchronous?: boolean;
5165
/** Additional subcommand options */
5266
subcommandOptions?: string[] | string;
@@ -111,3 +125,186 @@ export type AsyncExecuteResult = SubProcess;
111125
export type ExecuteResult<T extends ExecuteOptions> = T extends AsyncExecuteOptions
112126
? AsyncExecuteResult
113127
: SyncExecuteResult;
128+
129+
/**
130+
* CPU type information
131+
*/
132+
export interface CPUType {
133+
/** The CPU type name
134+
* @example "arm64e" */
135+
name: string;
136+
/** The CPU subtype
137+
* @example 2 */
138+
subType: number;
139+
/** The CPU type identifier
140+
* @example 16777228 */
141+
type: number;
142+
}
143+
144+
/**
145+
* Device capability information
146+
*/
147+
export interface Capability {
148+
/** The feature identifier
149+
* @example "com.apple.coredevice.feature.installapp" */
150+
featureIdentifier: string;
151+
/** The capability name
152+
* @example "Install Application" */
153+
name: string;
154+
}
155+
156+
/**
157+
* Connection properties for the device
158+
*/
159+
export interface ConnectionProperties {
160+
/** The authentication type
161+
* @example "manualPairing" */
162+
authenticationType: string;
163+
/** Whether this is a mobile device only
164+
* @example false */
165+
isMobileDeviceOnly: boolean;
166+
/** The last connection date in ISO format
167+
* @example "2025-01-01T12:00:00.000Z" */
168+
lastConnectionDate?: string;
169+
/** List of local hostnames
170+
* @example ["MyDevice.coredevice.local", "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV.coredevice.local"] */
171+
localHostnames?: string[];
172+
/** The pairing state
173+
* @example "paired" */
174+
pairingState: string;
175+
/** List of potential hostnames
176+
* @example ["MyDevice.coredevice.local", "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV.coredevice.local"] */
177+
potentialHostnames: string[];
178+
/** The transport type
179+
* @example "wired" */
180+
transportType?: string;
181+
/** The tunnel IP address
182+
* @example "fdda:f9b3:f5d9::1" */
183+
tunnelIPAddress?: string;
184+
/** The tunnel state
185+
* @example "connected" */
186+
tunnelState: string;
187+
/** The tunnel transport protocol
188+
* @example "tcp" */
189+
tunnelTransportProtocol?: string;
190+
}
191+
192+
/**
193+
* Device properties
194+
*/
195+
export interface DeviceProperties {
196+
/** The boot state
197+
* @example "booted" */
198+
bootState?: string;
199+
/** Whether booted from snapshot
200+
* @example true */
201+
bootedFromSnapshot?: boolean;
202+
/** The booted snapshot name
203+
* @example "com.apple.os.update-ABCDEF1234567890" */
204+
bootedSnapshotName?: string;
205+
/** Whether DDI services are available
206+
* @example true */
207+
ddiServicesAvailable?: boolean;
208+
/** The developer mode status
209+
* @example "enabled" */
210+
developerModeStatus?: string;
211+
/** Whether has internal OS build
212+
* @example false */
213+
hasInternalOSBuild?: boolean;
214+
/** The device name
215+
* @example "My iPhone" */
216+
name: string;
217+
/** The OS build update
218+
* @example "22A100" */
219+
osBuildUpdate: string;
220+
/** The OS version number
221+
* @example "18.0.0" */
222+
osVersionNumber: string;
223+
/** Whether root file system is writable
224+
* @example false */
225+
rootFileSystemIsWritable?: boolean;
226+
/** The screen viewing URL
227+
* @example "devices://device/open?id=ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV" */
228+
screenViewingURL?: string;
229+
/** Whether supports checked allocations
230+
* @example false */
231+
supportsCheckedAllocations?: boolean;
232+
}
233+
234+
/**
235+
* Hardware properties for the device
236+
*/
237+
export interface HardwareProperties {
238+
/** The CPU type
239+
* @example { name: "arm64e", subType: 2, type: 16777228 } */
240+
cpuType: CPUType;
241+
/** The device type
242+
* @example "iPhone" */
243+
deviceType: string;
244+
/** The ECID
245+
* @example 1234567890123456 */
246+
ecid: number;
247+
/** The hardware model
248+
* @example "D63AP" */
249+
hardwareModel: string;
250+
/** The internal storage capacity in bytes
251+
* @example 128000000000 */
252+
internalStorageCapacity?: number;
253+
/** Whether is production fused
254+
* @example true */
255+
isProductionFused?: boolean;
256+
/** The marketing name
257+
* @example "iPhone 15" */
258+
marketingName?: string;
259+
/** The platform
260+
* @example "iOS", "tvOS" */
261+
platform: string;
262+
/** The product type
263+
* @example "iPhone16,1" */
264+
productType: string;
265+
/** The reality type (physical or simulator)
266+
* @example "physical" */
267+
reality?: string;
268+
/** The serial number
269+
* @example "ABC1234XYZ" */
270+
serialNumber?: string;
271+
/** List of supported CPU types
272+
* @example [{ name: "arm64e", subType: 2, type: 16777228 }, { name: "arm64", subType: 0, type: 16777228 }] */
273+
supportedCPUTypes?: CPUType[];
274+
/** List of supported device families
275+
* @example [1, 2], [3] */
276+
supportedDeviceFamilies: number[];
277+
/** The thinning product type
278+
* @example "iPhone16,1" */
279+
thinningProductType?: string;
280+
/** The UDID
281+
* @example "00000000-0000000000000000" */
282+
udid: string;
283+
}
284+
285+
/**
286+
* Complete device information
287+
*/
288+
export interface DeviceInfo {
289+
/** List of device capabilities
290+
* @example [{ featureIdentifier: "com.apple.coredevice.feature.installapp", name: "Install Application" }] */
291+
capabilities: Capability[];
292+
/** Connection properties
293+
* @example { authenticationType: "manualPairing", pairingState: "paired", transportType: "wired" } */
294+
connectionProperties: ConnectionProperties;
295+
/** Device properties
296+
* @example { name: "My iPhone", bootState: "booted", osVersionNumber: "18.0.0" } */
297+
deviceProperties: DeviceProperties;
298+
/** Hardware properties
299+
* @example { deviceType: "iPhone", platform: "iOS", udid: "00000000-0000000000000000" } */
300+
hardwareProperties: HardwareProperties;
301+
/** The device identifier
302+
* @example "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV" */
303+
identifier: string;
304+
/** List of tags
305+
* @example [] */
306+
tags: string[];
307+
/** The visibility class
308+
* @example "default" */
309+
visibilityClass: string;
310+
}

test/unit/devicectl-specs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,10 @@ describe('Devicectl', function () {
6363
expect(devicectl.launchApp).to.be.a('function');
6464
});
6565
});
66+
67+
describe('listDevices', function () {
68+
it('should be a function', function () {
69+
expect(devicectl.listDevices).to.be.a('function');
70+
});
71+
});
6672
});

0 commit comments

Comments
 (0)