import { execSync } from "child_process";
import { readFileSync, unlinkSync } from "fs";
import { randomBytes } from "crypto";
import { tmpdir } from "os";
export interface Device {
id: string;
name: string;
platform: "aurora";
state: string;
isSimulator: boolean;
host?: string;
}
export interface LogOptions {
lines?: number;
priority?: string;
unit?: string;
grep?: string;
since?: string;
}
export class AuroraClient {
private escapeShellArg(arg: string): string {
return arg
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/'/g, "\\'")
.replace(/`/g, "\\`")
.replace(/\$/g, "\\$")
.replace(/ /g, "%s")
.replace(/&/g, "\\&")
.replace(/\(/g, "\\(")
.replace(/\)/g, "\\)")
.replace(/</g, "\\<")
.replace(/>/g, "\\>")
.replace(/\|/g, "\\|")
.replace(/;/g, "\\;");
}
private runCommandSync(command: string): string {
try {
const output = execSync(command, { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
return output.trim();
} catch (error: unknown) {
if (error instanceof Error) {
if (error.message.includes("audb: command not found")) {
throw new Error("audb not found. Install: cargo install audb-client");
}
throw new Error(`Command '${command}' failed: ${error.message}`);
}
throw new Error(`Command '${command}' failed with unknown error`);
}
}
async checkAvailability(): Promise<boolean> {
try {
execSync("audb --version", { encoding: "utf-8" });
return true;
} catch {
return false;
}
}
/**
* List all configured Aurora devices
* @returns Array of Device objects
*/
listDevices(): Device[] {
try {
const output = this.runCommandSync("audb device list");
const devices: Device[] = [];
// Strip ANSI escape codes
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
const lines = cleanOutput.split("\n");
for (const line of lines) {
// Skip headers, separators, and empty lines
if (!line.trim() || line.includes("---") || line.includes("Index")) continue;
// Parse format: "0 R570 192.168.2.13 22 aurora-arm connected(3609s) *"
const match = line.match(/^\s*\d+\s+(\S+)\s+([\d.]+)\s+\d+\s+(?:\S+)\s+(.+?)\s*(?:\*)?$/);
if (match) {
const [, name, host, status] = match;
const isConnected = status.includes("connected");
devices.push({
id: host,
name: name.trim(),
platform: "aurora",
state: isConnected ? "connected" : "disconnected",
isSimulator: false,
host,
});
}
}
return devices;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[Aurora] Failed to list devices: ${errorMessage}`);
// Return empty array on error (e.g., audb not installed)
return [];
}
}
getActiveDevice(): string {
const path = `${process.env.HOME}/.config/audb/current_device`;
try {
return readFileSync(path, "utf-8");
} catch (error: unknown) {
if (error instanceof Error && 'code' in error) {
const errorCode = (error as NodeJS.ErrnoException).code;
if (errorCode === 'ENOENT') {
throw new Error("No device selected");
}
}
throw new Error(`Failed to read active device from ${path}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Performs a tap at the specified coordinates.
* @param x - X coordinate in pixels
* @param y - Y coordinate in pixels
*/
tap(x: number, y: number): void {
this.runCommandSync(`audb tap ${x} ${y}`);
}
/**
* Performs a long press at the specified coordinates.
* @param x - X coordinate in pixels
* @param y - Y coordinate in pixels
* @param duration - Duration of the press in milliseconds
*/
longPress(x: number, y: number, duration: number): void {
this.runCommandSync(`audb tap ${x} ${y} --duration ${duration}`);
}
/**
* Performs a swipe in the specified direction.
* @param direction - Direction to swipe: "up", "down", "left", or "right"
*/
swipeDirection(direction: "up"|"down"|"left"|"right"): void {
this.runCommandSync(`audb swipe ${direction}`);
}
/**
* Performs a swipe from one coordinate to another.
* @param x1 - Starting X coordinate in pixels
* @param y1 - Starting Y coordinate in pixels
* @param x2 - Ending X coordinate in pixels
* @param y2 - Ending Y coordinate in pixels
*/
swipeCoords(x1: number, y1: number, x2: number, y2: number): void {
this.runCommandSync(`audb swipe ${x1} ${y1} ${x2} ${y2}`);
}
/**
* Performs a swipe from one coordinate to another.
* Compatible with AdbClient signature.
* @param x1 - Starting X coordinate
* @param y1 - Starting Y coordinate
* @param x2 - Ending X coordinate
* @param y2 - Ending Y coordinate
* @param durationMs - Duration in milliseconds (ignored by audb, kept for compatibility)
*/
swipe(x1: number, y1: number, x2: number, y2: number, durationMs?: number): void {
this.runCommandSync(`audb swipe ${x1} ${y1} ${x2} ${y2}`);
}
/**
* Input text on Aurora device.
* @unimplemented - audb doesn't have direct text input support yet
* @todo Implement via clipboard or D-Bus when available
*/
inputText(text: string): void {
console.warn(`[Aurora] inputText not implemented: "${text}"`);
// Placeholder - return silently or implement via clipboard in future
}
/**
* Get UI hierarchy from Aurora device.
* @unimplemented - UI scraping not available via audb yet
* @todo Implement when audb adds UI dump support
*/
getUiHierarchy(): string {
console.warn("[Aurora] getUiHierarchy not implemented");
return "<hierarchy><note>Aurora UI hierarchy not yet available via audb</note></hierarchy>";
}
/**
* Clear app data on Aurora device.
* @unimplemented - audb doesn't have this command yet
*/
clearAppData(packageName: string): void {
console.warn(`[Aurora] clearAppData not implemented for ${packageName}`);
}
/**
* Sends a keyboard key event to the device.
* @param key - Key name to send (e.g., "Enter", "Back", "Home")
*/
pressKey(key: string): void {
this.runCommandSync(`audb key ${key}`);
}
/**
* Take screenshot and return raw PNG buffer (consistent with Android/iOS)
* @returns Raw PNG buffer
*/
screenshotRaw(): Buffer {
const uniqueId = randomBytes(8).toString("hex");
const tmpFile = `${tmpdir()}/aurora_screenshot_${uniqueId}.png`;
try {
execSync(`audb screenshot --output "${tmpFile}"`);
return readFileSync(tmpFile);
} finally {
try { unlinkSync(tmpFile); } catch {}
}
}
/**
* Takes a screenshot of the Aurora device
* @returns Base64 encoded PNG screenshot
*/
screenshot(): string {
return this.screenshotRaw().toString("base64");
}
/**
* Launch an application on the Aurora device
* @param packageName - Application name (D-Bus format: ru.domain.AppName)
* @returns Output message from audb
*/
launchApp(packageName: string): string {
const output = this.runCommandSync(`audb launch ${packageName}`);
return output || `Launched ${packageName}`;
}
/**
* Stop a running application
* @param packageName - Application name (D-Bus format: ru.domain.AppName)
*/
stopApp(packageName: string): void {
this.runCommandSync(`audb stop ${packageName}`);
}
/**
* Install an RPM package on the Aurora device
* @param path - Local path to the RPM file
* @returns Installation result message
*/
installApp(path: string): string {
const output = this.runCommandSync(`audb package install ${path}`);
return output || `Installed ${path}`;
}
/**
* Uninstall a package from the Aurora device
* @param packageName - Package name (e.g., ru.domain.AppName)
* @returns Uninstallation result message
*/
uninstallApp(packageName: string): string {
const output = this.runCommandSync(`audb package uninstall ${packageName}`);
return output || `Uninstalled ${packageName}`;
}
/**
* List installed packages on the Aurora device
* @returns Array of package names
*/
listPackages(): string[] {
const output = this.runCommandSync("audb package list");
if (!output) return [];
return output.split("\n").filter(line => line.trim().length > 0);
}
/**
* Execute a shell command on the Aurora device
*
* WARNING: This method executes arbitrary commands on the device.
* Input validation should be performed at the call site.
*
* @param command - Shell command to execute (must be validated/sanitized)
* @returns Command output
*/
shell(command: string): string {
const escaped = this.escapeShellArg(command);
return this.runCommandSync(`audb shell ${escaped}`);
}
/**
* Get device logs with optional filters
* @param options - Log filtering options
* @param options.lines - Maximum number of log lines to retrieve
* @param options.priority - Filter by log priority level
* @param options.unit - Filter by systemd unit
* @param options.grep - Filter by grep pattern
* @param options.since - Show logs since timestamp
* @returns Log output
*/
getLogs(options: LogOptions = {}): string {
let cmd = "audb logs";
if (options.lines) cmd += ` -n ${options.lines}`;
if (options.priority) cmd += ` --priority ${options.priority}`;
if (options.unit) cmd += ` --unit ${options.unit}`;
if (options.grep) {
const escaped = options.grep.replace(/'/g, "'\\''");
cmd += ` --grep '${escaped}'`;
}
if (options.since) {
const escaped = options.since.replace(/'/g, "'\\''");
cmd += ` --since '${escaped}'`;
}
return this.runCommandSync(cmd);
}
/**
* Clear device logs
* @returns Result message
*/
clearLogs(): string {
return this.runCommandSync("audb logs --clear --force");
}
/**
* Get detailed system information
* @returns System info output
*/
getSystemInfo(): string {
return this.runCommandSync("audb info");
}
/**
* Upload a file to the Aurora device
* @param localPath - Path to the local file
* @param remotePath - Destination path on the device
* @returns Upload result message
*/
pushFile(localPath: string, remotePath: string): string {
const output = this.runCommandSync(`audb push ${localPath} ${remotePath}`);
return output || `Uploaded ${localPath} → ${remotePath}`;
}
/**
* Download a file from the Aurora device
* @param remotePath - Path to the remote file
* @param localPath - Optional local destination path (defaults to remote filename)
* @returns File contents as Buffer
*/
pullFile(remotePath: string, localPath?: string): Buffer {
const local = localPath || remotePath.split("/").pop() || "pulled_file";
this.runCommandSync(`audb pull ${remotePath} --output "${local}"`);
return readFileSync(local);
}
}
export const auroraClient = new AuroraClient();