From bdf1991a45389143e9df4f1b7939f0de93fff805 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 2 Nov 2025 17:42:54 +0100 Subject: [PATCH 1/5] feat: Add a possibility to list connected devices --- lib/devicectl.ts | 8 +- lib/mixins/list.ts | 13 ++++ lib/types.ts | 185 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 lib/mixins/list.ts diff --git a/lib/devicectl.ts b/lib/devicectl.ts index 44aad05..fef7200 100644 --- a/lib/devicectl.ts +++ b/lib/devicectl.ts @@ -5,6 +5,7 @@ import type {ExecuteOptions, ExecuteResult} from './types'; import * as processMixins from './mixins/process'; import * as infoMixins from './mixins/info'; import * as copyMixins from './mixins/copy'; +import * as listMixins from './mixins/list'; const XCRUN = 'xcrun'; const LOG_TAG = 'Devicectl'; @@ -44,11 +45,12 @@ export class Devicectl { logStdout = false, asynchronous = false, asJson = true, + noDevice = false, subcommandOptions, timeout, } = opts ?? {}; - const finalArgs = ['devicectl', ...subcommand, '--device', this.udid]; + const finalArgs = ['devicectl', ...subcommand, ...(noDevice ? [] : ['--device', this.udid])]; if (subcommandOptions && !_.isEmpty(subcommandOptions)) { finalArgs.push( @@ -85,8 +87,12 @@ export class Devicectl { sendMemoryWarning = processMixins.sendMemoryWarning; sendSignalToProcess = processMixins.sendSignalToProcess; launchApp = processMixins.launchApp; + listProcesses = infoMixins.listProcesses; listApps = infoMixins.listApps; + listFiles = copyMixins.listFiles; pullFile = copyMixins.pullFile; + + listDevices = listMixins.listDevices; } diff --git a/lib/mixins/list.ts b/lib/mixins/list.ts new file mode 100644 index 0000000..25eae94 --- /dev/null +++ b/lib/mixins/list.ts @@ -0,0 +1,13 @@ +import type {DeviceInfo} from '../types'; +import type {Devicectl} from '../devicectl'; + +/** + * Retrieves the list of connected device infos. + * Mught be empty if no devices are connected. + */ +export async function listDevices(this: Devicectl): Promise { + const {stdout} = await this.execute(['list', 'devices'], { + noDevice: true, + }); + return JSON.parse(stdout).result.devices; +} diff --git a/lib/types.ts b/lib/types.ts index 54e8ed6..39bdf00 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -42,6 +42,8 @@ export interface AppInfo { * Options for executing devicectl commands */ export interface ExecuteOptions { + /** Whether to add the --device option to the actual devicectl command */ + noDevice?: boolean; /** Whether to log stdout output */ logStdout?: boolean; /** Whether to return JSON output */ @@ -111,3 +113,186 @@ export type AsyncExecuteResult = SubProcess; export type ExecuteResult = T extends AsyncExecuteOptions ? AsyncExecuteResult : SyncExecuteResult; + +/** + * CPU type information + */ +export interface CPUType { + /** The CPU type name + * @example "arm64e" */ + name: string; + /** The CPU subtype + * @example 2 */ + subType: number; + /** The CPU type identifier + * @example 16777228 */ + type: number; +} + +/** + * Device capability information + */ +export interface Capability { + /** The feature identifier + * @example "com.apple.coredevice.feature.installapp" */ + featureIdentifier: string; + /** The capability name + * @example "Install Application" */ + name: string; +} + +/** + * Connection properties for the device + */ +export interface ConnectionProperties { + /** The authentication type + * @example "manualPairing" */ + authenticationType: string; + /** Whether this is a mobile device only + * @example false */ + isMobileDeviceOnly: boolean; + /** The last connection date in ISO format + * @example "2025-01-01T12:00:00.000Z" */ + lastConnectionDate: string; + /** List of local hostnames + * @example ["MyDevice.coredevice.local", "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV.coredevice.local"] */ + localHostnames: string[]; + /** The pairing state + * @example "paired" */ + pairingState: string; + /** List of potential hostnames + * @example ["MyDevice.coredevice.local", "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV.coredevice.local"] */ + potentialHostnames: string[]; + /** The transport type + * @example "wired" */ + transportType: string; + /** The tunnel IP address + * @example "fdda:f9b3:f5d9::1" */ + tunnelIPAddress: string; + /** The tunnel state + * @example "connected" */ + tunnelState: string; + /** The tunnel transport protocol + * @example "tcp" */ + tunnelTransportProtocol: string; +} + +/** + * Device properties + */ +export interface DeviceProperties { + /** The boot state + * @example "booted" */ + bootState: string; + /** Whether booted from snapshot + * @example true */ + bootedFromSnapshot: boolean; + /** The booted snapshot name + * @example "com.apple.os.update-ABCDEF1234567890" */ + bootedSnapshotName?: string; + /** Whether DDI services are available + * @example true */ + ddiServicesAvailable: boolean; + /** The developer mode status + * @example "enabled" */ + developerModeStatus: string; + /** Whether has internal OS build + * @example false */ + hasInternalOSBuild: boolean; + /** The device name + * @example "My iPhone" */ + name: string; + /** The OS build update + * @example "22A100" */ + osBuildUpdate: string; + /** The OS version number + * @example "18.0.0" */ + osVersionNumber: string; + /** Whether root file system is writable + * @example false */ + rootFileSystemIsWritable: boolean; + /** The screen viewing URL + * @example "devices://device/open?id=ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV" */ + screenViewingURL: string; + /** Whether supports checked allocations + * @example false */ + supportsCheckedAllocations: boolean; +} + +/** + * Hardware properties for the device + */ +export interface HardwareProperties { + /** The CPU type + * @example { name: "arm64e", subType: 2, type: 16777228 } */ + cpuType: CPUType; + /** The device type + * @example "iPhone" */ + deviceType: string; + /** The ECID + * @example 1234567890123456 */ + ecid: number; + /** The hardware model + * @example "D63AP" */ + hardwareModel: string; + /** The internal storage capacity in bytes + * @example 128000000000 */ + internalStorageCapacity: number; + /** Whether is production fused + * @example true */ + isProductionFused: boolean; + /** The marketing name + * @example "iPhone 15" */ + marketingName: string; + /** The platform + * @example "iOS" */ + platform: string; + /** The product type + * @example "iPhone16,1" */ + productType: string; + /** The reality type (physical or simulator) + * @example "physical" */ + reality: string; + /** The serial number + * @example "ABC1234XYZ" */ + serialNumber: string; + /** List of supported CPU types + * @example [{ name: "arm64e", subType: 2, type: 16777228 }, { name: "arm64", subType: 0, type: 16777228 }] */ + supportedCPUTypes: CPUType[]; + /** List of supported device families + * @example [1, 2] */ + supportedDeviceFamilies: number[]; + /** The thinning product type + * @example "iPhone16,1" */ + thinningProductType: string; + /** The UDID + * @example "00000000-0000000000000000" */ + udid: string; +} + +/** + * Complete device information + */ +export interface DeviceInfo { + /** List of device capabilities + * @example [{ featureIdentifier: "com.apple.coredevice.feature.installapp", name: "Install Application" }] */ + capabilities: Capability[]; + /** Connection properties + * @example { authenticationType: "manualPairing", pairingState: "paired", transportType: "wired" } */ + connectionProperties: ConnectionProperties; + /** Device properties + * @example { name: "My iPhone", bootState: "booted", osVersionNumber: "18.0.0" } */ + deviceProperties: DeviceProperties; + /** Hardware properties + * @example { deviceType: "iPhone", platform: "iOS", udid: "00000000-0000000000000000" } */ + hardwareProperties: HardwareProperties; + /** The device identifier + * @example "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV" */ + identifier: string; + /** List of tags + * @example [] */ + tags: string[]; + /** The visibility class + * @example "default" */ + visibilityClass: string; +} \ No newline at end of file From ebbbc896696dfb25ef6d7c267a9c95a9ce58ab32 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 2 Nov 2025 17:44:25 +0100 Subject: [PATCH 2/5] Add a unit test --- lib/mixins/list.ts | 2 +- test/unit/devicectl-specs.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mixins/list.ts b/lib/mixins/list.ts index 25eae94..16d0f76 100644 --- a/lib/mixins/list.ts +++ b/lib/mixins/list.ts @@ -3,7 +3,7 @@ import type {Devicectl} from '../devicectl'; /** * Retrieves the list of connected device infos. - * Mught be empty if no devices are connected. + * Might be empty if no devices are connected. */ export async function listDevices(this: Devicectl): Promise { const {stdout} = await this.execute(['list', 'devices'], { diff --git a/test/unit/devicectl-specs.ts b/test/unit/devicectl-specs.ts index 30fb2a3..7a6102b 100644 --- a/test/unit/devicectl-specs.ts +++ b/test/unit/devicectl-specs.ts @@ -63,4 +63,10 @@ describe('Devicectl', function () { expect(devicectl.launchApp).to.be.a('function'); }); }); + + describe('listDevices', function () { + it('should be a function', function () { + expect(devicectl.listDevices).to.be.a('function'); + }); + }); }); From f46a2effe030846ec6ace646edee49077010da90 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 2 Nov 2025 17:46:24 +0100 Subject: [PATCH 3/5] Add default values --- lib/types.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 39bdf00..1785f0b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -42,13 +42,25 @@ export interface AppInfo { * Options for executing devicectl commands */ export interface ExecuteOptions { - /** Whether to add the --device option to the actual devicectl command */ + /** + * Whether to add the --device option to the actual devicectl command + * @default false + */ noDevice?: boolean; - /** Whether to log stdout output */ + /** + * Whether to log stdout output + * @default false + */ logStdout?: boolean; - /** Whether to return JSON output */ + /** + * Whether to return JSON output + * @default true + */ asJson?: boolean; - /** Whether to run the command asynchronously */ + /** + * Whether to run the command asynchronously + * @default false + */ asynchronous?: boolean; /** Additional subcommand options */ subcommandOptions?: string[] | string; From 5e3542f68f47bdc6c92dddd48a02d0491d0b89b0 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 2 Nov 2025 17:47:16 +0100 Subject: [PATCH 4/5] tune docs --- lib/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 1785f0b..8e216bc 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -43,7 +43,7 @@ export interface AppInfo { */ export interface ExecuteOptions { /** - * Whether to add the --device option to the actual devicectl command + * Whether to drop the --device option from the actual devicectl command * @default false */ noDevice?: boolean; From 4271098e704b24af27d0b3c9d947d19e27253a96 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 2 Nov 2025 10:25:23 -0800 Subject: [PATCH 5/5] reflect tvOS --- lib/types.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 8e216bc..cf40e51 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -165,10 +165,10 @@ export interface ConnectionProperties { isMobileDeviceOnly: boolean; /** The last connection date in ISO format * @example "2025-01-01T12:00:00.000Z" */ - lastConnectionDate: string; + lastConnectionDate?: string; /** List of local hostnames * @example ["MyDevice.coredevice.local", "ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV.coredevice.local"] */ - localHostnames: string[]; + localHostnames?: string[]; /** The pairing state * @example "paired" */ pairingState: string; @@ -177,16 +177,16 @@ export interface ConnectionProperties { potentialHostnames: string[]; /** The transport type * @example "wired" */ - transportType: string; + transportType?: string; /** The tunnel IP address * @example "fdda:f9b3:f5d9::1" */ - tunnelIPAddress: string; + tunnelIPAddress?: string; /** The tunnel state * @example "connected" */ tunnelState: string; /** The tunnel transport protocol * @example "tcp" */ - tunnelTransportProtocol: string; + tunnelTransportProtocol?: string; } /** @@ -195,22 +195,22 @@ export interface ConnectionProperties { export interface DeviceProperties { /** The boot state * @example "booted" */ - bootState: string; + bootState?: string; /** Whether booted from snapshot * @example true */ - bootedFromSnapshot: boolean; + bootedFromSnapshot?: boolean; /** The booted snapshot name * @example "com.apple.os.update-ABCDEF1234567890" */ bootedSnapshotName?: string; /** Whether DDI services are available * @example true */ - ddiServicesAvailable: boolean; + ddiServicesAvailable?: boolean; /** The developer mode status * @example "enabled" */ - developerModeStatus: string; + developerModeStatus?: string; /** Whether has internal OS build * @example false */ - hasInternalOSBuild: boolean; + hasInternalOSBuild?: boolean; /** The device name * @example "My iPhone" */ name: string; @@ -222,13 +222,13 @@ export interface DeviceProperties { osVersionNumber: string; /** Whether root file system is writable * @example false */ - rootFileSystemIsWritable: boolean; + rootFileSystemIsWritable?: boolean; /** The screen viewing URL * @example "devices://device/open?id=ABCD1234-5678-90EF-GHIJ-KLMNOPQRSTUV" */ - screenViewingURL: string; + screenViewingURL?: string; /** Whether supports checked allocations * @example false */ - supportsCheckedAllocations: boolean; + supportsCheckedAllocations?: boolean; } /** @@ -249,34 +249,34 @@ export interface HardwareProperties { hardwareModel: string; /** The internal storage capacity in bytes * @example 128000000000 */ - internalStorageCapacity: number; + internalStorageCapacity?: number; /** Whether is production fused * @example true */ - isProductionFused: boolean; + isProductionFused?: boolean; /** The marketing name * @example "iPhone 15" */ - marketingName: string; + marketingName?: string; /** The platform - * @example "iOS" */ + * @example "iOS", "tvOS" */ platform: string; /** The product type * @example "iPhone16,1" */ productType: string; /** The reality type (physical or simulator) * @example "physical" */ - reality: string; + reality?: string; /** The serial number * @example "ABC1234XYZ" */ - serialNumber: string; + serialNumber?: string; /** List of supported CPU types * @example [{ name: "arm64e", subType: 2, type: 16777228 }, { name: "arm64", subType: 0, type: 16777228 }] */ - supportedCPUTypes: CPUType[]; + supportedCPUTypes?: CPUType[]; /** List of supported device families - * @example [1, 2] */ + * @example [1, 2], [3] */ supportedDeviceFamilies: number[]; /** The thinning product type * @example "iPhone16,1" */ - thinningProductType: string; + thinningProductType?: string; /** The UDID * @example "00000000-0000000000000000" */ udid: string;