Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17,163 changes: 10,032 additions & 7,131 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@
"@modelcontextprotocol/sdk": "^1.11.0",
"@xenova/transformers": "^2.17.2",
"@xmldom/xmldom": "^0.9.8",
"appium-uiautomator2-driver": "^4.2.3",
"appium-xcuitest-driver": "^9.6.0",
"appium-adb": "^12.12.1",
"appium-ios-device": "^3.1.0",
"appium-uiautomator2-driver": "^5.0.5",
"appium-xcuitest-driver": "^10.2.1",
"fast-xml-parser": "^5.2.3",
"fastmcp": "^1.23.2",
"form-data": "^4.0.3",
"langchain": "^0.3.27",
"lodash": "^4.17.21",
"node-simctl": "^8.0.4",
"rimraf": "^6.0.1",
"xpath": "^0.0.34",
"zod": "^3.24.3"
Expand All @@ -62,4 +65,3 @@
"typescript": "^5.8.3"
}
}

164 changes: 164 additions & 0 deletions src/devicemanager/adb-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ADB } from 'appium-adb';
import { log } from '../locators/logger.js';
/**
* Singleton ADB Manager to prevent multiple ADB instances
* This ensures only one ADB instance per host machine
*/
export class ADBManager {
private static instance: ADBManager;
private adbInstance: ADB | null = null;
private isInitialized = false;
private initializationPromise: Promise<ADB> | null = null;

private constructor() {}

/**
* Get the singleton instance of ADBManager
*/
public static getInstance(): ADBManager {
if (!ADBManager.instance) {
ADBManager.instance = new ADBManager();
}
return ADBManager.instance;
}

/**
* Initialize ADB instance with configuration
* @param options ADB configuration options
* @returns Promise<ADB> The initialized ADB instance
*/
public async initialize(
options: { adbExecTimeout?: number; udid?: string } = {}
): Promise<ADB> {
// If already initialized, return existing instance
if (this.isInitialized && this.adbInstance) {
log.debug(
'ADB instance already initialized, returning existing instance'
);
return this.adbInstance;
}

// If initialization is in progress, wait for it
if (this.initializationPromise) {
log.debug('ADB initialization in progress, waiting for completion');
return await this.initializationPromise;
}

// Start initialization
this.initializationPromise = this._createADBInstance(options);

try {
this.adbInstance = await this.initializationPromise;
this.isInitialized = true;
log.info('ADB instance initialized successfully');
return this.adbInstance;
} catch (error) {
log.error(`Failed to initialize ADB instance: ${error}`);
this.initializationPromise = null;
throw error;
}
}

/**
* Get the current ADB instance
* @returns ADB instance or null if not initialized
*/
public getADBInstance(): ADB | null {
return this.adbInstance;
}

/**
* Check if ADB is initialized
* @returns boolean indicating initialization status
*/
public isADBInitialized(): boolean {
return this.isInitialized && this.adbInstance !== null;
}

/**
* Reset the ADB instance (for testing or cleanup)
*/
public async reset(): Promise<void> {
if (this.adbInstance) {
try {
// Cleanup any existing ADB instance
log.info('Resetting ADB instance');
this.adbInstance = null;
this.isInitialized = false;
this.initializationPromise = null;
} catch (error) {
log.error(`Error resetting ADB instance: ${error}`);
}
}
}

/**
* Create ADB instance with proper error handling
* @param options ADB configuration options
* @returns Promise<ADB> The created ADB instance
*/
private async _createADBInstance(
options: { adbExecTimeout?: number; udid?: string } = {}
): Promise<ADB> {
const defaultOptions = {
adbExecTimeout: 60000,
...options,
};

log.info(
`Creating ADB instance with options: ${JSON.stringify(defaultOptions)}`
);

try {
const adb = await ADB.createADB(defaultOptions);
log.info('ADB instance created successfully');
return adb;
} catch (error) {
log.error(`Failed to create ADB instance: ${error}`);
throw new Error(`ADB initialization failed: ${error}`);
}
}

/**
* Get ADB instance for specific device operations
* This method ensures we reuse the singleton instance
* @param udid Optional device UDID for device-specific operations
* @returns Promise<ADB> The ADB instance
*/
public async getADBForDevice(udid?: string): Promise<ADB> {
if (!this.isADBInitialized()) {
await this.initialize({ udid });
}

if (!this.adbInstance) {
throw new Error('ADB instance not available');
}

return this.adbInstance;
}
}

