chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
[bumpversion]
current_version = 0.1.7
commit = True
tag = True
tag_name = npm-computer-v{new_version}
message = Bump @trycua/computer to v{new_version}
[bumpversion:file:package.json]
search = "version": "{current_version}"
replace = "version": "{new_version}"
+6
View File
@@ -0,0 +1,6 @@
root = true
[*]
indent_size = 2
end_of_line = lf
insert_final_newline = true
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
*.log
.DS_Store
.eslintcache
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2025 Cua
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
# @trycua/computer
Computer-Use Interface (CUI) framework for interacting with local macOS and Linux sandboxes.
**[Documentation](https://cua.ai/docs/cua/reference/computer-sdk)** - Installation, guides, and configuration.
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@trycua/computer",
"version": "0.1.7",
"packageManager": "pnpm@10.11.0",
"description": "Typescript SDK for Cua computer interaction",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/trycua/cua/tree/feature/computer/typescript/libs/typescript/computer",
"bugs": {
"url": "https://github.com/trycua/cua/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/trycua/cua.git"
},
"author": "cua",
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
"typecheck": "tsc --noEmit",
"release": "bumpp && pnpm publish",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@trycua/core": "workspace:^",
"pino": "^9.7.0",
"uuid": "^11.1.1",
"ws": "^8.21.0"
},
"devDependencies": {
"@types/node": "^22.15.17",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"bumpp": "^10.1.0",
"happy-dom": "^20.8.9",
"tsdown": "^0.11.9",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
"vitest": "^4.1.0",
"vite": "^7.3.5"
}
}
@@ -0,0 +1 @@
export { BaseComputer, CloudComputer } from './providers';
@@ -0,0 +1,145 @@
import { createHash } from 'node:crypto';
import os from 'node:os';
import { Telemetry } from '@trycua/core';
import pino from 'pino';
import type { OSType } from '../../types';
import type { BaseComputerConfig, Display, VMProviderType } from '../types';
const logger = pino({ name: 'computer.provider_base' });
/**
* Hash API key using SHA256 for secure telemetry identification.
*/
function hashApiKey(apiKey: string | undefined): string | undefined {
if (!apiKey) return undefined;
return createHash('sha256').update(apiKey).digest('hex');
}
/**
* Base Computer class with shared functionality
*/
export abstract class BaseComputer {
protected name: string;
protected osType: OSType;
protected vmProvider?: VMProviderType;
protected telemetry: Telemetry;
constructor(config: BaseComputerConfig) {
this.name = config.name;
this.osType = config.osType;
this.vmProvider = config.vmProvider;
this.telemetry = new Telemetry();
this.telemetry.recordEvent('module_init', {
module: 'computer',
version: process.env.npm_package_version,
node_version: process.version,
});
this.telemetry.recordEvent('computer_initialized', {
os: os.platform(),
os_version: os.version(),
node_version: process.version,
provider_type: config.vmProvider ?? 'unknown',
});
// Track which config args were provided
const argsProvided: string[] = ['name', 'osType']; // required args
const apiKey = 'apiKey' in config ? (config as { apiKey?: string }).apiKey : undefined;
if (apiKey) argsProvided.push('apiKey');
if ('apiBase' in config) argsProvided.push('apiBase');
if (config.vmProvider) argsProvided.push('vmProvider');
const initEventData: Record<string, unknown> = {
os_type: config.osType,
args_provided: argsProvided,
provider_type: config.vmProvider ?? 'unknown',
};
// Add hashed API key
if (apiKey) {
initEventData.api_key_hash = hashApiKey(apiKey);
}
this.telemetry.recordEvent('ts_computer_init', initEventData);
}
/**
* Get the name of the computer
*/
getName(): string {
return this.name;
}
/**
* Get the OS type of the computer
*/
getOSType(): OSType {
return this.osType;
}
/**
* Get the VM provider type
*/
getVMProviderType(): VMProviderType | undefined {
return this.vmProvider;
}
/**
* Shared method available to all computer types
*/
async disconnect(): Promise<void> {
logger.info(`Disconnecting from ${this.name}`);
// Implementation would go here
}
/**
* Parse display string into Display object
* @param display Display string in format "WIDTHxHEIGHT"
* @returns Display object
*/
public static parseDisplayString(display: string): Display {
const match = display.match(/^(\d+)x(\d+)$/);
if (!match) {
throw new Error(`Invalid display format: ${display}. Expected format: WIDTHxHEIGHT`);
}
return {
width: Number.parseInt(match[1], 10),
height: Number.parseInt(match[2], 10),
};
}
/**
* Parse memory string to MB integer.
*
* Examples:
* "8GB" -> 8192
* "1024MB" -> 1024
* "512" -> 512
*
* @param memoryStr - Memory string to parse
* @returns Memory value in MB
*/
public static parseMemoryString(memoryStr: string): number {
if (!memoryStr) {
return 0;
}
// Convert to uppercase for case-insensitive matching
const upperStr = memoryStr.toUpperCase().trim();
// Extract numeric value and unit
const match = upperStr.match(/^(\d+(?:\.\d+)?)\s*(GB|MB)?$/);
if (!match) {
throw new Error(`Invalid memory format: ${memoryStr}`);
}
const value = Number.parseFloat(match[1]);
const unit = match[2] || 'MB'; // Default to MB if no unit specified
// Convert to MB
if (unit === 'GB') {
return Math.round(value * 1024);
}
return Math.round(value);
}
}
@@ -0,0 +1,179 @@
import { createHash } from 'node:crypto';
import pino from 'pino';
import { v4 as uuidv4 } from 'uuid';
import { cuaVersionHeaders } from '@trycua/core';
import { type BaseComputerInterface, InterfaceFactory } from '../../interface/index';
import type { CloudComputerConfig, VMProviderType } from '../types';
import { BaseComputer } from './base';
/**
* Hash API key using SHA256 for secure telemetry identification.
*/
function hashApiKey(apiKey: string): string {
return createHash('sha256').update(apiKey).digest('hex');
}
const DEFAULT_API_BASE = process.env.CUA_API_BASE || 'https://api.cua.ai';
interface VMInfo {
name: string;
host?: string;
status?: string;
}
/**
* Cloud-specific computer implementation
*/
export class CloudComputer extends BaseComputer {
protected static vmProviderType: VMProviderType.CLOUD;
protected apiKey: string;
private iface?: BaseComputerInterface;
private initialized = false;
private cachedHost?: string;
private apiBase: string;
// Session tracking
private sessionId: string;
private sessionStartTime?: number;
private apiKeyHash: string;
protected logger = pino({ name: 'computer.provider_cloud' });
constructor(config: CloudComputerConfig) {
super(config);
this.apiKey = config.apiKey;
this.apiBase = DEFAULT_API_BASE;
this.sessionId = uuidv4();
this.apiKeyHash = hashApiKey(config.apiKey);
}
/**
* Get the host for this VM.
* Returns cached host if available, otherwise falls back to default format.
*/
get ip(): string {
return this.cachedHost || `${this.name}.sandbox.cua.ai`;
}
/**
* Fetch VM list from API and cache the host for this VM.
*/
private async fetchAndCacheHost(): Promise<string> {
try {
const response = await fetch(`${this.apiBase}/v1/vms`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
Accept: 'application/json',
...cuaVersionHeaders(
'computer',
typeof __CUA_VERSION__ !== 'undefined' ? __CUA_VERSION__ : ''
),
},
});
if (response.ok) {
const vms = (await response.json()) as VMInfo[];
const vm = vms.find((v) => v.name === this.name);
if (vm?.host) {
this.cachedHost = vm.host;
this.logger.info(`Cached host from API: ${this.cachedHost}`);
return this.cachedHost;
}
}
} catch (error) {
this.logger.warn(`Failed to fetch VM list for host lookup: ${error}`);
}
// Fall back to default format
const fallbackHost = `${this.name}.sandbox.cua.ai`;
this.cachedHost = fallbackHost;
this.logger.info(`Using fallback host: ${fallbackHost}`);
return fallbackHost;
}
/**
* Initialize the cloud VM and interface
*/
async run(): Promise<void> {
if (this.initialized) {
this.logger.info('Computer already initialized, skipping initialization');
return;
}
try {
// Fetch the host from API before connecting
const ipAddress = await this.fetchAndCacheHost();
this.logger.info(`Connecting to cloud VM at ${ipAddress}`);
// Create the interface with API key authentication
this.iface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress,
this.apiKey,
this.name
);
// Wait for the interface to be ready
this.logger.info('Waiting for interface to be ready...');
await this.iface.waitForReady();
this.initialized = true;
this.sessionStartTime = Date.now();
// Track session start with hashed API key
this.telemetry.recordEvent('ts_session_start', {
session_id: this.sessionId,
os_type: this.osType,
connection_type: 'cloud',
api_key_hash: this.apiKeyHash,
});
this.logger.info('Cloud computer ready');
} catch (error) {
this.logger.error(`Failed to initialize cloud computer: ${error}`);
throw new Error(`Failed to initialize cloud computer: ${error}`);
}
}
/**
* Stop the cloud computer (disconnect interface)
*/
async stop(): Promise<void> {
this.logger.info('Disconnecting from cloud computer...');
// Track session end with hashed API key
if (this.sessionStartTime) {
const durationSeconds = (Date.now() - this.sessionStartTime) / 1000;
this.telemetry.recordEvent('ts_session_end', {
session_id: this.sessionId,
duration_seconds: durationSeconds,
api_key_hash: this.apiKeyHash,
});
}
if (this.iface) {
this.iface.disconnect();
this.iface = undefined;
}
this.initialized = false;
this.logger.info('Disconnected from cloud computer');
}
/**
* Get the computer interface
*/
get interface(): BaseComputerInterface {
if (!this.iface) {
throw new Error('Computer not initialized. Call run() first.');
}
return this.iface;
}
/**
* Disconnect from the cloud computer
*/
async disconnect(): Promise<void> {
await this.stop();
}
}
@@ -0,0 +1,2 @@
export * from './base';
export * from './cloud';
@@ -0,0 +1,46 @@
import type { OSType, ScreenSize } from '../types';
/**
* Display configuration for the computer.
*/
export interface Display extends ScreenSize {
scale_factor?: number;
}
/**
* Computer configuration model.
*/
export interface BaseComputerConfig {
/**
* The VM name
* @default ""
*/
name: string;
/**
* The operating system type ('macos', 'windows', or 'linux')
* @default "macos"
*/
osType: OSType;
/**
* The VM provider type
*/
vmProvider?: VMProviderType;
}
export interface CloudComputerConfig extends BaseComputerConfig {
/**
* Optional API key for cloud providers
*/
apiKey: string;
}
export enum VMProviderType {
DOCKER = 'docker',
LUME = 'lume',
CLOUD = 'cloud',
QEMU = 'qemu',
WINDOWS_SANDBOX = 'windows-sandbox',
HOST = 'host',
}
+2
View File
@@ -0,0 +1,2 @@
/** Build-time constant injected by tsdown `define`. */
declare const __CUA_VERSION__: string;
+5
View File
@@ -0,0 +1,5 @@
// Export classes
export { CloudComputer as Computer } from './computer';
export { OSType } from './types';
export { VMProviderType as ProviderType } from './computer/types';
@@ -0,0 +1,390 @@
/**
* Base interface for computer control.
*/
import pino from 'pino';
import { v4 as uuidv4 } from 'uuid';
import WebSocket from 'ws';
import { Telemetry } from '@trycua/core';
import type { ScreenSize } from '../types';
export type MouseButton = 'left' | 'middle' | 'right';
export interface CursorPosition {
x: number;
y: number;
}
export interface AccessibilityNode {
role: string;
title?: string;
value?: string;
description?: string;
bounds?: {
x: number;
y: number;
width: number;
height: number;
};
children?: AccessibilityNode[];
}
/**
* Base class for computer control interfaces.
*/
export abstract class BaseComputerInterface {
protected ipAddress: string;
protected username: string;
protected password: string;
protected closed = false;
protected commandLock: Promise<unknown> = Promise.resolve();
protected ws: WebSocket;
protected apiKey?: string;
protected vmName?: string;
protected logger = pino({ name: 'computer.interface-base' });
// Telemetry
protected telemetry: Telemetry;
protected sessionId: string;
constructor(
ipAddress: string,
username = 'lume',
password = 'lume',
apiKey?: string,
vmName?: string
) {
this.ipAddress = ipAddress;
this.username = username;
this.password = password;
this.apiKey = apiKey;
this.vmName = vmName;
// Initialize telemetry
this.telemetry = new Telemetry();
this.sessionId = uuidv4();
// Initialize WebSocket with headers if needed
const headers: { [key: string]: string } = {};
if (this.apiKey && this.vmName) {
headers['X-API-Key'] = this.apiKey;
headers['X-VM-Name'] = this.vmName;
}
// Create the WebSocket instance
this.ws = this.createWebSocket(headers);
}
private createWebSocket(headers: { [key: string]: string }): WebSocket {
const ws = new WebSocket(this.wsUri, { headers });
// Constructors open immediately, and some tests only verify inheritance
// without ever calling connect(). Keep those failed background attempts
// from surfacing as unhandled process errors; connect() still attaches its
// own one-shot error listener and rejects active connection attempts.
ws.on('error', (error: Error) => {
this.logger.debug(`WebSocket background error: ${error.message}`);
});
return ws;
}
/**
* Get the WebSocket URI for connection.
* Subclasses can override this to customize the URI.
*/
protected get wsUri(): string {
const protocol = this.apiKey ? 'wss' : 'ws';
// Check if ipAddress already includes a port
if (this.ipAddress.includes(':')) {
return `${protocol}://${this.ipAddress}/ws`;
}
// Otherwise, append the default port
const port = this.apiKey ? '8443' : '8000';
return `${protocol}://${this.ipAddress}:${port}/ws`;
}
/**
* Wait for interface to be ready.
* @param timeout Maximum time to wait in seconds
* @throws Error if interface is not ready within timeout
*/
async waitForReady(timeout = 60): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout * 1000) {
try {
await this.connect();
return;
} catch (error) {
console.log(error);
// Wait a bit before retrying
this.logger.error(`Error connecting to websocket: ${JSON.stringify(error)}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
throw new Error(`Interface not ready after ${timeout} seconds`);
}
/**
* Authenticate with the WebSocket server.
* This should be called immediately after the WebSocket connection is established.
*/
private async authenticate(): Promise<void> {
if (!this.apiKey || !this.vmName) {
// No authentication needed
return;
}
this.logger.info('Performing authentication handshake...');
const authMessage = {
command: 'authenticate',
params: {
api_key: this.apiKey,
container_name: this.vmName,
},
};
return new Promise<void>((resolve, reject) => {
const authHandler = (data: WebSocket.RawData) => {
try {
const authResult = JSON.parse(data.toString());
if (!authResult.success) {
const errorMsg = authResult.error || 'Authentication failed';
this.logger.error(`Authentication failed: ${errorMsg}`);
this.ws.close();
reject(new Error(`Authentication failed: ${errorMsg}`));
} else {
this.logger.info('Authentication successful');
this.ws.off('message', authHandler);
resolve();
}
} catch (error) {
this.ws.off('message', authHandler);
reject(error);
}
};
this.ws.on('message', authHandler);
this.ws.send(JSON.stringify(authMessage));
});
}
/**
* Connect to the WebSocket server.
*/
public async connect(): Promise<void> {
// If the WebSocket is already open, check if we need to authenticate
if (this.ws.readyState === WebSocket.OPEN) {
this.logger.info('Websocket is open, ensuring authentication is complete.');
this.closed = false;
return this.authenticate();
}
// If the WebSocket is closed or closing, reinitialize it
if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
this.logger.info('Websocket is closed. Reinitializing connection.');
const headers: { [key: string]: string } = {};
if (this.apiKey && this.vmName) {
headers['X-API-Key'] = this.apiKey;
headers['X-VM-Name'] = this.vmName;
}
this.ws = this.createWebSocket(headers);
}
// Connect and authenticate
return new Promise((resolve, reject) => {
const onOpen = async () => {
try {
this.closed = false;
// Always authenticate immediately after connection
await this.authenticate();
resolve();
} catch (error) {
reject(error);
}
};
// If already connecting, wait for it to complete then authenticate
if (this.ws.readyState === WebSocket.CONNECTING) {
this.ws.addEventListener('open', onOpen, { once: true });
this.ws.addEventListener('error', (error) => reject(error), {
once: true,
});
return;
}
// Set up event handlers
this.ws.on('open', onOpen);
this.ws.on('error', (error: Error) => {
reject(error);
});
this.ws.on('close', () => {
if (!this.closed) {
// Attempt to reconnect
setTimeout(() => {
this.connect().catch((error) => {
this.logger.error(`Error reconnecting to websocket: ${JSON.stringify(error)}`);
});
}, 1000);
}
});
});
}
/**
* Send a command to the WebSocket server.
*/
public async sendCommand(
command: string,
params: { [key: string]: unknown } = {}
): Promise<{ [key: string]: unknown }> {
// Track action execution
this.telemetry.recordEvent('ts_action_executed', {
action_type: command,
session_id: this.sessionId,
});
// Create a new promise for this specific command
const commandPromise = new Promise<{ [key: string]: unknown }>((resolve, reject) => {
// Chain it to the previous commands
const executeCommand = async (): Promise<{
[key: string]: unknown;
}> => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
await this.connect();
}
return new Promise<{ [key: string]: unknown }>((innerResolve, innerReject) => {
const messageHandler = (data: WebSocket.RawData) => {
try {
const response = JSON.parse(data.toString());
if (response.error) {
innerReject(new Error(response.error));
} else {
innerResolve(response);
}
} catch (error) {
innerReject(error);
}
this.ws.off('message', messageHandler);
};
this.ws.on('message', messageHandler);
const wsCommand = { command, params };
this.ws.send(JSON.stringify(wsCommand));
});
};
// Add this command to the lock chain
this.commandLock = this.commandLock.then(() => executeCommand().then(resolve, reject));
});
return commandPromise;
}
/**
* Check if the WebSocket is connected.
*/
public isConnected(): boolean {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
/**
* Close the interface connection.
*/
disconnect(): void {
this.closed = true;
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
} else if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
// If still connecting, terminate the connection attempt
this.ws.terminate();
}
}
/**
* Force close the interface connection.
* By default, this just calls close(), but subclasses can override
* to provide more forceful cleanup.
*/
forceClose(): void {
this.disconnect();
}
// Mouse Actions
abstract mouseDown(x?: number, y?: number, button?: MouseButton): Promise<void>;
abstract mouseUp(x?: number, y?: number, button?: MouseButton): Promise<void>;
abstract leftClick(x?: number, y?: number): Promise<void>;
abstract rightClick(x?: number, y?: number): Promise<void>;
abstract doubleClick(x?: number, y?: number): Promise<void>;
abstract moveCursor(x: number, y: number): Promise<void>;
abstract dragTo(x: number, y: number, button?: MouseButton, duration?: number): Promise<void>;
abstract drag(
path: Array<[number, number]>,
button?: MouseButton,
duration?: number
): Promise<void>;
// Keyboard Actions
abstract keyDown(key: string): Promise<void>;
abstract keyUp(key: string): Promise<void>;
abstract typeText(text: string): Promise<void>;
abstract pressKey(key: string): Promise<void>;
abstract hotkey(...keys: string[]): Promise<void>;
// Scrolling Actions
abstract scroll(x: number, y: number): Promise<void>;
abstract scrollDown(clicks?: number): Promise<void>;
abstract scrollUp(clicks?: number): Promise<void>;
// Screen Actions
abstract screenshot(): Promise<Buffer>;
abstract getScreenSize(): Promise<ScreenSize>;
abstract getCursorPosition(): Promise<CursorPosition>;
// Window Management
abstract open(target: string): Promise<void>;
abstract launch(app: string, args?: string[]): Promise<number | undefined>;
abstract getCurrentWindowId(): Promise<number | string>;
abstract getApplicationWindows(app: string): Promise<Array<number | string>>;
abstract getWindowName(windowId: number | string): Promise<string>;
abstract getWindowSize(windowId: number | string): Promise<[number, number]>;
abstract getWindowPosition(windowId: number | string): Promise<[number, number]>;
abstract setWindowSize(windowId: number | string, width: number, height: number): Promise<void>;
abstract setWindowPosition(windowId: number | string, x: number, y: number): Promise<void>;
abstract maximizeWindow(windowId: number | string): Promise<void>;
abstract minimizeWindow(windowId: number | string): Promise<void>;
abstract activateWindow(windowId: number | string): Promise<void>;
abstract closeWindow(windowId: number | string): Promise<void>;
// Desktop Actions
abstract getDesktopEnvironment(): Promise<string>;
abstract setWallpaper(path: string): Promise<void>;
// Clipboard Actions
abstract copyToClipboard(): Promise<string>;
abstract setClipboard(text: string): Promise<void>;
// File System Actions
abstract fileExists(path: string): Promise<boolean>;
abstract directoryExists(path: string): Promise<boolean>;
abstract listDir(path: string): Promise<string[]>;
abstract readText(path: string): Promise<string>;
abstract writeText(path: string, content: string): Promise<void>;
abstract readBytes(path: string): Promise<Buffer>;
abstract writeBytes(path: string, content: Buffer): Promise<void>;
abstract deleteFile(path: string): Promise<void>;
abstract createDir(path: string): Promise<void>;
abstract deleteDir(path: string): Promise<void>;
abstract runCommand(command: string): Promise<[string, string]>;
// Accessibility Actions
abstract getAccessibilityTree(): Promise<AccessibilityNode>;
abstract toScreenCoordinates(x: number, y: number): Promise<[number, number]>;
abstract toScreenshotCoordinates(x: number, y: number): Promise<[number, number]>;
}
@@ -0,0 +1,39 @@
/**
* Factory for creating computer interfaces.
*/
import type { OSType } from '../types';
import type { BaseComputerInterface } from './base';
import { LinuxComputerInterface } from './linux';
import { MacOSComputerInterface } from './macos';
import { WindowsComputerInterface } from './windows';
export const InterfaceFactory = {
/**
* Create an interface for the specified OS.
*
* @param os Operating system type ('macos', 'linux', or 'windows')
* @param ipAddress IP address of the computer to control
* @param apiKey Optional API key for cloud authentication
* @param vmName Optional VM name for cloud authentication
* @returns The appropriate interface for the OS
* @throws Error if the OS type is not supported
*/
createInterfaceForOS(
os: OSType,
ipAddress: string,
apiKey?: string,
vmName?: string
): BaseComputerInterface {
switch (os) {
case 'macos':
return new MacOSComputerInterface(ipAddress, 'lume', 'lume', apiKey, vmName);
case 'linux':
return new LinuxComputerInterface(ipAddress, 'lume', 'lume', apiKey, vmName);
case 'windows':
return new WindowsComputerInterface(ipAddress, 'lume', 'lume', apiKey, vmName);
default:
throw new Error(`Unsupported OS type: ${os}`);
}
},
};
@@ -0,0 +1,6 @@
export { BaseComputerInterface } from './base';
export type { MouseButton, CursorPosition, AccessibilityNode } from './base';
export { InterfaceFactory } from './factory';
export { MacOSComputerInterface } from './macos';
export { LinuxComputerInterface } from './linux';
export { WindowsComputerInterface } from './windows';
@@ -0,0 +1,14 @@
/**
* Linux computer interface implementation.
*/
import { MacOSComputerInterface } from './macos';
/**
* Linux interface implementation.
* Since the cloud provider uses the same WebSocket protocol for all OS types,
* we can reuse the macOS implementation.
*/
export class LinuxComputerInterface extends MacOSComputerInterface {
// Linux uses the same WebSocket interface as macOS for cloud provider
}
@@ -0,0 +1,696 @@
/**
* macOS computer interface implementation.
*/
import type { ScreenSize } from '../types';
import type { AccessibilityNode, CursorPosition, MouseButton } from './base';
import { BaseComputerInterface } from './base';
export class MacOSComputerInterface extends BaseComputerInterface {
// Mouse Actions
/**
* Press and hold a mouse button at the specified coordinates.
* @param {number} [x] - X coordinate for the mouse action
* @param {number} [y] - Y coordinate for the mouse action
* @param {MouseButton} [button='left'] - Mouse button to press down
* @returns {Promise<void>}
*/
async mouseDown(x?: number, y?: number, button: MouseButton = 'left'): Promise<void> {
await this.sendCommand('mouse_down', { x, y, button });
}
/**
* Release a mouse button at the specified coordinates.
* @param {number} [x] - X coordinate for the mouse action
* @param {number} [y] - Y coordinate for the mouse action
* @param {MouseButton} [button='left'] - Mouse button to release
* @returns {Promise<void>}
*/
async mouseUp(x?: number, y?: number, button: MouseButton = 'left'): Promise<void> {
await this.sendCommand('mouse_up', { x, y, button });
}
/**
* Perform a left mouse click at the specified coordinates.
* @param {number} [x] - X coordinate for the click
* @param {number} [y] - Y coordinate for the click
* @returns {Promise<void>}
*/
async leftClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('left_click', { x, y });
}
/**
* Perform a right mouse click at the specified coordinates.
* @param {number} [x] - X coordinate for the click
* @param {number} [y] - Y coordinate for the click
* @returns {Promise<void>}
*/
async rightClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('right_click', { x, y });
}
/**
* Perform a double click at the specified coordinates.
* @param {number} [x] - X coordinate for the double click
* @param {number} [y] - Y coordinate for the double click
* @returns {Promise<void>}
*/
async doubleClick(x?: number, y?: number): Promise<void> {
await this.sendCommand('double_click', { x, y });
}
/**
* Move the cursor to the specified coordinates.
* @param {number} x - X coordinate to move to
* @param {number} y - Y coordinate to move to
* @returns {Promise<void>}
*/
async moveCursor(x: number, y: number): Promise<void> {
await this.sendCommand('move_cursor', { x, y });
}
/**
* Drag from current position to the specified coordinates.
* @param {number} x - X coordinate to drag to
* @param {number} y - Y coordinate to drag to
* @param {MouseButton} [button='left'] - Mouse button to use for dragging
* @param {number} [duration=0.5] - Duration of the drag operation in seconds
* @returns {Promise<void>}
*/
async dragTo(x: number, y: number, button: MouseButton = 'left', duration = 0.5): Promise<void> {
await this.sendCommand('drag_to', { x, y, button, duration });
}
/**
* Drag along a path of coordinates.
* @param {Array<[number, number]>} path - Array of [x, y] coordinate pairs to drag through
* @param {MouseButton} [button='left'] - Mouse button to use for dragging
* @param {number} [duration=0.5] - Duration of the drag operation in seconds
* @returns {Promise<void>}
*/
async drag(
path: Array<[number, number]>,
button: MouseButton = 'left',
duration = 0.5
): Promise<void> {
await this.sendCommand('drag', { path, button, duration });
}
// Keyboard Actions
/**
* Press and hold a key.
* @param {string} key - Key to press down
* @returns {Promise<void>}
*/
async keyDown(key: string): Promise<void> {
await this.sendCommand('key_down', { key });
}
/**
* Release a key.
* @param {string} key - Key to release
* @returns {Promise<void>}
*/
async keyUp(key: string): Promise<void> {
await this.sendCommand('key_up', { key });
}
/**
* Type text as if entered from keyboard.
* @param {string} text - Text to type
* @returns {Promise<void>}
*/
async typeText(text: string): Promise<void> {
await this.sendCommand('type_text', { text });
}
/**
* Press and release a key.
* @param {string} key - Key to press
* @returns {Promise<void>}
*/
async pressKey(key: string): Promise<void> {
await this.sendCommand('press_key', { key });
}
/**
* Press multiple keys simultaneously as a hotkey combination.
* @param {...string} keys - Keys to press together
* @returns {Promise<void>}
*/
async hotkey(...keys: string[]): Promise<void> {
await this.sendCommand('hotkey', { keys });
}
// Scrolling Actions
/**
* Scroll by the specified amount in x and y directions.
* @param {number} x - Horizontal scroll amount
* @param {number} y - Vertical scroll amount
* @returns {Promise<void>}
*/
async scroll(x: number, y: number): Promise<void> {
await this.sendCommand('scroll', { x, y });
}
/**
* Scroll down by the specified number of clicks.
* @param {number} [clicks=1] - Number of scroll clicks
* @returns {Promise<void>}
*/
async scrollDown(clicks = 1): Promise<void> {
await this.sendCommand('scroll_down', { clicks });
}
/**
* Scroll up by the specified number of clicks.
* @param {number} [clicks=1] - Number of scroll clicks
* @returns {Promise<void>}
*/
async scrollUp(clicks = 1): Promise<void> {
await this.sendCommand('scroll_up', { clicks });
}
// Screen Actions
/**
* Take a screenshot of the screen.
* @returns {Promise<Buffer>} Screenshot image data as a Buffer
* @throws {Error} If screenshot fails
*/
async screenshot(): Promise<Buffer> {
const response = await this.sendCommand('screenshot');
if (!response.image_data) {
throw new Error('Failed to take screenshot');
}
return Buffer.from(response.image_data as string, 'base64');
}
/**
* Get the current screen size.
* @returns {Promise<ScreenSize>} Screen dimensions
* @throws {Error} If unable to get screen size
*/
async getScreenSize(): Promise<ScreenSize> {
const response = await this.sendCommand('get_screen_size');
if (!response.success || !response.size) {
throw new Error('Failed to get screen size');
}
return response.size as ScreenSize;
}
/**
* Get the current cursor position.
* @returns {Promise<CursorPosition>} Current cursor coordinates
* @throws {Error} If unable to get cursor position
*/
async getCursorPosition(): Promise<CursorPosition> {
const response = await this.sendCommand('get_cursor_position');
if (!response.success || !response.position) {
throw new Error('Failed to get cursor position');
}
return response.position as CursorPosition;
}
// Window Management
/** Open a file path or URL with the default handler. */
async open(target: string): Promise<void> {
const response = await this.sendCommand('open', { target });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to open target');
}
}
/** Launch an application (string may include args). Returns pid if available. */
async launch(app: string, args?: string[]): Promise<number | undefined> {
const response = await this.sendCommand('launch', args ? { app, args } : { app });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to launch application');
}
return (response.pid as number) || undefined;
}
/** Get the current active window id. */
async getCurrentWindowId(): Promise<number | string> {
const response = await this.sendCommand('get_current_window_id');
if (!response.success || response.window_id === undefined) {
throw new Error((response.error as string) || 'Failed to get current window id');
}
return response.window_id as number | string;
}
/** Get windows belonging to an application (by name). */
async getApplicationWindows(app: string): Promise<Array<number | string>> {
const response = await this.sendCommand('get_application_windows', { app });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get application windows');
}
return (response.windows as Array<number | string>) || [];
}
/** Get window title/name by id. */
async getWindowName(windowId: number | string): Promise<string> {
const response = await this.sendCommand('get_window_name', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window name');
}
return (response.name as string) || '';
}
/** Get window size as [width, height]. */
async getWindowSize(windowId: number | string): Promise<[number, number]> {
const response = await this.sendCommand('get_window_size', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window size');
}
return [Number(response.width) || 0, Number(response.height) || 0];
}
/** Get window position as [x, y]. */
async getWindowPosition(windowId: number | string): Promise<[number, number]> {
const response = await this.sendCommand('get_window_position', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get window position');
}
return [Number(response.x) || 0, Number(response.y) || 0];
}
/** Set window size. */
async setWindowSize(windowId: number | string, width: number, height: number): Promise<void> {
const response = await this.sendCommand('set_window_size', {
window_id: windowId,
width,
height,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set window size');
}
}
/** Set window position. */
async setWindowPosition(windowId: number | string, x: number, y: number): Promise<void> {
const response = await this.sendCommand('set_window_position', {
window_id: windowId,
x,
y,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set window position');
}
}
/** Maximize a window. */
async maximizeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('maximize_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to maximize window');
}
}
/** Minimize a window. */
async minimizeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('minimize_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to minimize window');
}
}
/** Activate a window by id. */
async activateWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('activate_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to activate window');
}
}
/** Close a window by id. */
async closeWindow(windowId: number | string): Promise<void> {
const response = await this.sendCommand('close_window', { window_id: windowId });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to close window');
}
}
// Desktop Actions
/**
* Get the current desktop environment string (e.g., 'xfce4', 'gnome', 'kde', 'mac', 'windows').
*/
async getDesktopEnvironment(): Promise<string> {
const response = await this.sendCommand('get_desktop_environment');
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get desktop environment');
}
return (response.environment as string) || 'unknown';
}
/**
* Set the desktop wallpaper image.
* @param path Absolute path to the image file on the VM
*/
async setWallpaper(path: string): Promise<void> {
const response = await this.sendCommand('set_wallpaper', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to set wallpaper');
}
}
// Clipboard Actions
/**
* Copy current selection to clipboard and return the content.
* @returns {Promise<string>} Clipboard content
* @throws {Error} If unable to get clipboard content
*/
async copyToClipboard(): Promise<string> {
const response = await this.sendCommand('copy_to_clipboard');
if (!response.success || !response.content) {
throw new Error('Failed to get clipboard content');
}
return response.content as string;
}
/**
* Set the clipboard content to the specified text.
* @param {string} text - Text to set in clipboard
* @returns {Promise<void>}
*/
async setClipboard(text: string): Promise<void> {
await this.sendCommand('set_clipboard', { text });
}
// File System Actions
/**
* Check if a file exists at the specified path.
* @param {string} path - Path to the file
* @returns {Promise<boolean>} True if file exists, false otherwise
*/
async fileExists(path: string): Promise<boolean> {
const response = await this.sendCommand('file_exists', { path });
return (response.exists as boolean) || false;
}
/**
* Check if a directory exists at the specified path.
* @param {string} path - Path to the directory
* @returns {Promise<boolean>} True if directory exists, false otherwise
*/
async directoryExists(path: string): Promise<boolean> {
const response = await this.sendCommand('directory_exists', { path });
return (response.exists as boolean) || false;
}
/**
* List the contents of a directory.
* @param {string} path - Path to the directory
* @returns {Promise<string[]>} Array of file and directory names
* @throws {Error} If unable to list directory
*/
async listDir(path: string): Promise<string[]> {
const response = await this.sendCommand('list_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to list directory');
}
return (response.files as string[]) || [];
}
/**
* Get the size of a file in bytes.
* @param {string} path - Path to the file
* @returns {Promise<number>} File size in bytes
* @throws {Error} If unable to get file size
*/
async getFileSize(path: string): Promise<number> {
const response = await this.sendCommand('get_file_size', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get file size');
}
return (response.size as number) || 0;
}
/**
* Read file content in chunks for large files.
* @private
* @param {string} path - Path to the file
* @param {number} offset - Starting byte offset
* @param {number} totalLength - Total number of bytes to read
* @param {number} [chunkSize=1048576] - Size of each chunk in bytes
* @returns {Promise<Buffer>} File content as Buffer
* @throws {Error} If unable to read file chunk
*/
private async readBytesChunked(
path: string,
offset: number,
totalLength: number,
chunkSize: number = 1024 * 1024
): Promise<Buffer> {
const chunks: Buffer[] = [];
let currentOffset = offset;
let remaining = totalLength;
while (remaining > 0) {
const readSize = Math.min(chunkSize, remaining);
const response = await this.sendCommand('read_bytes', {
path,
offset: currentOffset,
length: readSize,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to read file chunk');
}
const chunkData = Buffer.from(response.content_b64 as string, 'base64');
chunks.push(chunkData);
currentOffset += readSize;
remaining -= readSize;
}
return Buffer.concat(chunks);
}
/**
* Write file content in chunks for large files.
* @private
* @param {string} path - Path to the file
* @param {Buffer} content - Content to write
* @param {boolean} [append=false] - Whether to append to existing file
* @param {number} [chunkSize=1048576] - Size of each chunk in bytes
* @returns {Promise<void>}
* @throws {Error} If unable to write file chunk
*/
private async writeBytesChunked(
path: string,
content: Buffer,
append: boolean = false,
chunkSize: number = 1024 * 1024
): Promise<void> {
const totalSize = content.length;
let currentOffset = 0;
while (currentOffset < totalSize) {
const chunkEnd = Math.min(currentOffset + chunkSize, totalSize);
const chunkData = content.subarray(currentOffset, chunkEnd);
// First chunk uses the original append flag, subsequent chunks always append
const chunkAppend = currentOffset === 0 ? append : true;
const response = await this.sendCommand('write_bytes', {
path,
content_b64: chunkData.toString('base64'),
append: chunkAppend,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to write file chunk');
}
currentOffset = chunkEnd;
}
}
/**
* Read text from a file with specified encoding.
* @param {string} path - Path to the file to read
* @param {BufferEncoding} [encoding='utf8'] - Text encoding to use
* @returns {Promise<string>} The decoded text content of the file
*/
async readText(path: string, encoding: BufferEncoding = 'utf8'): Promise<string> {
const contentBytes = await this.readBytes(path);
return contentBytes.toString(encoding);
}
/**
* Write text to a file with specified encoding.
* @param {string} path - Path to the file to write
* @param {string} content - Text content to write
* @param {BufferEncoding} [encoding='utf8'] - Text encoding to use
* @param {boolean} [append=false] - Whether to append to the file instead of overwriting
* @returns {Promise<void>}
*/
async writeText(
path: string,
content: string,
encoding: BufferEncoding = 'utf8',
append: boolean = false
): Promise<void> {
const contentBytes = Buffer.from(content, encoding);
await this.writeBytes(path, contentBytes, append);
}
/**
* Read bytes from a file, with optional offset and length.
* @param {string} path - Path to the file
* @param {number} [offset=0] - Starting byte offset
* @param {number} [length] - Number of bytes to read (reads entire file if not specified)
* @returns {Promise<Buffer>} File content as Buffer
* @throws {Error} If unable to read file
*/
async readBytes(path: string, offset: number = 0, length?: number): Promise<Buffer> {
// For large files, use chunked reading
if (length === undefined) {
// Get file size first to determine if we need chunking
const fileSize = await this.getFileSize(path);
// If file is larger than 5MB, read in chunks
if (fileSize > 5 * 1024 * 1024) {
const readLength = offset > 0 ? fileSize - offset : fileSize;
return await this.readBytesChunked(path, offset, readLength);
}
}
const response = await this.sendCommand('read_bytes', {
path,
offset,
length,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to read file');
}
return Buffer.from(response.content_b64 as string, 'base64');
}
/**
* Write bytes to a file.
* @param {string} path - Path to the file
* @param {Buffer} content - Content to write as Buffer
* @param {boolean} [append=false] - Whether to append to existing file
* @returns {Promise<void>}
* @throws {Error} If unable to write file
*/
async writeBytes(path: string, content: Buffer, append: boolean = false): Promise<void> {
// For large files, use chunked writing
if (content.length > 5 * 1024 * 1024) {
// 5MB threshold
await this.writeBytesChunked(path, content, append);
return;
}
const response = await this.sendCommand('write_bytes', {
path,
content_b64: content.toString('base64'),
append,
});
if (!response.success) {
throw new Error((response.error as string) || 'Failed to write file');
}
}
/**
* Delete a file at the specified path.
* @param {string} path - Path to the file to delete
* @returns {Promise<void>}
* @throws {Error} If unable to delete file
*/
async deleteFile(path: string): Promise<void> {
const response = await this.sendCommand('delete_file', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to delete file');
}
}
/**
* Create a directory at the specified path.
* @param {string} path - Path where to create the directory
* @returns {Promise<void>}
* @throws {Error} If unable to create directory
*/
async createDir(path: string): Promise<void> {
const response = await this.sendCommand('create_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to create directory');
}
}
/**
* Delete a directory at the specified path.
* @param {string} path - Path to the directory to delete
* @returns {Promise<void>}
* @throws {Error} If unable to delete directory
*/
async deleteDir(path: string): Promise<void> {
const response = await this.sendCommand('delete_dir', { path });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to delete directory');
}
}
/**
* Execute a shell command and return stdout and stderr.
* @param {string} command - Command to execute
* @returns {Promise<[string, string]>} Tuple of [stdout, stderr]
* @throws {Error} If command execution fails
*/
async runCommand(command: string): Promise<[string, string]> {
const response = await this.sendCommand('run_command', { command });
if (!response.success) {
throw new Error((response.error as string) || 'Failed to run command');
}
return [(response.stdout as string) || '', (response.stderr as string) || ''];
}
// Accessibility Actions
/**
* Get the accessibility tree of the current screen.
* @returns {Promise<AccessibilityNode>} Root accessibility node
* @throws {Error} If unable to get accessibility tree
*/
async getAccessibilityTree(): Promise<AccessibilityNode> {
const response = await this.sendCommand('get_accessibility_tree');
if (!response.success) {
throw new Error((response.error as string) || 'Failed to get accessibility tree');
}
return response as unknown as AccessibilityNode;
}
/**
* Convert coordinates to screen coordinates.
* @param {number} x - X coordinate to convert
* @param {number} y - Y coordinate to convert
* @returns {Promise<[number, number]>} Converted screen coordinates as [x, y]
* @throws {Error} If coordinate conversion fails
*/
async toScreenCoordinates(x: number, y: number): Promise<[number, number]> {
const response = await this.sendCommand('to_screen_coordinates', { x, y });
if (!response.success || !response.coordinates) {
throw new Error('Failed to convert to screen coordinates');
}
return response.coordinates as [number, number];
}
/**
* Convert coordinates to screenshot coordinates.
* @param {number} x - X coordinate to convert
* @param {number} y - Y coordinate to convert
* @returns {Promise<[number, number]>} Converted screenshot coordinates as [x, y]
* @throws {Error} If coordinate conversion fails
*/
async toScreenshotCoordinates(x: number, y: number): Promise<[number, number]> {
const response = await this.sendCommand('to_screenshot_coordinates', {
x,
y,
});
if (!response.success || !response.coordinates) {
throw new Error('Failed to convert to screenshot coordinates');
}
return response.coordinates as [number, number];
}
}
@@ -0,0 +1,14 @@
/**
* Windows computer interface implementation.
*/
import { MacOSComputerInterface } from './macos';
/**
* Windows interface implementation.
* Since the cloud provider uses the same WebSocket protocol for all OS types,
* we can reuse the macOS implementation.
*/
export class WindowsComputerInterface extends MacOSComputerInterface {
// Windows uses the same WebSocket interface as macOS for cloud provider
}
+10
View File
@@ -0,0 +1,10 @@
export enum OSType {
MACOS = 'macos',
WINDOWS = 'windows',
LINUX = 'linux',
}
export interface ScreenSize {
width: number;
height: number;
}
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { Computer } from '../../src';
import { OSType } from '../../src/types';
describe('Computer Cloud', () => {
it('Should create computer instance', () => {
const cloud = new Computer({
apiKey: 'asdf',
name: 's-linux-1234',
osType: OSType.LINUX,
});
expect(cloud).toBeInstanceOf(Computer);
});
});
@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest';
import { InterfaceFactory } from '../../src/interface/factory.ts';
import { LinuxComputerInterface } from '../../src/interface/linux.ts';
import { MacOSComputerInterface } from '../../src/interface/macos.ts';
import { WindowsComputerInterface } from '../../src/interface/windows.ts';
import { OSType } from '../../src/types.ts';
describe('InterfaceFactory', () => {
const testParams = {
ipAddress: '192.168.1.100',
username: 'testuser',
password: 'testpass',
apiKey: 'test-api-key',
vmName: 'test-vm',
};
describe('createInterfaceForOS', () => {
it('should create MacOSComputerInterface for macOS', () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.MACOS,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(MacOSComputerInterface);
});
it('should create LinuxComputerInterface for Linux', () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.LINUX,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(LinuxComputerInterface);
});
it('should create WindowsComputerInterface for Windows', () => {
const interface_ = InterfaceFactory.createInterfaceForOS(
OSType.WINDOWS,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
expect(interface_).toBeInstanceOf(WindowsComputerInterface);
});
it('should throw error for unsupported OS type', () => {
expect(() => {
InterfaceFactory.createInterfaceForOS(
'unsupported' as OSType,
testParams.ipAddress,
testParams.apiKey,
testParams.vmName
);
}).toThrow('Unsupported OS type: unsupported');
});
it('should create interface without API key and VM name', () => {
const interface_ = InterfaceFactory.createInterfaceForOS(OSType.MACOS, testParams.ipAddress);
expect(interface_).toBeInstanceOf(MacOSComputerInterface);
});
});
});
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import * as InterfaceExports from '../../src/interface/index.ts';
describe('Interface Module Exports', () => {
it('should export InterfaceFactory', () => {
expect(InterfaceExports.InterfaceFactory).toBeDefined();
expect(InterfaceExports.InterfaceFactory.createInterfaceForOS).toBeDefined();
});
it('should export BaseComputerInterface', () => {
expect(InterfaceExports.BaseComputerInterface).toBeDefined();
});
it('should export MacOSComputerInterface', () => {
expect(InterfaceExports.MacOSComputerInterface).toBeDefined();
});
it('should export LinuxComputerInterface', () => {
expect(InterfaceExports.LinuxComputerInterface).toBeDefined();
});
it('should export WindowsComputerInterface', () => {
expect(InterfaceExports.WindowsComputerInterface).toBeDefined();
});
it('should export all expected interfaces', () => {
const expectedExports = [
'InterfaceFactory',
'BaseComputerInterface',
'MacOSComputerInterface',
'LinuxComputerInterface',
'WindowsComputerInterface',
];
const actualExports = Object.keys(InterfaceExports);
for (const exportName of expectedExports) {
expect(actualExports).toContain(exportName);
}
});
});
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { LinuxComputerInterface } from '../../src/interface/linux.ts';
import { MacOSComputerInterface } from '../../src/interface/macos.ts';
describe('LinuxComputerInterface', () => {
const testParams = {
ipAddress: 'test.cua.com', // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable
username: 'testuser',
password: 'testpass',
apiKey: 'test-api-key',
vmName: 'test-vm',
};
describe('Inheritance', () => {
it('should extend MacOSComputerInterface', () => {
const linuxInterface = new LinuxComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
expect(linuxInterface).toBeInstanceOf(MacOSComputerInterface);
expect(linuxInterface).toBeInstanceOf(LinuxComputerInterface);
});
});
});
@@ -0,0 +1,940 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { WebSocket, WebSocketServer } from 'ws';
import { MacOSComputerInterface } from '../../src/interface/macos.ts';
describe('MacOSComputerInterface', () => {
// Define test parameters
const testParams = {
ipAddress: 'localhost',
username: 'testuser',
password: 'testpass',
// apiKey: "test-api-key", No API Key for local testing
vmName: 'test-vm',
};
// WebSocket server mock
let wss: WebSocketServer;
let serverPort: number;
let connectedClients: WebSocket[] = [];
// Track received messages for verification
interface ReceivedMessage {
action: string;
[key: string]: unknown;
}
let receivedMessages: ReceivedMessage[] = [];
// Set up WebSocket server before all tests
beforeEach(async () => {
receivedMessages = [];
connectedClients = [];
// Create WebSocket server on a random available port
wss = new WebSocketServer({ port: 0 });
serverPort = (wss.address() as { port: number }).port;
// Update test params with the actual server address
testParams.ipAddress = `localhost:${serverPort}`;
// Handle WebSocket connections
wss.on('connection', (ws) => {
connectedClients.push(ws);
// Handle incoming messages
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
receivedMessages.push(message);
// Send appropriate responses based on action
switch (message.command) {
case 'screenshot':
ws.send(
JSON.stringify({
image_data: Buffer.from('fake-screenshot-data').toString('base64'),
success: true,
})
);
break;
case 'get_screen_size':
ws.send(
JSON.stringify({
size: { width: 1920, height: 1080 },
success: true,
})
);
break;
case 'get_cursor_position':
ws.send(
JSON.stringify({
position: { x: 100, y: 200 },
success: true,
})
);
break;
case 'copy_to_clipboard':
ws.send(
JSON.stringify({
content: 'clipboard content',
success: true,
})
);
break;
case 'file_exists':
ws.send(
JSON.stringify({
exists: true,
success: true,
})
);
break;
case 'directory_exists':
ws.send(
JSON.stringify({
exists: true,
success: true,
})
);
break;
case 'list_dir':
ws.send(
JSON.stringify({
files: ['file1.txt', 'file2.txt'],
success: true,
})
);
break;
case 'read_text':
ws.send(
JSON.stringify({
content: 'file content',
success: true,
})
);
break;
case 'read_bytes':
ws.send(
JSON.stringify({
content_b64: Buffer.from(
message.params?.path === '/path/to/file.txt' ? 'file content' : 'binary content'
).toString('base64'),
success: true,
})
);
break;
case 'run_command':
ws.send(
JSON.stringify({
stdout: 'command output',
stderr: '',
success: true,
})
);
break;
case 'get_accessibility_tree':
ws.send(
JSON.stringify({
role: 'window',
title: 'Test Window',
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
children: [],
success: true,
})
);
break;
case 'to_screen_coordinates':
case 'to_screenshot_coordinates':
ws.send(
JSON.stringify({
coordinates: [message.params?.x || 0, message.params?.y || 0],
success: true,
})
);
break;
default:
// For all other actions, just send success
ws.send(JSON.stringify({ success: true }));
break;
}
} catch (error) {
ws.send(JSON.stringify({ error: (error as Error).message }));
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
});
// Clean up WebSocket server after each test
afterEach(async () => {
// Close all connected clients
for (const client of connectedClients) {
if (client.readyState === WebSocket.OPEN) {
client.close();
}
}
// Close the server
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
});
describe('Connection Management', () => {
it('should connect with proper authentication headers', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
// Verify the interface is connected
expect(macosInterface.isConnected()).toBe(true);
expect(connectedClients.length).toBe(1);
await macosInterface.disconnect();
});
it('should handle connection without API key', async () => {
// Create a separate server that doesn't check auth
const noAuthWss = new WebSocketServer({ port: 0 });
const noAuthPort = (noAuthWss.address() as { port: number }).port;
noAuthWss.on('connection', (ws) => {
ws.on('message', () => {
ws.send(JSON.stringify({ success: true }));
});
});
const macosInterface = new MacOSComputerInterface(
`localhost:${noAuthPort}`,
testParams.username,
testParams.password,
undefined,
undefined
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
await macosInterface.disconnect();
await new Promise<void>((resolve) => {
noAuthWss.close(() => resolve());
});
});
});
describe('Mouse Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send mouse_down command', async () => {
await macosInterface.mouseDown(100, 200, 'left');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'mouse_down',
params: {
x: 100,
y: 200,
button: 'left',
},
});
});
it('should send mouse_up command', async () => {
await macosInterface.mouseUp(100, 200, 'right');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'mouse_up',
params: {
x: 100,
y: 200,
button: 'right',
},
});
});
it('should send left_click command', async () => {
await macosInterface.leftClick(150, 250);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'left_click',
params: {
x: 150,
y: 250,
},
});
});
it('should send right_click command', async () => {
await macosInterface.rightClick(200, 300);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'right_click',
params: {
x: 200,
y: 300,
},
});
});
it('should send double_click command', async () => {
await macosInterface.doubleClick(250, 350);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'double_click',
params: {
x: 250,
y: 350,
},
});
});
it('should send move_cursor command', async () => {
await macosInterface.moveCursor(300, 400);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'move_cursor',
params: {
x: 300,
y: 400,
},
});
});
it('should send drag_to command', async () => {
await macosInterface.dragTo(400, 500, 'left', 1.5);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'drag_to',
params: {
x: 400,
y: 500,
button: 'left',
duration: 1.5,
},
});
});
it('should send drag command with path', async () => {
const path: Array<[number, number]> = [
[100, 100],
[200, 200],
[300, 300],
];
await macosInterface.drag(path, 'middle', 2.0);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'drag',
params: {
path: path,
button: 'middle',
duration: 2.0,
},
});
});
});
describe('Keyboard Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send key_down command', async () => {
await macosInterface.keyDown('a');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'key_down',
params: {
key: 'a',
},
});
});
it('should send key_up command', async () => {
await macosInterface.keyUp('b');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'key_up',
params: {
key: 'b',
},
});
});
it('should send type_text command', async () => {
await macosInterface.typeText('Hello, World!');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'type_text',
params: {
text: 'Hello, World!',
},
});
});
it('should send press_key command', async () => {
await macosInterface.pressKey('enter');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'press_key',
params: {
key: 'enter',
},
});
});
it('should send hotkey command', async () => {
await macosInterface.hotkey('cmd', 'c');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'hotkey',
params: {
keys: ['cmd', 'c'],
},
});
});
});
describe('Scrolling Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should send scroll command', async () => {
await macosInterface.scroll(10, -5);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll',
params: {
x: 10,
y: -5,
},
});
});
it('should send scroll_down command', async () => {
await macosInterface.scrollDown(3);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll_down',
params: {
clicks: 3,
},
});
});
it('should send scroll_up command', async () => {
await macosInterface.scrollUp(2);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'scroll_up',
params: {
clicks: 2,
},
});
});
});
describe('Screen Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should get screenshot', async () => {
const screenshot = await macosInterface.screenshot();
expect(screenshot).toBeInstanceOf(Buffer);
expect(screenshot.toString()).toBe('fake-screenshot-data');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'screenshot',
params: {},
});
});
it('should get screen size', async () => {
const size = await macosInterface.getScreenSize();
expect(size).toEqual({ width: 1920, height: 1080 });
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_screen_size',
params: {},
});
});
it('should get cursor position', async () => {
const position = await macosInterface.getCursorPosition();
expect(position).toEqual({ x: 100, y: 200 });
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_cursor_position',
params: {},
});
});
});
describe('Clipboard Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should copy to clipboard', async () => {
const text = await macosInterface.copyToClipboard();
expect(text).toBe('clipboard content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'copy_to_clipboard',
params: {},
});
});
it('should set clipboard', async () => {
await macosInterface.setClipboard('new clipboard text');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'set_clipboard',
params: {
text: 'new clipboard text',
},
});
});
});
describe('File System Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should check file exists', async () => {
const exists = await macosInterface.fileExists('/path/to/file');
expect(exists).toBe(true);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'file_exists',
params: {
path: '/path/to/file',
},
});
});
it('should check directory exists', async () => {
const exists = await macosInterface.directoryExists('/path/to/dir');
expect(exists).toBe(true);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'directory_exists',
params: {
path: '/path/to/dir',
},
});
});
it('should list directory', async () => {
const files = await macosInterface.listDir('/path/to/dir');
expect(files).toEqual(['file1.txt', 'file2.txt']);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'list_dir',
params: {
path: '/path/to/dir',
},
});
});
it('should read text file', async () => {
const content = await macosInterface.readText('/path/to/file.txt');
expect(content).toBe('file content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'read_bytes',
params: {
path: '/path/to/file.txt',
offset: 0,
},
});
});
it('should write text file', async () => {
await macosInterface.writeText('/path/to/file.txt', 'new content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'write_bytes',
params: {
path: '/path/to/file.txt',
content_b64: Buffer.from('new content').toString('base64'),
append: false,
},
});
});
it('should read binary file', async () => {
const content = await macosInterface.readBytes('/path/to/file.bin');
expect(content).toBeInstanceOf(Buffer);
expect(content.toString()).toBe('binary content');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'read_bytes',
params: {
path: '/path/to/file.bin',
offset: 0,
},
});
});
it('should write binary file', async () => {
const buffer = Buffer.from('binary data');
await macosInterface.writeBytes('/path/to/file.bin', buffer);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'write_bytes',
params: {
path: '/path/to/file.bin',
content_b64: buffer.toString('base64'),
append: false,
},
});
});
it('should delete file', async () => {
await macosInterface.deleteFile('/path/to/file');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'delete_file',
params: {
path: '/path/to/file',
},
});
});
it('should create directory', async () => {
await macosInterface.createDir('/path/to/new/dir');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'create_dir',
params: {
path: '/path/to/new/dir',
},
});
});
it('should delete directory', async () => {
await macosInterface.deleteDir('/path/to/dir');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'delete_dir',
params: {
path: '/path/to/dir',
},
});
});
it('should run command', async () => {
const [stdout, stderr] = await macosInterface.runCommand('ls -la');
expect(stdout).toBe('command output');
expect(stderr).toBe('');
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'run_command',
params: {
command: 'ls -la',
},
});
});
});
describe('Accessibility Actions', () => {
let macosInterface: MacOSComputerInterface;
beforeEach(async () => {
macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
});
afterEach(async () => {
if (macosInterface) {
await macosInterface.disconnect();
}
});
it('should get accessibility tree', async () => {
const tree = await macosInterface.getAccessibilityTree();
expect(tree).toEqual({
role: 'window',
title: 'Test Window',
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
children: [],
success: true,
});
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'get_accessibility_tree',
params: {},
});
});
it('should convert to screen coordinates', async () => {
const [x, y] = await macosInterface.toScreenCoordinates(100, 200);
expect(x).toBe(100);
expect(y).toBe(200);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'to_screen_coordinates',
params: {
x: 100,
y: 200,
},
});
});
it('should convert to screenshot coordinates', async () => {
const [x, y] = await macosInterface.toScreenshotCoordinates(300, 400);
expect(x).toBe(300);
expect(y).toBe(400);
const lastMessage = receivedMessages[receivedMessages.length - 1];
expect(lastMessage).toEqual({
command: 'to_screenshot_coordinates',
params: {
x: 300,
y: 400,
},
});
});
});
describe('Error Handling', () => {
it('should handle WebSocket connection errors', async () => {
// Use a valid but unreachable IP to avoid DNS errors
const macosInterface = new MacOSComputerInterface(
'localhost:9999',
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
// Connection should fail
await expect(macosInterface.connect()).rejects.toThrow();
});
it('should handle command errors', async () => {
// Create a server that returns errors
const errorWss = new WebSocketServer({ port: 0 });
const errorPort = (errorWss.address() as { port: number }).port;
errorWss.on('connection', (ws) => {
ws.on('message', () => {
ws.send(JSON.stringify({ error: 'Command failed', success: false }));
});
});
const macosInterface = new MacOSComputerInterface(
`localhost:${errorPort}`,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
// Command should throw error
await expect(macosInterface.leftClick(100, 100)).rejects.toThrow('Command failed');
await macosInterface.disconnect();
await new Promise<void>((resolve) => {
errorWss.close(() => resolve());
});
});
it('should handle disconnection gracefully', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
// Disconnect
macosInterface.disconnect();
expect(macosInterface.isConnected()).toBe(false);
// Should reconnect automatically on next command
await macosInterface.leftClick(100, 100);
expect(macosInterface.isConnected()).toBe(true);
await macosInterface.disconnect();
});
it('should handle force close', async () => {
const macosInterface = new MacOSComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
undefined,
testParams.vmName
);
await macosInterface.connect();
expect(macosInterface.isConnected()).toBe(true);
// Force close
macosInterface.forceClose();
expect(macosInterface.isConnected()).toBe(false);
});
});
});
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { MacOSComputerInterface } from '../../src/interface/macos.ts';
import { WindowsComputerInterface } from '../../src/interface/windows.ts';
describe('WindowsComputerInterface', () => {
const testParams = {
ipAddress: '192.0.2.1', // TEST-NET-1 address (RFC 5737) - guaranteed not to be routable
username: 'testuser',
password: 'testpass',
apiKey: 'test-api-key',
vmName: 'test-vm',
};
describe('Inheritance', () => {
it('should extend MacOSComputerInterface', () => {
const windowsInterface = new WindowsComputerInterface(
testParams.ipAddress,
testParams.username,
testParams.password,
testParams.apiKey,
testParams.vmName
);
expect(windowsInterface).toBeInstanceOf(MacOSComputerInterface);
expect(windowsInterface).toBeInstanceOf(WindowsComputerInterface);
});
});
});
+7
View File
@@ -0,0 +1,7 @@
import { afterAll, afterEach, beforeAll } from 'vitest';
beforeAll(() => {});
afterAll(() => {});
afterEach(() => {});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["es2023"],
"moduleDetection": "force",
"module": "preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"types": ["node"],
"allowSyntheticDefaultImports": true,
"strict": true,
"noUnusedLocals": true,
"declaration": true,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["src"]
}
+16
View File
@@ -0,0 +1,16 @@
import { readFileSync } from 'node:fs';
import { defineConfig } from 'tsdown';
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'));
export default defineConfig([
{
entry: ['./src/index.ts'],
platform: 'node',
dts: true,
external: ['child_process', 'util'],
define: {
__CUA_VERSION__: JSON.stringify(pkg.version),
},
},
]);
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./tests/setup.ts'],
environment: 'node',
globals: true,
},
});