import { spawn, exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export interface ExecutionResult {
success: boolean;
stdout: string;
stderr: string;
exitCode: number | null;
duration: number;
}
export interface ExecutionOptions {
cwd?: string;
timeout?: number;
env?: Record<string, string>;
}
/**
* Execute a Detox CLI command and return structured results
*/
export async function executeDetoxCommand(
args: string[],
options: ExecutionOptions = {}
): Promise<ExecutionResult> {
const startTime = Date.now();
const cwd = options.cwd || process.cwd();
const timeout = options.timeout || 300000; // 5 minutes default
return new Promise((resolve) => {
const proc = spawn("npx", ["detox", ...args], {
cwd,
shell: true,
env: { ...process.env, ...options.env },
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
const timeoutId = setTimeout(() => {
proc.kill("SIGTERM");
resolve({
success: false,
stdout,
stderr: stderr + "\nProcess timed out",
exitCode: null,
duration: Date.now() - startTime,
});
}, timeout);
proc.on("close", (code) => {
clearTimeout(timeoutId);
resolve({
success: code === 0,
stdout,
stderr,
exitCode: code,
duration: Date.now() - startTime,
});
});
proc.on("error", (error) => {
clearTimeout(timeoutId);
resolve({
success: false,
stdout,
stderr: error.message,
exitCode: null,
duration: Date.now() - startTime,
});
});
});
}
/**
* Execute a shell command and return results
*/
export async function executeCommand(
command: string,
options: ExecutionOptions = {}
): Promise<ExecutionResult> {
const startTime = Date.now();
const cwd = options.cwd || process.cwd();
const timeout = options.timeout || 60000;
try {
const { stdout, stderr } = await execAsync(command, {
cwd,
timeout,
env: { ...process.env, ...options.env },
});
return {
success: true,
stdout,
stderr,
exitCode: 0,
duration: Date.now() - startTime,
};
} catch (error: any) {
return {
success: false,
stdout: error.stdout || "",
stderr: error.stderr || error.message,
exitCode: error.code || 1,
duration: Date.now() - startTime,
};
}
}
/**
* List iOS simulators using xcrun simctl
*/
export async function listIOSSimulators(): Promise<any[]> {
const result = await executeCommand("xcrun simctl list devices --json");
if (!result.success) {
return [];
}
try {
const data = JSON.parse(result.stdout);
const devices: any[] = [];
for (const [runtime, deviceList] of Object.entries(data.devices || {})) {
if (Array.isArray(deviceList)) {
for (const device of deviceList) {
devices.push({
id: device.udid,
name: device.name,
state: device.state,
runtime: runtime.replace("com.apple.CoreSimulator.SimRuntime.", ""),
platform: "ios",
});
}
}
}
return devices;
} catch {
return [];
}
}
/**
* List Android emulators using emulator command
*/
export async function listAndroidEmulators(): Promise<any[]> {
const result = await executeCommand("emulator -list-avds");
if (!result.success) {
return [];
}
const avdNames = result.stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
return avdNames.map((name) => ({
id: name,
name,
state: "available",
platform: "android",
}));
}
/**
* Check if Detox is installed in the project
*/
export async function isDetoxInstalled(cwd?: string): Promise<boolean> {
const result = await executeCommand("npx detox --version", { cwd });
return result.success;
}