/**
* Global ADB Manager instance
* Use this throughout the application to access ADB functionality
*/
export const adbManager = ADBManager.getInstance();

/**
* Convenience function to get ADB instance
* @param options ADB configuration options
* @returns Promise<ADB> The ADB instance
*/
export async function getADBInstance(
options: { adbExecTimeout?: number; udid?: string } = {}
): Promise<ADB> {
return await adbManager.getADBForDevice(options.udid);
}

/**
* Convenience function to get existing ADB instance (without initialization)
* @returns ADB instance or null if not initialized
*/
export function getExistingADBInstance(): ADB | null {
return adbManager.getADBInstance();
}
145 changes: 145 additions & 0 deletions src/devicemanager/ios-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Simctl } from 'node-simctl';
import { utilities } from 'appium-ios-device';
import { log } from '../locators/logger.js';

export interface IOSDevice {
name: string;
udid: string;
state?: string;
type: 'simulator' | 'real';
platform?: string;
}

/**
* iOS Device Manager to list and manage iOS devices and simulators
*/
export class IOSManager {
private static instance: IOSManager;
private simctl: Simctl;

private constructor() {
this.simctl = new Simctl();
}

/**
* Get the singleton instance of IOSManager
*/
public static getInstance(): IOSManager {
if (!IOSManager.instance) {
IOSManager.instance = new IOSManager();
}
return IOSManager.instance;
}

/**
* Check if running on macOS (required for iOS development)
*/
public isMac(): boolean {
return process.platform === 'darwin';
}

/**
* List all iOS simulators
* @returns Array of iOS simulators
*/
public async listSimulators(): Promise<IOSDevice[]> {
if (!this.isMac()) {
log.warn('iOS simulators are only available on macOS');
return [];
}

try {
const devices = await this.simctl.getDevices();
const simulators: IOSDevice[] = [];

// devices is an object with runtime as key (e.g., "18.2") and array of devices as value
// node-simctl returns simplified runtime keys that are already iOS versions
for (const [runtime, deviceList] of Object.entries(devices)) {
if (Array.isArray(deviceList)) {
for (const device of deviceList) {
simulators.push({
name: device.name,
udid: device.udid,
state: device.state,
type: 'simulator',
platform: runtime, // Runtime is already the iOS version (e.g., "18.2")
});
}
}
}

return simulators;
} catch (error) {
log.error(`Error listing iOS simulators: ${error}`);
return [];
}
}

/**
* List only booted (running) iOS simulators
* @returns Array of booted simulators
*/
public async listBootedSimulators(): Promise<IOSDevice[]> {
const allSimulators = await this.listSimulators();
return allSimulators.filter(simulator => simulator.state === 'Booted');
}

/**
* List all connected real iOS devices
* @returns Array of real iOS devices
*/
public async listRealDevices(): Promise<IOSDevice[]> {
if (!this.isMac()) {
log.warn('iOS real devices are only available on macOS');
return [];
}

try {
const devices = await utilities.getConnectedDevices();
return devices.map((udid: string) => ({
name: udid, // We'll use UDID as name for now
udid: udid,
type: 'real' as const,
}));
} catch (error) {
log.error(`Error listing iOS real devices: ${error}`);
return [];
}
}

/**
* Get all available iOS simulators
* @returns Array of all iOS simulators (both booted and shutdown)
*/
public async getAvailableSimulators(): Promise<IOSDevice[]> {
return await this.listSimulators();
}

/**
* Get all available real devices
* @returns Array of real iOS devices
*/
public async getAvailableRealDevices(): Promise<IOSDevice[]> {
return await this.listRealDevices();
}

/**
* Get devices based on device type
* @param deviceType 'simulator' or 'real'
* @returns Array of iOS devices
*/
public async getDevicesByType(
deviceType: 'simulator' | 'real'
): Promise<IOSDevice[]> {
if (deviceType === 'simulator') {
return await this.getAvailableSimulators();
} else {
return await this.getAvailableRealDevices();
}
}
}

/**
* Global iOS Manager instance
*/
export const iosManager = IOSManager.getInstance();
4 changes: 4 additions & 0 deletions src/locators/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class Logger {
error(...args: any[]): void {
console.error(...args);
}

debug(...args: any[]): void {
console.debug(...args);
}
}

export const log = new Logger();
Loading