mirror of
https://github.com/trycua/computer.git
synced 2026-01-08 06:20:00 -06:00
Initial computer typescript library creation
This commit is contained in:
6
libs/computer/typescript/.editorconfig
Normal file
6
libs/computer/typescript/.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
1
libs/computer/typescript/.gitattributes
vendored
Normal file
1
libs/computer/typescript/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
1
libs/computer/typescript/.github/FUNDING.yml
vendored
Normal file
1
libs/computer/typescript/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: sxzz
|
||||
4
libs/computer/typescript/.github/renovate.json5
vendored
Normal file
4
libs/computer/typescript/.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
extends: ['github>sxzz/renovate-config'],
|
||||
automerge: true,
|
||||
}
|
||||
26
libs/computer/typescript/.github/workflows/release.yml
vendored
Normal file
26
libs/computer/typescript/.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
38
libs/computer/typescript/.github/workflows/unit-test.yml
vendored
Normal file
38
libs/computer/typescript/.github/workflows/unit-test.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Unit Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
|
||||
- name: Set node LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
6
libs/computer/typescript/.gitignore
vendored
Normal file
6
libs/computer/typescript/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
|
||||
*.log
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
1
libs/computer/typescript/.nvmrc
Normal file
1
libs/computer/typescript/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v24.2.0
|
||||
21
libs/computer/typescript/LICENSE
Normal file
21
libs/computer/typescript/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2025 C/UA
|
||||
|
||||
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.
|
||||
119
libs/computer/typescript/README.md
Normal file
119
libs/computer/typescript/README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# C/UA Computer TypeScript Library
|
||||
|
||||
The TypeScript library for C/UA Computer - a powerful computer control and automation library.
|
||||
|
||||
## Overview
|
||||
|
||||
This library is a TypeScript port of the Python computer library, providing the same functionality for controlling virtual machines and computer interfaces. It includes:
|
||||
|
||||
- **Computer Class**: Main class for interacting with computers (virtual or host)
|
||||
- **VM Providers**: Support for different VM providers (Lume, Lumier, Cloud)
|
||||
- **Computer Interfaces**: OS-specific interfaces for controlling computers (macOS, Linux, Windows)
|
||||
- **Utilities**: Helper functions for display parsing, memory parsing, logging, and telemetry
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @cua/computer
|
||||
# or
|
||||
pnpm add @cua/computer
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { Computer } from '@cua/computer';
|
||||
|
||||
// Create a new computer instance
|
||||
const computer = new Computer({
|
||||
display: '1024x768',
|
||||
memory: '8GB',
|
||||
cpu: '4',
|
||||
osType: 'macos',
|
||||
image: 'macos-sequoia-cua:latest'
|
||||
});
|
||||
|
||||
// Start the computer
|
||||
await computer.run();
|
||||
|
||||
// Get the computer interface for interaction
|
||||
const interface = computer.interface;
|
||||
|
||||
// Take a screenshot
|
||||
const screenshot = await interface.getScreenshot();
|
||||
|
||||
// Click at coordinates
|
||||
await interface.click(500, 300);
|
||||
|
||||
// Type text
|
||||
await interface.typeText('Hello, world!');
|
||||
|
||||
// Stop the computer
|
||||
await computer.stop();
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is organized into several key modules:
|
||||
|
||||
### Core Components
|
||||
- `Computer`: Main class that manages VM lifecycle and interfaces
|
||||
- `ComputerOptions`: Configuration options for computer instances
|
||||
|
||||
### Models
|
||||
- `Display`: Display configuration (width, height)
|
||||
- `ComputerConfig`: Internal computer configuration
|
||||
|
||||
### Providers
|
||||
- `BaseVMProvider`: Abstract base class for VM providers
|
||||
- `VMProviderFactory`: Factory for creating provider instances
|
||||
- Provider types: `LUME`, `LUMIER`, `CLOUD`
|
||||
|
||||
### Interfaces
|
||||
- `BaseComputerInterface`: Abstract base class for OS interfaces
|
||||
- `InterfaceFactory`: Factory for creating OS-specific interfaces
|
||||
- Interface models: Key types, mouse buttons, accessibility tree
|
||||
|
||||
### Utilities
|
||||
- `Logger`: Logging with different verbosity levels
|
||||
- `helpers`: Default computer management and sandboxed execution
|
||||
- `utils`: Display/memory parsing, timeout utilities
|
||||
- `telemetry`: Usage tracking and metrics
|
||||
|
||||
## Development
|
||||
|
||||
- Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Run the unit tests:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
- Build the library:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
- Type checking:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
## External Dependencies
|
||||
|
||||
- `sharp`: For image processing and screenshot manipulation
|
||||
- Additional provider-specific packages need to be installed separately:
|
||||
- `@cua/computer-lume`: For Lume provider support
|
||||
- `@cua/computer-lumier`: For Lumier provider support
|
||||
- `@cua/computer-cloud`: For Cloud provider support
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License 2025 [C/UA](https://github.com/trycua)
|
||||
86
libs/computer/typescript/biome.json
Normal file
86
libs/computer/typescript/biome.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"build",
|
||||
"node_modules"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"useEditorconfig": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"useSelfClosingElements": "warn",
|
||||
"noUnusedTemplateLiteral": "warn",
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"useMediaCaption": "off",
|
||||
"useKeyWithClickEvents": "warn",
|
||||
"useKeyWithMouseEvents": "warn",
|
||||
"noSvgWithoutTitle": "off",
|
||||
"useButtonType": "warn",
|
||||
"noAutofocus": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedFunctionParameters": "warn",
|
||||
"noUnusedImports": "warn"
|
||||
},
|
||||
"complexity": {
|
||||
"useOptionalChain": "info"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "warn",
|
||||
"fix": "safe",
|
||||
"options": {
|
||||
"attributes": [
|
||||
"className"
|
||||
],
|
||||
"functions": [
|
||||
"cn"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
}
|
||||
54
libs/computer/typescript/package.json
Normal file
54
libs/computer/typescript/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@cua/computer",
|
||||
"version": "0.0.0",
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "",
|
||||
"bugs": {
|
||||
"url": ""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/trycua/cua.git"
|
||||
},
|
||||
"author": "",
|
||||
"funding": "",
|
||||
"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": "biome lint --cache .",
|
||||
"lint:fix": "biome lint --cache --fix .",
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"release": "bumpp && pnpm publish",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"sharp": "^0.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/node": "^22.15.17",
|
||||
"bumpp": "^10.1.0",
|
||||
"happy-dom": "^17.4.7",
|
||||
"tsdown": "^0.11.9",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
}
|
||||
}
|
||||
2000
libs/computer/typescript/pnpm-lock.yaml
generated
Normal file
2000
libs/computer/typescript/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
libs/computer/typescript/pnpm-workspace.yaml
Normal file
3
libs/computer/typescript/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- "@biomejs/biome"
|
||||
- sharp
|
||||
644
libs/computer/typescript/src/computer/computer.ts
Normal file
644
libs/computer/typescript/src/computer/computer.ts
Normal file
@@ -0,0 +1,644 @@
|
||||
import type { Display, ComputerConfig } from '../models';
|
||||
import type { BaseComputerInterface } from '../interface/base';
|
||||
import { InterfaceFactory } from '../interface/factory';
|
||||
import type { BaseVMProvider } from '../providers/base';
|
||||
import { VMProviderType } from '../providers/base';
|
||||
import { VMProviderFactory } from '../providers/factory';
|
||||
import { Logger, LogLevel } from '../logger';
|
||||
import { recordComputerInitialization, recordVMStart, recordVMStop } from '../telemetry';
|
||||
import { setDefaultComputer } from '../helpers';
|
||||
import { parseDisplayString, parseImageString, sleep, withTimeout } from '../utils';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export type OSType = 'macos' | 'linux' | 'windows';
|
||||
|
||||
export interface ComputerOptions {
|
||||
display?: Display | { width: number; height: number } | string;
|
||||
memory?: string;
|
||||
cpu?: string;
|
||||
osType?: OSType;
|
||||
name?: string;
|
||||
image?: string;
|
||||
sharedDirectories?: string[];
|
||||
useHostComputerServer?: boolean;
|
||||
verbosity?: number | LogLevel;
|
||||
telemetryEnabled?: boolean;
|
||||
providerType?: VMProviderType | string;
|
||||
port?: number;
|
||||
noVNCPort?: number;
|
||||
host?: string;
|
||||
storage?: string;
|
||||
ephemeral?: boolean;
|
||||
apiKey?: string;
|
||||
experiments?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computer is the main class for interacting with the computer.
|
||||
*/
|
||||
export class Computer {
|
||||
private logger: Logger;
|
||||
private vmLogger: Logger;
|
||||
private interfaceLogger: Logger;
|
||||
|
||||
private image: string;
|
||||
private port?: number;
|
||||
private noVNCPort?: number;
|
||||
private host: string;
|
||||
private osType: OSType;
|
||||
private providerType: VMProviderType | string;
|
||||
private ephemeral: boolean;
|
||||
private apiKey?: string;
|
||||
private experiments: string[];
|
||||
private storage?: string;
|
||||
private sharedPath?: string;
|
||||
private sharedDirectories: string[];
|
||||
private _telemetryEnabled: boolean;
|
||||
private _initialized: boolean = false;
|
||||
private _running: boolean = false;
|
||||
private verbosity: number | LogLevel;
|
||||
private useHostComputerServer: boolean;
|
||||
private config?: ComputerConfig;
|
||||
private _providerContext?: BaseVMProvider;
|
||||
private _interface?: BaseComputerInterface;
|
||||
private _stopEvent?: Promise<void>;
|
||||
private _keepAliveTask?: Promise<void>;
|
||||
|
||||
/**
|
||||
* Initialize a new Computer instance.
|
||||
*
|
||||
* @param options Configuration options for the Computer
|
||||
*/
|
||||
constructor(options: ComputerOptions = {}) {
|
||||
const {
|
||||
display = '1024x768',
|
||||
memory = '8GB',
|
||||
cpu = '4',
|
||||
osType = 'macos',
|
||||
name = '',
|
||||
image = 'macos-sequoia-cua:latest',
|
||||
sharedDirectories = [],
|
||||
useHostComputerServer = false,
|
||||
verbosity = LogLevel.NORMAL,
|
||||
telemetryEnabled = true,
|
||||
providerType = VMProviderType.LUME,
|
||||
port = 7777,
|
||||
noVNCPort = 8006,
|
||||
host = process.env.PYLUME_HOST || 'localhost',
|
||||
storage,
|
||||
ephemeral = false,
|
||||
apiKey,
|
||||
experiments = []
|
||||
} = options;
|
||||
|
||||
this.verbosity = verbosity;
|
||||
this.logger = new Logger('cua.computer', verbosity);
|
||||
this.logger.info('Initializing Computer...');
|
||||
|
||||
// Store original parameters
|
||||
this.image = image;
|
||||
this.port = port;
|
||||
this.noVNCPort = noVNCPort;
|
||||
this.host = host;
|
||||
this.osType = osType;
|
||||
this.providerType = providerType;
|
||||
this.ephemeral = ephemeral;
|
||||
this.apiKey = apiKey;
|
||||
this.experiments = experiments;
|
||||
|
||||
if (this.experiments.includes('app-use')) {
|
||||
if (this.osType !== 'macos') {
|
||||
throw new Error('App use experiment is only supported on macOS');
|
||||
}
|
||||
}
|
||||
|
||||
// The default is currently to use non-ephemeral storage
|
||||
if (storage && ephemeral && storage !== 'ephemeral') {
|
||||
throw new Error('Storage path and ephemeral flag cannot be used together');
|
||||
}
|
||||
this.storage = ephemeral ? 'ephemeral' : storage;
|
||||
|
||||
// For Lumier provider, store the first shared directory path to use
|
||||
// for VM file sharing
|
||||
this.sharedPath = undefined;
|
||||
if (sharedDirectories && sharedDirectories.length > 0) {
|
||||
this.sharedPath = sharedDirectories[0];
|
||||
this.logger.info(`Using first shared directory for VM file sharing: ${this.sharedPath}`);
|
||||
}
|
||||
|
||||
// Store telemetry preference
|
||||
this._telemetryEnabled = telemetryEnabled;
|
||||
|
||||
// Configure component loggers with proper hierarchy
|
||||
this.vmLogger = new Logger('cua.vm', verbosity);
|
||||
this.interfaceLogger = new Logger('cua.interface', verbosity);
|
||||
|
||||
this.useHostComputerServer = useHostComputerServer;
|
||||
|
||||
if (!useHostComputerServer) {
|
||||
const imageInfo = parseImageString(image);
|
||||
|
||||
const vmName = name || image.replace(':', '_');
|
||||
|
||||
// Convert display parameter to Display object
|
||||
let displayConfig: Display;
|
||||
if (typeof display === 'string') {
|
||||
const { width, height } = parseDisplayString(display);
|
||||
displayConfig = { width, height };
|
||||
} else if ('width' in display && 'height' in display) {
|
||||
displayConfig = display as Display;
|
||||
} else {
|
||||
displayConfig = display as Display;
|
||||
}
|
||||
|
||||
this.config = {
|
||||
image: imageInfo.name,
|
||||
tag: imageInfo.tag,
|
||||
name: vmName,
|
||||
display: displayConfig,
|
||||
memory,
|
||||
cpu,
|
||||
};
|
||||
}
|
||||
|
||||
// Store shared directories config
|
||||
this.sharedDirectories = sharedDirectories;
|
||||
|
||||
// Record initialization in telemetry (if enabled)
|
||||
if (telemetryEnabled) {
|
||||
recordComputerInitialization();
|
||||
} else {
|
||||
this.logger.debug('Telemetry disabled - skipping initialization tracking');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a virtual desktop from a list of app names, returning a DioramaComputer
|
||||
* that proxies Diorama.Interface but uses diorama_cmds via the computer interface.
|
||||
*
|
||||
* @param apps List of application names to include in the desktop.
|
||||
* @returns A proxy object with the Diorama interface, but using diorama_cmds.
|
||||
*/
|
||||
createDesktopFromApps(apps: string[]): any {
|
||||
if (!this.experiments.includes('app-use')) {
|
||||
throw new Error("App Usage is an experimental feature. Enable it by passing experiments=['app-use'] to Computer()");
|
||||
}
|
||||
// DioramaComputer would be imported and used here
|
||||
throw new Error('DioramaComputer not yet implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the computer (async context manager enter).
|
||||
*/
|
||||
async __aenter__(): Promise<this> {
|
||||
await this.run();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the computer (async context manager exit).
|
||||
*/
|
||||
async __aexit__(excType: any, excVal: any, excTb: any): Promise<void> {
|
||||
await this.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the VM and computer interface.
|
||||
*/
|
||||
async run(): Promise<string | undefined> {
|
||||
// If already initialized, just log and return
|
||||
if (this._initialized) {
|
||||
this.logger.info('Computer already initialized, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Starting computer...');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let ipAddress: string;
|
||||
|
||||
// If using host computer server
|
||||
if (this.useHostComputerServer) {
|
||||
this.logger.info('Using host computer server');
|
||||
ipAddress = 'localhost';
|
||||
|
||||
// Create the interface
|
||||
this._interface = InterfaceFactory.createInterfaceForOS(
|
||||
this.osType,
|
||||
ipAddress
|
||||
);
|
||||
|
||||
this.logger.info('Waiting for host computer server to be ready...');
|
||||
await this._interface.waitForReady();
|
||||
this.logger.info('Host computer server ready');
|
||||
} else {
|
||||
// Start or connect to VM
|
||||
this.logger.info(`Starting VM: ${this.image}`);
|
||||
|
||||
if (!this._providerContext) {
|
||||
try {
|
||||
const providerTypeName = typeof this.providerType === 'object'
|
||||
? this.providerType
|
||||
: this.providerType;
|
||||
|
||||
this.logger.verbose(`Initializing ${providerTypeName} provider context...`);
|
||||
|
||||
// Create VM provider instance with explicit parameters
|
||||
const providerOptions = {
|
||||
port: this.port,
|
||||
host: this.host,
|
||||
storage: this.storage,
|
||||
sharedPath: this.sharedPath,
|
||||
image: this.image,
|
||||
verbose: this.verbosity >= LogLevel.DEBUG,
|
||||
ephemeral: this.ephemeral,
|
||||
noVNCPort: this.noVNCPort,
|
||||
apiKey: this.apiKey
|
||||
};
|
||||
|
||||
if (!this.config) {
|
||||
throw new Error('Computer config not initialized');
|
||||
}
|
||||
|
||||
this.config.vm_provider = await VMProviderFactory.createProvider(
|
||||
this.providerType,
|
||||
providerOptions
|
||||
);
|
||||
|
||||
this._providerContext = await this.config.vm_provider.__aenter__();
|
||||
this.logger.verbose('VM provider context initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to import provider dependencies: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the VM
|
||||
if (!this.config || !this.config.vm_provider) {
|
||||
throw new Error('VM provider not initialized');
|
||||
}
|
||||
|
||||
const runOpts = {
|
||||
display: this.config.display,
|
||||
memory: this.config.memory,
|
||||
cpu: this.config.cpu,
|
||||
shared_directories: this.sharedDirectories
|
||||
};
|
||||
|
||||
this.logger.info(`Running VM ${this.config.name} with options:`, runOpts);
|
||||
|
||||
if (this._telemetryEnabled) {
|
||||
recordVMStart(this.config.name, String(this.providerType));
|
||||
}
|
||||
|
||||
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
|
||||
|
||||
try {
|
||||
await this.config.vm_provider.runVM(
|
||||
this.image,
|
||||
this.config.name,
|
||||
runOpts,
|
||||
storageParam
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('already running')) {
|
||||
this.logger.info(`VM ${this.config.name} is already running`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for VM to be ready
|
||||
try {
|
||||
this.logger.info('Waiting for VM to be ready...');
|
||||
await this.waitVMReady();
|
||||
|
||||
// Get IP address
|
||||
ipAddress = await this.getIP();
|
||||
this.logger.info(`VM is ready with IP: ${ipAddress}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error waiting for VM: ${error}`);
|
||||
throw new Error(`VM failed to become ready: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the interface
|
||||
try {
|
||||
// Verify we have a valid IP before initializing the interface
|
||||
if (!ipAddress || ipAddress === 'unknown' || ipAddress === '0.0.0.0') {
|
||||
throw new Error(`Cannot initialize interface - invalid IP address: ${ipAddress}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Initializing interface for ${this.osType} at ${ipAddress}`);
|
||||
|
||||
// Pass authentication credentials if using cloud provider
|
||||
if (this.providerType === VMProviderType.CLOUD && this.apiKey && this.config?.name) {
|
||||
this._interface = InterfaceFactory.createInterfaceForOS(
|
||||
this.osType,
|
||||
ipAddress,
|
||||
this.apiKey,
|
||||
this.config.name
|
||||
);
|
||||
} else {
|
||||
this._interface = InterfaceFactory.createInterfaceForOS(
|
||||
this.osType,
|
||||
ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for the WebSocket interface to be ready
|
||||
this.logger.info('Connecting to WebSocket interface...');
|
||||
|
||||
try {
|
||||
await withTimeout(
|
||||
this._interface.waitForReady(),
|
||||
30000,
|
||||
`Could not connect to WebSocket interface at ${ipAddress}:8000/ws`
|
||||
);
|
||||
this.logger.info('WebSocket interface connected successfully');
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to connect to WebSocket interface at ${ipAddress}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create an event to keep the VM running in background if needed
|
||||
if (!this.useHostComputerServer) {
|
||||
// In TypeScript, we'll use a Promise instead of asyncio.Event
|
||||
let resolveStop: () => void;
|
||||
this._stopEvent = new Promise<void>(resolve => {
|
||||
resolveStop = resolve;
|
||||
});
|
||||
this._keepAliveTask = this._stopEvent;
|
||||
}
|
||||
|
||||
this.logger.info('Computer is ready');
|
||||
|
||||
// Set the initialization flag
|
||||
this._initialized = true;
|
||||
|
||||
// Set this instance as the default computer for remote decorators
|
||||
setDefaultComputer(this);
|
||||
|
||||
this.logger.info('Computer successfully initialized');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
// Log initialization time for performance monitoring
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.debug(`Computer initialization took ${durationMs.toFixed(2)}ms`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to initialize computer: ${error}`);
|
||||
throw new Error(`Failed to initialize computer: ${error}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the computer's WebSocket interface.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this._interface) {
|
||||
// Note: The interface close method would need to be implemented
|
||||
// this._interface.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the computer's WebSocket interface and stop the computer.
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
this.logger.info('Stopping Computer...');
|
||||
|
||||
// In VM mode, first explicitly stop the VM, then exit the provider context
|
||||
if (!this.useHostComputerServer && this._providerContext && this.config?.vm_provider) {
|
||||
try {
|
||||
this.logger.info(`Stopping VM ${this.config.name}...`);
|
||||
await this.config.vm_provider.stopVM(
|
||||
this.config.name,
|
||||
this.storage
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error stopping VM: ${error}`);
|
||||
}
|
||||
|
||||
this.logger.verbose('Closing VM provider context...');
|
||||
await this.config.vm_provider.__aexit__(null, null, null);
|
||||
this._providerContext = undefined;
|
||||
}
|
||||
|
||||
await this.disconnect();
|
||||
this.logger.info('Computer stopped');
|
||||
} catch (error) {
|
||||
this.logger.debug(`Error during cleanup: ${error}`);
|
||||
} finally {
|
||||
// Log stop time for performance monitoring
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.debug(`Computer stop process took ${durationMs.toFixed(2)}ms`);
|
||||
|
||||
if (this._telemetryEnabled && this.config?.name) {
|
||||
recordVMStop(this.config.name, durationMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IP address of the VM or localhost if using host computer server.
|
||||
*/
|
||||
async getIP(maxRetries: number = 15, retryDelay: number = 2): Promise<string> {
|
||||
// For host computer server, always return localhost immediately
|
||||
if (this.useHostComputerServer) {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
// Get IP from the provider
|
||||
if (!this.config?.vm_provider) {
|
||||
throw new Error('VM provider is not initialized');
|
||||
}
|
||||
|
||||
// Log that we're waiting for the IP
|
||||
this.logger.info(`Waiting for VM ${this.config.name} to get an IP address...`);
|
||||
|
||||
// Call the provider's get_ip method which will wait indefinitely
|
||||
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
|
||||
|
||||
// Log the image being used
|
||||
this.logger.info(`Running VM using image: ${this.image}`);
|
||||
|
||||
// Call provider.getIP with explicit parameters
|
||||
const ip = await this.config.vm_provider.getIP(
|
||||
this.config.name,
|
||||
storageParam,
|
||||
retryDelay
|
||||
);
|
||||
|
||||
// Log success
|
||||
this.logger.info(`VM ${this.config.name} has IP address: ${ip}`);
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for VM to be ready with an IP address.
|
||||
*/
|
||||
async waitVMReady(): Promise<Record<string, any> | undefined> {
|
||||
if (this.useHostComputerServer) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timeout = 600; // 10 minutes timeout
|
||||
const interval = 2.0; // 2 seconds between checks
|
||||
const startTime = Date.now() / 1000;
|
||||
let lastStatus: string | undefined;
|
||||
let attempts = 0;
|
||||
|
||||
this.logger.info(`Waiting for VM ${this.config?.name} to be ready (timeout: ${timeout}s)...`);
|
||||
|
||||
while ((Date.now() / 1000) - startTime < timeout) {
|
||||
attempts++;
|
||||
const elapsed = (Date.now() / 1000) - startTime;
|
||||
|
||||
try {
|
||||
// Keep polling for VM info
|
||||
if (!this.config?.vm_provider) {
|
||||
this.logger.error('VM provider is not initialized');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const vm = await this.config.vm_provider.getVM(this.config.name);
|
||||
|
||||
// Log full VM properties for debugging (every 30 attempts)
|
||||
if (attempts % 30 === 0) {
|
||||
this.logger.info(
|
||||
`VM properties at attempt ${attempts}: ${JSON.stringify(vm)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Get current status for logging
|
||||
const currentStatus = vm?.status;
|
||||
if (currentStatus !== lastStatus) {
|
||||
this.logger.info(
|
||||
`VM status changed to: ${currentStatus} (after ${elapsed.toFixed(1)}s)`
|
||||
);
|
||||
lastStatus = currentStatus;
|
||||
}
|
||||
|
||||
// Check if VM is ready
|
||||
if (vm && vm.status === 'running' && vm.ip_address && vm.ip_address !== '0.0.0.0') {
|
||||
this.logger.info(`VM ${this.config.name} is ready with IP: ${vm.ip_address}`);
|
||||
return vm;
|
||||
}
|
||||
|
||||
// Wait before next check
|
||||
await sleep(interval * 1000);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking VM status: ${error}`);
|
||||
await sleep(interval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`VM ${this.config?.name} failed to become ready within ${timeout} seconds`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update VM settings.
|
||||
*/
|
||||
async update(cpu?: number, memory?: string): Promise<void> {
|
||||
if (this.useHostComputerServer) {
|
||||
this.logger.warning('Cannot update settings for host computer server');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config?.vm_provider) {
|
||||
throw new Error('VM provider is not initialized');
|
||||
}
|
||||
|
||||
await this.config.vm_provider.updateVM(
|
||||
this.config.name,
|
||||
cpu,
|
||||
memory,
|
||||
this.storage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of a screenshot.
|
||||
*/
|
||||
async getScreenshotSize(screenshot: Buffer): Promise<{ width: number; height: number }> {
|
||||
const metadata = await sharp(screenshot).metadata();
|
||||
return {
|
||||
width: metadata.width || 0,
|
||||
height: metadata.height || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computer interface for interacting with the VM.
|
||||
*/
|
||||
get interface(): BaseComputerInterface {
|
||||
if (!this._interface) {
|
||||
throw new Error('Computer interface not initialized. Call run() first.');
|
||||
}
|
||||
return this._interface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled for this computer instance.
|
||||
*/
|
||||
get telemetryEnabled(): boolean {
|
||||
return this._telemetryEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normalized coordinates to screen coordinates.
|
||||
*/
|
||||
toScreenCoordinates(x: number, y: number): [number, number] {
|
||||
if (!this.config?.display) {
|
||||
throw new Error('Display configuration not available');
|
||||
}
|
||||
return [
|
||||
x * this.config.display.width,
|
||||
y * this.config.display.height
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert screen coordinates to screenshot coordinates.
|
||||
*/
|
||||
async toScreenshotCoordinates(x: number, y: number): Promise<[number, number]> {
|
||||
// In the Python version, this uses the interface to get screenshot dimensions
|
||||
// For now, we'll assume 1:1 mapping
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Install packages in a virtual environment.
|
||||
*/
|
||||
async venvInstall(venvName: string, requirements: string[]): Promise<[string, string]> {
|
||||
// This would be implemented using the interface to run commands
|
||||
// TODO: Implement venvInstall
|
||||
throw new Error('venvInstall not yet implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell command in a virtual environment.
|
||||
*/
|
||||
async venvCmd(venvName: string, command: string): Promise<[string, string]> {
|
||||
// This would be implemented using the interface to run commands
|
||||
// TODO: Implement venvCmd
|
||||
throw new Error('venvCmd not yet implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute function in a virtual environment using source code extraction.
|
||||
*/
|
||||
async venvExec(venvName: string, pythonFunc: Function, ...args: any[]): Promise<any> {
|
||||
// This would be implemented using the interface to run Python code
|
||||
// TODO: Implement venvExec
|
||||
throw new Error('venvExec not yet implemented');
|
||||
}
|
||||
}
|
||||
2
libs/computer/typescript/src/computer/index.ts
Normal file
2
libs/computer/typescript/src/computer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Re-export the Computer class and related types
|
||||
export * from './computer';
|
||||
76
libs/computer/typescript/src/helpers.ts
Normal file
76
libs/computer/typescript/src/helpers.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Helper functions and decorators for the Computer module.
|
||||
*/
|
||||
|
||||
import type { Computer } from './computer';
|
||||
|
||||
// Global reference to the default computer instance
|
||||
let _defaultComputer: Computer | null = null;
|
||||
|
||||
/**
|
||||
* Set the default computer instance to be used by the remote decorator.
|
||||
*
|
||||
* @param computer The computer instance to use as default
|
||||
*/
|
||||
export function setDefaultComputer(computer: Computer): void {
|
||||
_defaultComputer = computer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default computer instance.
|
||||
*
|
||||
* @returns The default computer instance or null
|
||||
*/
|
||||
export function getDefaultComputer(): Computer | null {
|
||||
return _defaultComputer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator that wraps a function to be executed remotely via computer.venvExec
|
||||
*
|
||||
* @param venvName Name of the virtual environment to execute in
|
||||
* @param computer The computer instance to use, or "default" to use the globally set default
|
||||
* @param maxRetries Maximum number of retries for the remote execution
|
||||
*/
|
||||
export function sandboxed(
|
||||
venvName: string = 'default',
|
||||
computer: Computer | 'default' = 'default',
|
||||
maxRetries: number = 3
|
||||
) {
|
||||
return function <T extends (...args: any[]) => any>(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: Parameters<T>): Promise<ReturnType<T>> {
|
||||
// Determine which computer instance to use
|
||||
const comp = computer === 'default' ? _defaultComputer : computer;
|
||||
|
||||
if (!comp) {
|
||||
throw new Error(
|
||||
'No computer instance available. Either specify a computer instance or call setDefaultComputer() first.'
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await comp.venvExec(venvName, originalMethod, ...args);
|
||||
} catch (error) {
|
||||
console.error(`Attempt ${i + 1} failed:`, error);
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached, but satisfies TypeScript's control flow analysis
|
||||
throw new Error('Unexpected: maxRetries loop completed without returning or throwing');
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
28
libs/computer/typescript/src/index.ts
Normal file
28
libs/computer/typescript/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Core components
|
||||
export { Computer } from "./computer";
|
||||
export type { ComputerOptions, OSType } from "./computer";
|
||||
|
||||
// Models
|
||||
export type { Display, ComputerConfig } from "./models";
|
||||
|
||||
// Provider components
|
||||
export { VMProviderType, BaseVMProviderImpl } from "./providers";
|
||||
export type { BaseVMProvider } from "./providers";
|
||||
export { VMProviderFactory } from "./providers";
|
||||
export type { VMProviderOptions } from "./providers";
|
||||
|
||||
// Interface components
|
||||
export type { BaseComputerInterface } from "./interface";
|
||||
export { InterfaceFactory } from "./interface";
|
||||
export type { InterfaceOptions } from "./interface";
|
||||
export { Key } from "./interface";
|
||||
export type {
|
||||
KeyType,
|
||||
MouseButton,
|
||||
NavigationKey,
|
||||
SpecialKey,
|
||||
ModifierKey,
|
||||
FunctionKey,
|
||||
AccessibilityWindow,
|
||||
AccessibilityTree
|
||||
} from "./interface";
|
||||
41
libs/computer/typescript/src/interface/base.ts
Normal file
41
libs/computer/typescript/src/interface/base.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { KeyType, MouseButton, AccessibilityTree } from './models';
|
||||
|
||||
/**
|
||||
* Base interface for computer control implementations.
|
||||
*/
|
||||
export interface BaseComputerInterface {
|
||||
/**
|
||||
* Wait for the interface to be ready.
|
||||
*/
|
||||
waitForReady(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get a screenshot of the current screen.
|
||||
*/
|
||||
getScreenshot(): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
* Move the mouse to the specified coordinates.
|
||||
*/
|
||||
moveMouse(x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Click the mouse at the current position.
|
||||
*/
|
||||
click(button?: MouseButton): Promise<void>;
|
||||
|
||||
/**
|
||||
* Type text at the current cursor position.
|
||||
*/
|
||||
typeText(text: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Press a key.
|
||||
*/
|
||||
pressKey(key: KeyType): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the accessibility tree.
|
||||
*/
|
||||
getAccessibilityTree(): Promise<AccessibilityTree>;
|
||||
}
|
||||
56
libs/computer/typescript/src/interface/factory.ts
Normal file
56
libs/computer/typescript/src/interface/factory.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { BaseComputerInterface } from './base';
|
||||
import type { OSType } from '../computer';
|
||||
|
||||
export interface InterfaceOptions {
|
||||
ipAddress: string;
|
||||
apiKey?: string;
|
||||
vmName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating OS-specific computer interfaces.
|
||||
*/
|
||||
export class InterfaceFactory {
|
||||
/**
|
||||
* Create an interface for the specified OS.
|
||||
*
|
||||
* @param os The operating system type ('macos', 'linux', 'windows')
|
||||
* @param ipAddress The IP address to connect to
|
||||
* @param apiKey Optional API key for cloud providers
|
||||
* @param vmName Optional VM name for cloud providers
|
||||
* @returns An instance of the appropriate computer interface
|
||||
*/
|
||||
static createInterfaceForOS(
|
||||
os: OSType,
|
||||
ipAddress: string,
|
||||
apiKey?: string,
|
||||
vmName?: string
|
||||
): BaseComputerInterface {
|
||||
const options: InterfaceOptions = {
|
||||
ipAddress,
|
||||
apiKey,
|
||||
vmName
|
||||
};
|
||||
|
||||
switch (os) {
|
||||
case 'macos':
|
||||
// Dynamic import would be used in real implementation
|
||||
// TODO: Implement macOS interface
|
||||
throw new Error('macOS interface not yet implemented');
|
||||
|
||||
case 'linux':
|
||||
// Dynamic import would be used in real implementation
|
||||
// TODO: Implement Linux interface
|
||||
throw new Error('Linux interface not yet implemented');
|
||||
|
||||
case 'windows':
|
||||
// Dynamic import would be used in real implementation
|
||||
// TODO: Implement Windows interface
|
||||
throw new Error('Windows interface not yet implemented');
|
||||
|
||||
default:
|
||||
// TODO: Implement interface for this OS
|
||||
throw new Error(`Interface for OS ${os} not implemented`);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
libs/computer/typescript/src/interface/index.ts
Normal file
5
libs/computer/typescript/src/interface/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./base";
|
||||
export * from "./factory";
|
||||
export * from "./models";
|
||||
export * from "./macos";
|
||||
export * from "./linux";
|
||||
47
libs/computer/typescript/src/interface/linux.ts
Normal file
47
libs/computer/typescript/src/interface/linux.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { BaseComputerInterface } from './base';
|
||||
import type { KeyType, MouseButton, AccessibilityTree } from './models';
|
||||
|
||||
/**
|
||||
* Linux-specific implementation of the computer interface.
|
||||
*/
|
||||
export class LinuxComputerInterface implements BaseComputerInterface {
|
||||
private ip_address: string;
|
||||
|
||||
constructor(ip_address: string) {
|
||||
this.ip_address = ip_address;
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async getScreenshot(): Promise<Buffer> {
|
||||
// Implementation will go here
|
||||
return Buffer.from([]);
|
||||
}
|
||||
|
||||
async moveMouse(x: number, y: number): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async click(button: MouseButton = 'left'): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async typeText(text: string): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async pressKey(key: KeyType): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async getAccessibilityTree(): Promise<AccessibilityTree> {
|
||||
// Implementation will go here
|
||||
return {
|
||||
success: false,
|
||||
frontmost_application: '',
|
||||
windows: []
|
||||
};
|
||||
}
|
||||
}
|
||||
47
libs/computer/typescript/src/interface/macos.ts
Normal file
47
libs/computer/typescript/src/interface/macos.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { BaseComputerInterface } from './base';
|
||||
import type { KeyType, MouseButton, AccessibilityTree } from './models';
|
||||
|
||||
/**
|
||||
* macOS-specific implementation of the computer interface.
|
||||
*/
|
||||
export class MacOSComputerInterface implements BaseComputerInterface {
|
||||
private ip_address: string;
|
||||
|
||||
constructor(ip_address: string) {
|
||||
this.ip_address = ip_address;
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async getScreenshot(): Promise<Buffer> {
|
||||
// Implementation will go here
|
||||
return Buffer.from([]);
|
||||
}
|
||||
|
||||
async moveMouse(x: number, y: number): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async click(button: MouseButton = 'left'): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async typeText(text: string): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async pressKey(key: KeyType): Promise<void> {
|
||||
// Implementation will go here
|
||||
}
|
||||
|
||||
async getAccessibilityTree(): Promise<AccessibilityTree> {
|
||||
// Implementation will go here
|
||||
return {
|
||||
success: false,
|
||||
frontmost_application: '',
|
||||
windows: []
|
||||
};
|
||||
}
|
||||
}
|
||||
96
libs/computer/typescript/src/interface/models.ts
Normal file
96
libs/computer/typescript/src/interface/models.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Navigation key literals
|
||||
*/
|
||||
export type NavigationKey = 'pagedown' | 'pageup' | 'home' | 'end' | 'left' | 'right' | 'up' | 'down';
|
||||
|
||||
/**
|
||||
* Special key literals
|
||||
*/
|
||||
export type SpecialKey = 'enter' | 'esc' | 'tab' | 'space' | 'backspace' | 'del';
|
||||
|
||||
/**
|
||||
* Modifier key literals
|
||||
*/
|
||||
export type ModifierKey = 'ctrl' | 'alt' | 'shift' | 'win' | 'command' | 'option';
|
||||
|
||||
/**
|
||||
* Function key literals
|
||||
*/
|
||||
export type FunctionKey = 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
|
||||
|
||||
/**
|
||||
* Keyboard keys that can be used with press_key.
|
||||
*/
|
||||
export enum Key {
|
||||
// Navigation
|
||||
PAGE_DOWN = 'pagedown',
|
||||
PAGE_UP = 'pageup',
|
||||
HOME = 'home',
|
||||
END = 'end',
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
UP = 'up',
|
||||
DOWN = 'down',
|
||||
|
||||
// Special keys
|
||||
RETURN = 'enter',
|
||||
ENTER = 'enter',
|
||||
ESCAPE = 'esc',
|
||||
ESC = 'esc',
|
||||
TAB = 'tab',
|
||||
SPACE = 'space',
|
||||
BACKSPACE = 'backspace',
|
||||
DELETE = 'del',
|
||||
|
||||
// Modifier keys
|
||||
ALT = 'alt',
|
||||
CTRL = 'ctrl',
|
||||
SHIFT = 'shift',
|
||||
WIN = 'win',
|
||||
COMMAND = 'command',
|
||||
OPTION = 'option',
|
||||
|
||||
// Function keys
|
||||
F1 = 'f1',
|
||||
F2 = 'f2',
|
||||
F3 = 'f3',
|
||||
F4 = 'f4',
|
||||
F5 = 'f5',
|
||||
F6 = 'f6',
|
||||
F7 = 'f7',
|
||||
F8 = 'f8',
|
||||
F9 = 'f9',
|
||||
F10 = 'f10',
|
||||
F11 = 'f11',
|
||||
F12 = 'f12'
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined key type
|
||||
*/
|
||||
export type KeyType = Key | NavigationKey | SpecialKey | ModifierKey | FunctionKey | string;
|
||||
|
||||
/**
|
||||
* Key type for mouse actions
|
||||
*/
|
||||
export type MouseButton = 'left' | 'right' | 'middle';
|
||||
|
||||
/**
|
||||
* Information about a window in the accessibility tree.
|
||||
*/
|
||||
export interface AccessibilityWindow {
|
||||
app_name: string;
|
||||
pid: number;
|
||||
frontmost: boolean;
|
||||
has_windows: boolean;
|
||||
windows: Array<Record<string, any>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete accessibility tree information.
|
||||
*/
|
||||
export interface AccessibilityTree {
|
||||
success: boolean;
|
||||
frontmost_application: string;
|
||||
windows: AccessibilityWindow[];
|
||||
}
|
||||
50
libs/computer/typescript/src/logger.ts
Normal file
50
libs/computer/typescript/src/logger.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Logger implementation for the Computer library.
|
||||
*/
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 10,
|
||||
VERBOSE = 15,
|
||||
INFO = 20,
|
||||
NORMAL = 20,
|
||||
WARNING = 30,
|
||||
ERROR = 40,
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private name: string;
|
||||
private verbosity: number;
|
||||
|
||||
constructor(name: string, verbosity: number | LogLevel = LogLevel.NORMAL) {
|
||||
this.name = name;
|
||||
this.verbosity = typeof verbosity === 'number' ? verbosity : verbosity;
|
||||
}
|
||||
|
||||
private log(level: LogLevel, message: string, ...args: any[]): void {
|
||||
if (level >= this.verbosity) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const levelName = LogLevel[level];
|
||||
console.log(`[${timestamp}] [${this.name}] [${levelName}] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
this.log(LogLevel.DEBUG, message, ...args);
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
this.log(LogLevel.INFO, message, ...args);
|
||||
}
|
||||
|
||||
verbose(message: string, ...args: any[]): void {
|
||||
this.log(LogLevel.VERBOSE, message, ...args);
|
||||
}
|
||||
|
||||
warning(message: string, ...args: any[]): void {
|
||||
this.log(LogLevel.WARNING, message, ...args);
|
||||
}
|
||||
|
||||
error(message: string, ...args: any[]): void {
|
||||
this.log(LogLevel.ERROR, message, ...args);
|
||||
}
|
||||
}
|
||||
21
libs/computer/typescript/src/models.ts
Normal file
21
libs/computer/typescript/src/models.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Display configuration for the computer.
|
||||
*/
|
||||
export interface Display {
|
||||
width: number;
|
||||
height: number;
|
||||
scale_factor?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computer configuration model.
|
||||
*/
|
||||
export interface ComputerConfig {
|
||||
image: string;
|
||||
tag: string;
|
||||
name: string;
|
||||
display: Display;
|
||||
memory: string;
|
||||
cpu: string;
|
||||
vm_provider?: any; // Will be properly typed when implemented
|
||||
}
|
||||
140
libs/computer/typescript/src/providers/base.ts
Normal file
140
libs/computer/typescript/src/providers/base.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Types of VM providers available.
|
||||
*/
|
||||
export enum VMProviderType {
|
||||
LUME = 'lume',
|
||||
LUMIER = 'lumier',
|
||||
CLOUD = 'cloud',
|
||||
UNKNOWN = 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for VM providers.
|
||||
* All VM provider implementations must implement this interface.
|
||||
*/
|
||||
export interface BaseVMProvider {
|
||||
/**
|
||||
* Get the provider type.
|
||||
*/
|
||||
readonly providerType: VMProviderType;
|
||||
|
||||
/**
|
||||
* Get VM information by name.
|
||||
*
|
||||
* @param name Name of the VM to get information for
|
||||
* @param storage Optional storage path override
|
||||
* @returns Dictionary with VM information including status, IP address, etc.
|
||||
*/
|
||||
getVM(name: string, storage?: string): Promise<Record<string, any>>;
|
||||
|
||||
/**
|
||||
* List all available VMs.
|
||||
*/
|
||||
listVMs(): Promise<Array<Record<string, any>>>;
|
||||
|
||||
/**
|
||||
* Run a VM by name with the given options.
|
||||
*
|
||||
* @param image VM image to run
|
||||
* @param name Name for the VM
|
||||
* @param runOpts Run options for the VM
|
||||
* @param storage Optional storage path
|
||||
* @returns VM run response
|
||||
*/
|
||||
runVM(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: Record<string, any>,
|
||||
storage?: string
|
||||
): Promise<Record<string, any>>;
|
||||
|
||||
/**
|
||||
* Stop a VM by name.
|
||||
*
|
||||
* @param name Name of the VM to stop
|
||||
* @param storage Optional storage path
|
||||
*/
|
||||
stopVM(name: string, storage?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the IP address of a VM.
|
||||
*
|
||||
* @param name Name of the VM
|
||||
* @param storage Optional storage path
|
||||
* @param retryDelay Delay between retries in seconds
|
||||
* @returns IP address of the VM
|
||||
*/
|
||||
getIP(name: string, storage?: string, retryDelay?: number): Promise<string>;
|
||||
|
||||
/**
|
||||
* Update VM settings.
|
||||
*
|
||||
* @param name Name of the VM
|
||||
* @param cpu New CPU allocation
|
||||
* @param memory New memory allocation
|
||||
* @param storage Optional storage path
|
||||
*/
|
||||
updateVM(
|
||||
name: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Context manager enter method
|
||||
*/
|
||||
__aenter__(): Promise<this>;
|
||||
|
||||
/**
|
||||
* Context manager exit method
|
||||
*/
|
||||
__aexit__(
|
||||
excType: any,
|
||||
excVal: any,
|
||||
excTb: any
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for VM providers that implements context manager
|
||||
*/
|
||||
export abstract class BaseVMProviderImpl implements BaseVMProvider {
|
||||
abstract readonly providerType: VMProviderType;
|
||||
|
||||
abstract getVM(name: string, storage?: string): Promise<Record<string, any>>;
|
||||
abstract listVMs(): Promise<Array<Record<string, any>>>;
|
||||
abstract runVM(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: Record<string, any>,
|
||||
storage?: string
|
||||
): Promise<Record<string, any>>;
|
||||
abstract stopVM(name: string, storage?: string): Promise<void>;
|
||||
abstract getIP(name: string, storage?: string, retryDelay?: number): Promise<string>;
|
||||
abstract updateVM(
|
||||
name: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string
|
||||
): Promise<void>;
|
||||
|
||||
async __aenter__(): Promise<this> {
|
||||
// Default implementation - can be overridden
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async context manager exit.
|
||||
*
|
||||
* This method is called when exiting an async context manager block.
|
||||
* It handles proper cleanup of resources, including stopping any running containers.
|
||||
*/
|
||||
async __aexit__(
|
||||
_excType: any,
|
||||
_excVal: any,
|
||||
_excTb: any
|
||||
): Promise<void> {
|
||||
// Default implementation - can be overridden
|
||||
}
|
||||
}
|
||||
5
libs/computer/typescript/src/providers/cloud/index.ts
Normal file
5
libs/computer/typescript/src/providers/cloud/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Cloud VM provider implementation.
|
||||
*/
|
||||
|
||||
export { CloudProvider } from "./provider";
|
||||
68
libs/computer/typescript/src/providers/cloud/provider.ts
Normal file
68
libs/computer/typescript/src/providers/cloud/provider.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Cloud VM provider implementation.
|
||||
*
|
||||
* This provider is a placeholder for cloud-based VM provisioning.
|
||||
* It will be implemented to support cloud VM services in the future.
|
||||
*/
|
||||
|
||||
import { BaseVMProviderImpl, VMProviderType } from '../base';
|
||||
|
||||
export interface CloudProviderOptions {
|
||||
verbose?: boolean;
|
||||
apiKey?: string;
|
||||
region?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class CloudProvider extends BaseVMProviderImpl {
|
||||
readonly providerType = VMProviderType.CLOUD;
|
||||
|
||||
private verbose: boolean;
|
||||
private options: CloudProviderOptions;
|
||||
|
||||
constructor(options: CloudProviderOptions = {}) {
|
||||
super();
|
||||
this.verbose = options.verbose || false;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
// TODO: Implement getVM for cloud provider
|
||||
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
|
||||
// TODO: Implement listVMs for cloud provider
|
||||
async listVMs(): Promise<Array<Record<string, any>>> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
|
||||
// TODO: Implement runVM for cloud provider
|
||||
async runVM(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: Record<string, any>,
|
||||
storage?: string
|
||||
): Promise<Record<string, any>> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
|
||||
// TODO: Implement stopVM for cloud provider
|
||||
async stopVM(name: string, storage?: string): Promise<void> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
|
||||
// TODO: Implement getIP for cloud provider
|
||||
async getIP(name: string, storage?: string, retryDelay?: number): Promise<string> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
|
||||
// TODO: Implement updateVM for cloud provider
|
||||
async updateVM(
|
||||
name: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string
|
||||
): Promise<void> {
|
||||
throw new Error('CloudProvider is not fully implemented yet. Please use LUME or LUMIER provider instead.');
|
||||
}
|
||||
}
|
||||
150
libs/computer/typescript/src/providers/factory.ts
Normal file
150
libs/computer/typescript/src/providers/factory.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Factory for creating VM providers.
|
||||
*/
|
||||
|
||||
import type { BaseVMProvider } from './base';
|
||||
import { VMProviderType } from './base';
|
||||
|
||||
export interface VMProviderOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
binPath?: string;
|
||||
storage?: string;
|
||||
sharedPath?: string;
|
||||
image?: string;
|
||||
verbose?: boolean;
|
||||
ephemeral?: boolean;
|
||||
noVNCPort?: number;
|
||||
[key: string]: any; // Allow additional provider-specific options
|
||||
}
|
||||
|
||||
export class VMProviderFactory {
|
||||
/**
|
||||
* Create a VM provider instance based on the provider type.
|
||||
*
|
||||
* @param providerType The type of provider to create
|
||||
* @param options Provider-specific options
|
||||
* @returns The created VM provider instance
|
||||
* @throws Error if the provider type is not supported or dependencies are missing
|
||||
*/
|
||||
static async createProvider(
|
||||
providerType: VMProviderType | string,
|
||||
options: VMProviderOptions = {}
|
||||
): Promise<BaseVMProvider> {
|
||||
// Convert string to enum if needed
|
||||
let type: VMProviderType;
|
||||
if (typeof providerType === 'string') {
|
||||
const normalizedType = providerType.toLowerCase();
|
||||
if (Object.values(VMProviderType).includes(normalizedType as VMProviderType)) {
|
||||
type = normalizedType as VMProviderType;
|
||||
} else {
|
||||
type = VMProviderType.UNKNOWN;
|
||||
}
|
||||
} else {
|
||||
type = providerType;
|
||||
}
|
||||
|
||||
// Extract common options with defaults
|
||||
const {
|
||||
port = 7777,
|
||||
host = 'localhost',
|
||||
binPath,
|
||||
storage,
|
||||
sharedPath,
|
||||
image,
|
||||
verbose = false,
|
||||
ephemeral = false,
|
||||
noVNCPort,
|
||||
...additionalOptions
|
||||
} = options;
|
||||
|
||||
switch (type) {
|
||||
case VMProviderType.LUME: {
|
||||
try {
|
||||
// Dynamic import for Lume provider
|
||||
const { LumeProvider, HAS_LUME } = await import('./lume');
|
||||
|
||||
if (!HAS_LUME) {
|
||||
throw new Error(
|
||||
'The required dependencies for LumeProvider are not available. ' +
|
||||
'Please ensure curl is installed and in your PATH.'
|
||||
);
|
||||
}
|
||||
|
||||
return new LumeProvider({
|
||||
port,
|
||||
host,
|
||||
storage,
|
||||
verbose,
|
||||
ephemeral
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
||||
throw new Error(
|
||||
'The LumeProvider module is not available. ' +
|
||||
'Please install it with: npm install @cua/computer-lume'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
case VMProviderType.LUMIER: {
|
||||
try {
|
||||
// Dynamic import for Lumier provider
|
||||
const { LumierProvider, HAS_LUMIER } = await import('./lumier');
|
||||
|
||||
if (!HAS_LUMIER) {
|
||||
throw new Error(
|
||||
'Docker is required for LumierProvider. ' +
|
||||
'Please install Docker for Apple Silicon and Lume CLI before using this provider.'
|
||||
);
|
||||
}
|
||||
|
||||
return new LumierProvider({
|
||||
port,
|
||||
host,
|
||||
storage,
|
||||
sharedPath,
|
||||
image: image || 'macos-sequoia-cua:latest',
|
||||
verbose,
|
||||
ephemeral,
|
||||
noVNCPort
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
||||
throw new Error(
|
||||
'The LumierProvider module is not available. ' +
|
||||
'Docker and Lume CLI are required for LumierProvider. ' +
|
||||
'Please install Docker for Apple Silicon and run the Lume installer script.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
case VMProviderType.CLOUD: {
|
||||
try {
|
||||
// Dynamic import for Cloud provider
|
||||
const { CloudProvider } = await import('./cloud');
|
||||
|
||||
return new CloudProvider({
|
||||
verbose,
|
||||
...additionalOptions
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
||||
throw new Error(
|
||||
'The CloudProvider is not fully implemented yet. ' +
|
||||
'Please use LUME or LUMIER provider instead.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${providerType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
libs/computer/typescript/src/providers/index.ts
Normal file
12
libs/computer/typescript/src/providers/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Export all provider-related modules.
|
||||
*/
|
||||
|
||||
export * from './base';
|
||||
export * from './factory';
|
||||
export * from './lume_api';
|
||||
|
||||
// Export provider implementations
|
||||
export * from './lume';
|
||||
export * from './lumier';
|
||||
export * from './cloud';
|
||||
16
libs/computer/typescript/src/providers/lume/index.ts
Normal file
16
libs/computer/typescript/src/providers/lume/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Lume VM provider implementation.
|
||||
*/
|
||||
|
||||
export let HAS_LUME = false;
|
||||
|
||||
try {
|
||||
// Check if curl is available
|
||||
const { execSync } = require('child_process');
|
||||
execSync('which curl', { stdio: 'ignore' });
|
||||
HAS_LUME = true;
|
||||
} catch {
|
||||
HAS_LUME = false;
|
||||
}
|
||||
|
||||
export { LumeProvider } from './provider';
|
||||
182
libs/computer/typescript/src/providers/lume/provider.ts
Normal file
182
libs/computer/typescript/src/providers/lume/provider.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Lume VM provider implementation using curl commands.
|
||||
*
|
||||
* This provider uses direct curl commands to interact with the Lume API,
|
||||
* removing the dependency on the pylume Python package.
|
||||
*/
|
||||
|
||||
import { BaseVMProviderImpl, VMProviderType } from '../base';
|
||||
import type { LumeRunOptions } from '../lume_api';
|
||||
import {
|
||||
HAS_CURL,
|
||||
lumeApiGet,
|
||||
lumeApiRun,
|
||||
lumeApiStop,
|
||||
lumeApiUpdate,
|
||||
lumeApiPull,
|
||||
parseMemory
|
||||
} from '../lume_api';
|
||||
|
||||
export interface LumeProviderOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
storage?: string;
|
||||
verbose?: boolean;
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export class LumeProvider extends BaseVMProviderImpl {
|
||||
readonly providerType = VMProviderType.LUME;
|
||||
|
||||
private host: string;
|
||||
private port: number;
|
||||
private storage?: string;
|
||||
private verbose: boolean;
|
||||
private ephemeral: boolean;
|
||||
|
||||
constructor(options: LumeProviderOptions = {}) {
|
||||
super();
|
||||
|
||||
if (!HAS_CURL) {
|
||||
throw new Error(
|
||||
'curl is required for LumeProvider. ' +
|
||||
'Please ensure it is installed and in your PATH.'
|
||||
);
|
||||
}
|
||||
|
||||
this.host = options.host || 'localhost';
|
||||
this.port = options.port || 7777;
|
||||
this.storage = options.storage;
|
||||
this.verbose = options.verbose || false;
|
||||
this.ephemeral = options.ephemeral || false;
|
||||
}
|
||||
|
||||
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
|
||||
return lumeApiGet(
|
||||
name,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.port,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
|
||||
async listVMs(): Promise<Array<Record<string, any>>> {
|
||||
const response = await lumeApiGet(
|
||||
'',
|
||||
this.storage,
|
||||
this.host,
|
||||
this.port,
|
||||
this.verbose
|
||||
);
|
||||
|
||||
// The response should be an array of VMs
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// If it's an object with a vms property
|
||||
if (response.vms && Array.isArray(response.vms)) {
|
||||
return response.vms;
|
||||
}
|
||||
|
||||
// Otherwise return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async runVM(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: LumeRunOptions,
|
||||
storage?: string
|
||||
): Promise<Record<string, any>> {
|
||||
// Ensure the image is available
|
||||
if (this.verbose) {
|
||||
console.log(`Pulling image ${image} if needed...`);
|
||||
}
|
||||
|
||||
try {
|
||||
await lumeApiPull(image, this.host, this.port, this.verbose);
|
||||
} catch (error) {
|
||||
if (this.verbose) {
|
||||
console.log(`Failed to pull image: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the VM
|
||||
return lumeApiRun(
|
||||
image,
|
||||
name,
|
||||
runOpts,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.port,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
|
||||
async stopVM(name: string, storage?: string): Promise<void> {
|
||||
await lumeApiStop(
|
||||
name,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.port,
|
||||
this.verbose
|
||||
);
|
||||
|
||||
// If ephemeral, the VM should be automatically deleted after stopping
|
||||
if (this.ephemeral && this.verbose) {
|
||||
console.log(`VM ${name} stopped and removed (ephemeral mode)`);
|
||||
}
|
||||
}
|
||||
|
||||
async getIP(name: string, storage?: string, retryDelay: number = 1): Promise<string> {
|
||||
const maxRetries = 30;
|
||||
let retries = 0;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
const vmInfo = await this.getVM(name, storage);
|
||||
|
||||
if (vmInfo.ip && vmInfo.ip !== '') {
|
||||
return vmInfo.ip;
|
||||
}
|
||||
|
||||
if (vmInfo.status === 'stopped' || vmInfo.status === 'error') {
|
||||
throw new Error(`VM ${name} is in ${vmInfo.status} state`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (retries === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
retries++;
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay * 1000));
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get IP for VM ${name} after ${maxRetries} retries`);
|
||||
}
|
||||
|
||||
async updateVM(
|
||||
name: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string
|
||||
): Promise<void> {
|
||||
// Validate memory format if provided
|
||||
if (memory) {
|
||||
parseMemory(memory); // This will throw if invalid
|
||||
}
|
||||
|
||||
await lumeApiUpdate(
|
||||
name,
|
||||
cpu,
|
||||
memory,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.port,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
}
|
||||
265
libs/computer/typescript/src/providers/lume_api.ts
Normal file
265
libs/computer/typescript/src/providers/lume_api.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Lume API utilities for interacting with the Lume VM API.
|
||||
* This module provides low-level API functions used by the Lume provider.
|
||||
*/
|
||||
|
||||
import { exec, execSync } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export let HAS_CURL = false;
|
||||
|
||||
// Check for curl availability
|
||||
try {
|
||||
execSync("which curl", { stdio: "ignore" });
|
||||
HAS_CURL = true;
|
||||
} catch {
|
||||
HAS_CURL = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse memory string to bytes.
|
||||
* Supports formats like "2GB", "512MB", "1024KB", etc.
|
||||
* Defaults to 1GB
|
||||
*/
|
||||
export function parseMemory(memory = "1GB"): number {
|
||||
const match = memory.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B?)$/i);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid memory format: ${memory}`);
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]!);
|
||||
const unit = match[2]!.toUpperCase();
|
||||
|
||||
const multipliers: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
TB: 1024 * 1024 * 1024 * 1024,
|
||||
K: 1024,
|
||||
M: 1024 * 1024,
|
||||
G: 1024 * 1024 * 1024,
|
||||
T: 1024 * 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
return Math.floor(value * (multipliers[unit] || 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a curl command and return the result.
|
||||
*/
|
||||
async function executeCurl(
|
||||
command: string
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
return { stdout, stderr };
|
||||
} catch (error: any) {
|
||||
throw new Error(`Curl command failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get VM information using Lume API.
|
||||
*/
|
||||
export async function lumeApiGet(
|
||||
vmName: string = "",
|
||||
storage?: string,
|
||||
host: string = "localhost",
|
||||
port: number = 7777,
|
||||
debug: boolean = false
|
||||
): Promise<Record<string, any>> {
|
||||
let url = `http://${host}:${port}/vms`;
|
||||
if (vmName) {
|
||||
url += `/${encodeURIComponent(vmName)}`;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (storage) {
|
||||
params.append("storage", storage);
|
||||
}
|
||||
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const command = `curl -s -X GET "${url}"`;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
const { stdout } = await executeCurl(command);
|
||||
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse API response: ${stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for running a VM using the Lume API.
|
||||
*/
|
||||
export interface LumeRunOptions {
|
||||
/** CPU cores to allocate to the VM */
|
||||
cpu?: number;
|
||||
/** Memory to allocate to the VM (e.g., "8GB", "512MB") */
|
||||
memory?: string;
|
||||
/** Display configuration for the VM */
|
||||
display?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
dpi?: number;
|
||||
color_depth?: number;
|
||||
};
|
||||
/** Environment variables to set in the VM */
|
||||
env?: Record<string, string>;
|
||||
/** Directories to share with the VM */
|
||||
shared_directories?: Record<string, string>;
|
||||
/** Network configuration */
|
||||
network?: {
|
||||
type?: string;
|
||||
bridge?: string;
|
||||
nat?: boolean;
|
||||
};
|
||||
/** Whether to run the VM in headless mode */
|
||||
headless?: boolean;
|
||||
/** Whether to enable GPU acceleration */
|
||||
gpu?: boolean;
|
||||
/** Storage location for the VM */
|
||||
storage?: string;
|
||||
/** Custom VM configuration options */
|
||||
vm_options?: Record<string, any>;
|
||||
/** Additional provider-specific options */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a VM using Lume API.
|
||||
*/
|
||||
export async function lumeApiRun(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: LumeRunOptions,
|
||||
storage?: string,
|
||||
host: string = "localhost",
|
||||
port: number = 7777,
|
||||
debug: boolean = false
|
||||
): Promise<Record<string, any>> {
|
||||
const url = `http://${host}:${port}/vms/run`;
|
||||
|
||||
const body: LumeRunOptions = {
|
||||
image,
|
||||
name,
|
||||
...runOpts,
|
||||
};
|
||||
|
||||
if (storage) {
|
||||
body.storage = storage;
|
||||
}
|
||||
|
||||
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
|
||||
body
|
||||
)}'`;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
const { stdout } = await executeCurl(command);
|
||||
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse API response: ${stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a VM using Lume API.
|
||||
*/
|
||||
export async function lumeApiStop(
|
||||
vmName: string,
|
||||
storage?: string,
|
||||
host: string = "localhost",
|
||||
port: number = 7777,
|
||||
debug: boolean = false
|
||||
): Promise<void> {
|
||||
const url = `http://${host}:${port}/vms/${encodeURIComponent(vmName)}/stop`;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (storage) {
|
||||
params.append("storage", storage);
|
||||
}
|
||||
|
||||
const fullUrl = params.toString() ? `${url}?${params.toString()}` : url;
|
||||
const command = `curl -s -X POST "${fullUrl}"`;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
await executeCurl(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update VM settings using Lume API.
|
||||
*/
|
||||
export async function lumeApiUpdate(
|
||||
vmName: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string,
|
||||
host: string = "localhost",
|
||||
port: number = 7777,
|
||||
debug: boolean = false
|
||||
): Promise<void> {
|
||||
const url = `http://${host}:${port}/vms/${encodeURIComponent(vmName)}/update`;
|
||||
|
||||
const body: LumeRunOptions = {};
|
||||
if (cpu !== undefined) {
|
||||
body.cpu = cpu;
|
||||
}
|
||||
if (memory !== undefined) {
|
||||
body.memory = memory;
|
||||
}
|
||||
if (storage) {
|
||||
body.storage = storage;
|
||||
}
|
||||
|
||||
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
|
||||
body
|
||||
)}'`;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
await executeCurl(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a VM image using Lume API.
|
||||
*/
|
||||
export async function lumeApiPull(
|
||||
image: string,
|
||||
host: string = "localhost",
|
||||
port: number = 7777,
|
||||
debug: boolean = false
|
||||
): Promise<void> {
|
||||
const url = `http://${host}:${port}/images/pull`;
|
||||
|
||||
const body: LumeRunOptions = { image };
|
||||
const command = `curl -s -X POST "${url}" -H "Content-Type: application/json" -d '${JSON.stringify(
|
||||
body
|
||||
)}'`;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
await executeCurl(command);
|
||||
}
|
||||
16
libs/computer/typescript/src/providers/lumier/index.ts
Normal file
16
libs/computer/typescript/src/providers/lumier/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Lumier VM provider implementation.
|
||||
*/
|
||||
|
||||
export let HAS_LUMIER = false;
|
||||
|
||||
try {
|
||||
// Check if Docker is available
|
||||
const { execSync } = require("child_process");
|
||||
execSync("which docker", { stdio: "ignore" });
|
||||
HAS_LUMIER = true;
|
||||
} catch {
|
||||
HAS_LUMIER = false;
|
||||
}
|
||||
|
||||
export { LumierProvider } from "./provider";
|
||||
401
libs/computer/typescript/src/providers/lumier/provider.ts
Normal file
401
libs/computer/typescript/src/providers/lumier/provider.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Lumier VM provider implementation.
|
||||
*
|
||||
* This provider uses Docker containers running the Lumier image to create
|
||||
* macOS and Linux VMs. It handles VM lifecycle operations through Docker
|
||||
* commands and container management.
|
||||
*/
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { BaseVMProviderImpl, VMProviderType } from "../base";
|
||||
import {
|
||||
lumeApiGet,
|
||||
lumeApiRun,
|
||||
lumeApiStop,
|
||||
lumeApiUpdate,
|
||||
} from "../lume_api";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface LumierProviderOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
storage?: string;
|
||||
sharedPath?: string;
|
||||
image?: string;
|
||||
verbose?: boolean;
|
||||
ephemeral?: boolean;
|
||||
noVNCPort?: number;
|
||||
}
|
||||
|
||||
export class LumierProvider extends BaseVMProviderImpl {
|
||||
readonly providerType = VMProviderType.LUMIER;
|
||||
|
||||
private host: string;
|
||||
private apiPort: number;
|
||||
private vncPort?: number;
|
||||
private ephemeral: boolean;
|
||||
private storage?: string;
|
||||
private sharedPath?: string;
|
||||
private image: string;
|
||||
private verbose: boolean;
|
||||
private containerName?: string;
|
||||
private containerId?: string;
|
||||
|
||||
constructor(options: LumierProviderOptions = {}) {
|
||||
super();
|
||||
|
||||
this.host = options.host || "localhost";
|
||||
this.apiPort = options.port || 7777;
|
||||
this.vncPort = options.noVNCPort;
|
||||
this.ephemeral = options.ephemeral || false;
|
||||
|
||||
// Handle ephemeral storage
|
||||
if (this.ephemeral) {
|
||||
this.storage = "ephemeral";
|
||||
} else {
|
||||
this.storage = options.storage;
|
||||
}
|
||||
|
||||
this.sharedPath = options.sharedPath;
|
||||
this.image = options.image || "macos-sequoia-cua:latest";
|
||||
this.verbose = options.verbose || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse memory string to MB integer.
|
||||
*/
|
||||
private parseMemory(memoryStr: string | number): number {
|
||||
if (typeof memoryStr === "number") {
|
||||
return memoryStr;
|
||||
}
|
||||
|
||||
const match = memoryStr.match(/^(\d+)([A-Za-z]*)$/);
|
||||
if (match) {
|
||||
const value = parseInt(match[1] || "0");
|
||||
const unit = match[2]?.toUpperCase() || "";
|
||||
|
||||
if (unit === "GB" || unit === "G") {
|
||||
return value * 1024;
|
||||
} else if (unit === "MB" || unit === "M" || unit === "") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Could not parse memory string '${memoryStr}', using 8GB default`
|
||||
);
|
||||
return 8192; // Default to 8GB
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Docker container exists.
|
||||
*/
|
||||
private async containerExists(name: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync(`docker inspect ${name}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container status.
|
||||
*/
|
||||
private async getContainerStatus(name: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`docker inspect -f '{{.State.Status}}' ${name}`
|
||||
);
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return "not_found";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Lumier container.
|
||||
*/
|
||||
private async startContainer(
|
||||
name: string,
|
||||
cpu: number = 4,
|
||||
memory: string = "8GB",
|
||||
runOpts: Record<string, any> = {}
|
||||
): Promise<void> {
|
||||
const memoryMB = this.parseMemory(memory);
|
||||
|
||||
// Build Docker run command
|
||||
let dockerCmd = `docker run -d --name ${name}`;
|
||||
|
||||
// Add resource limits
|
||||
dockerCmd += ` --cpus=${cpu}`;
|
||||
dockerCmd += ` --memory=${memoryMB}m`;
|
||||
|
||||
// Add port mappings
|
||||
dockerCmd += ` -p ${this.apiPort}:7777`;
|
||||
if (this.vncPort) {
|
||||
dockerCmd += ` -p ${this.vncPort}:8006`;
|
||||
}
|
||||
|
||||
// Add storage volume if not ephemeral
|
||||
if (this.storage && this.storage !== "ephemeral") {
|
||||
dockerCmd += ` -v ${this.storage}:/storage`;
|
||||
}
|
||||
|
||||
// Add shared path if specified
|
||||
if (this.sharedPath) {
|
||||
dockerCmd += ` -v ${this.sharedPath}:/shared`;
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
if (runOpts.env) {
|
||||
for (const [key, value] of Object.entries(runOpts.env)) {
|
||||
dockerCmd += ` -e ${key}=${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the image
|
||||
dockerCmd += ` ${this.image}`;
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`Starting container with command: ${dockerCmd}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(dockerCmd);
|
||||
this.containerId = stdout.trim();
|
||||
this.containerName = name;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to start container: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the API to be ready.
|
||||
*/
|
||||
private async waitForAPI(maxRetries: number = 30): Promise<void> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await lumeApiGet("", undefined, this.host, this.apiPort, false);
|
||||
return;
|
||||
} catch {
|
||||
if (i === maxRetries - 1) {
|
||||
throw new Error("API failed to become ready");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getVM(name: string, storage?: string): Promise<Record<string, any>> {
|
||||
// First check if container exists
|
||||
const containerStatus = await this.getContainerStatus(
|
||||
this.containerName || name
|
||||
);
|
||||
|
||||
if (containerStatus === "not_found") {
|
||||
throw new Error(`Container ${name} not found`);
|
||||
}
|
||||
|
||||
// If container is not running, return basic status
|
||||
if (containerStatus !== "running") {
|
||||
return {
|
||||
name,
|
||||
status: containerStatus,
|
||||
ip: "",
|
||||
cpu: 0,
|
||||
memory: "0MB",
|
||||
};
|
||||
}
|
||||
|
||||
// Get VM info from API
|
||||
return lumeApiGet(
|
||||
name,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
|
||||
async listVMs(): Promise<Array<Record<string, any>>> {
|
||||
// Check if our container is running
|
||||
if (!this.containerName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const containerStatus = await this.getContainerStatus(this.containerName);
|
||||
if (containerStatus !== "running") {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get VMs from API
|
||||
const response = await lumeApiGet(
|
||||
"",
|
||||
this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (response.vms && Array.isArray(response.vms)) {
|
||||
return response.vms;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async runVM(
|
||||
image: string,
|
||||
name: string,
|
||||
runOpts: Record<string, any>,
|
||||
storage?: string
|
||||
): Promise<Record<string, any>> {
|
||||
// Check if container already exists
|
||||
const exists = await this.containerExists(name);
|
||||
|
||||
if (exists) {
|
||||
const status = await this.getContainerStatus(name);
|
||||
if (status === "running") {
|
||||
// Container already running, just run VM through API
|
||||
return lumeApiRun(
|
||||
image,
|
||||
name,
|
||||
runOpts,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
} else {
|
||||
// Start existing container
|
||||
await execAsync(`docker start ${name}`);
|
||||
}
|
||||
} else {
|
||||
// Create and start new container
|
||||
await this.startContainer(
|
||||
name,
|
||||
runOpts.cpu || 4,
|
||||
runOpts.memory || "8GB",
|
||||
runOpts
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for API to be ready
|
||||
await this.waitForAPI();
|
||||
|
||||
// Run VM through API
|
||||
return lumeApiRun(
|
||||
image,
|
||||
name,
|
||||
runOpts,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
|
||||
async stopVM(name: string, storage?: string): Promise<void> {
|
||||
// First stop VM through API
|
||||
try {
|
||||
await lumeApiStop(
|
||||
name,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
} catch (error) {
|
||||
if (this.verbose) {
|
||||
console.log(`Failed to stop VM through API: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the container
|
||||
if (this.containerName) {
|
||||
try {
|
||||
await execAsync(`docker stop ${this.containerName}`);
|
||||
|
||||
// Remove container if ephemeral
|
||||
if (this.ephemeral) {
|
||||
await execAsync(`docker rm ${this.containerName}`);
|
||||
if (this.verbose) {
|
||||
console.log(
|
||||
`Container ${this.containerName} stopped and removed (ephemeral mode)`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to stop container: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getIP(
|
||||
name: string,
|
||||
storage?: string,
|
||||
retryDelay: number = 1
|
||||
): Promise<string> {
|
||||
const maxRetries = 30;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const vmInfo = await this.getVM(name, storage);
|
||||
|
||||
if (vmInfo.ip && vmInfo.ip !== "") {
|
||||
return vmInfo.ip;
|
||||
}
|
||||
|
||||
if (vmInfo.status === "stopped" || vmInfo.status === "error") {
|
||||
throw new Error(`VM ${name} is in ${vmInfo.status} state`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay * 1000));
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to get IP for VM ${name} after ${maxRetries} retries`
|
||||
);
|
||||
}
|
||||
|
||||
async updateVM(
|
||||
name: string,
|
||||
cpu?: number,
|
||||
memory?: string,
|
||||
storage?: string
|
||||
): Promise<void> {
|
||||
await lumeApiUpdate(
|
||||
name,
|
||||
cpu,
|
||||
memory,
|
||||
storage || this.storage,
|
||||
this.host,
|
||||
this.apiPort,
|
||||
this.verbose
|
||||
);
|
||||
}
|
||||
|
||||
async __aexit__(excType: any, excVal: any, excTb: any): Promise<void> {
|
||||
// Clean up container if ephemeral
|
||||
if (this.ephemeral && this.containerName) {
|
||||
try {
|
||||
await execAsync(`docker stop ${this.containerName}`);
|
||||
await execAsync(`docker rm ${this.containerName}`);
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
libs/computer/typescript/src/telemetry.ts
Normal file
115
libs/computer/typescript/src/telemetry.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Telemetry tracking for Computer usage.
|
||||
*/
|
||||
|
||||
interface TelemetryEvent {
|
||||
event: string;
|
||||
timestamp: Date;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class TelemetryManager {
|
||||
private enabled: boolean = true;
|
||||
private events: TelemetryEvent[] = [];
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
track(event: string, properties?: Record<string, any>): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telemetryEvent: TelemetryEvent = {
|
||||
event,
|
||||
timestamp: new Date(),
|
||||
properties,
|
||||
};
|
||||
|
||||
this.events.push(telemetryEvent);
|
||||
|
||||
// In a real implementation, this would send to a telemetry service
|
||||
// For now, just log to debug
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("[Telemetry]", event, properties);
|
||||
}
|
||||
}
|
||||
|
||||
getEvents(): TelemetryEvent[] {
|
||||
return [...this.events];
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.events = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const telemetryManager = new TelemetryManager();
|
||||
|
||||
/**
|
||||
* Record computer initialization event
|
||||
*/
|
||||
export function recordComputerInitialization(): void {
|
||||
telemetryManager.track("computer_initialized", {
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || "unknown",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record VM start event
|
||||
*/
|
||||
export function recordVMStart(vmName: string, provider: string): void {
|
||||
telemetryManager.track("vm_started", {
|
||||
vm_name: vmName,
|
||||
provider,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record VM stop event
|
||||
*/
|
||||
export function recordVMStop(vmName: string, duration: number): void {
|
||||
telemetryManager.track("vm_stopped", {
|
||||
vm_name: vmName,
|
||||
duration_ms: duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record interface action
|
||||
*/
|
||||
export function recordInterfaceAction(
|
||||
action: string,
|
||||
details?: Record<string, any>
|
||||
): void {
|
||||
telemetryManager.track("interface_action", {
|
||||
action,
|
||||
...details,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set telemetry enabled/disabled
|
||||
*/
|
||||
export function setTelemetryEnabled(enabled: boolean): void {
|
||||
telemetryManager.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled
|
||||
*/
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
return telemetryManager.isEnabled();
|
||||
}
|
||||
|
||||
export { telemetryManager };
|
||||
118
libs/computer/typescript/src/utils.ts
Normal file
118
libs/computer/typescript/src/utils.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Utility functions for the Computer module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sleep for a specified number of milliseconds
|
||||
* @param ms Number of milliseconds to sleep
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a display string in format "WIDTHxHEIGHT"
|
||||
* @param display Display string to parse
|
||||
* @returns Object with width and height
|
||||
*/
|
||||
export function parseDisplayString(display: string): {
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const match = display.match(/^(\d+)x(\d+)$/);
|
||||
if (!match || !match[1] || !match[2]) {
|
||||
throw new Error(
|
||||
"Display string must be in format 'WIDTHxHEIGHT' (e.g. '1024x768')"
|
||||
);
|
||||
}
|
||||
return {
|
||||
width: parseInt(match[1], 10),
|
||||
height: parseInt(match[2], 10),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate image format (should be in format "image:tag")
|
||||
* @param image Image string to validate
|
||||
* @returns Object with image name and tag
|
||||
*/
|
||||
export function parseImageString(image: string): { name: string; tag: string } {
|
||||
const parts = image.split(":");
|
||||
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error("Image must be in the format <image_name>:<tag>");
|
||||
}
|
||||
return {
|
||||
name: parts[0],
|
||||
tag: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to human-readable format
|
||||
* @param bytes Number of bytes
|
||||
* @returns Human-readable string
|
||||
*/
|
||||
export function formatBytes(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse memory string (e.g., "8GB") to bytes
|
||||
* @param memory Memory string
|
||||
* @returns Number of bytes
|
||||
*/
|
||||
export function parseMemoryString(memory: string): number {
|
||||
const match = memory.match(/^(\d+)(B|KB|MB|GB|TB)?$/i);
|
||||
if (!match || !match[1] || !match[2]) {
|
||||
throw new Error("Invalid memory format. Use format like '8GB' or '1024MB'");
|
||||
}
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
const multipliers: Record<string, number> = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
TB: 1024 * 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
return value * (multipliers[unit] || 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout promise that rejects after specified milliseconds
|
||||
* @param ms Timeout in milliseconds
|
||||
* @param message Optional error message
|
||||
*/
|
||||
export function timeout<T>(ms: number, message?: string): Promise<T> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(message || `Timeout after ${ms}ms`));
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Race a promise against a timeout
|
||||
* @param promise The promise to race
|
||||
* @param ms Timeout in milliseconds
|
||||
* @param message Optional timeout error message
|
||||
*/
|
||||
export async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
ms: number,
|
||||
message?: string
|
||||
): Promise<T> {
|
||||
return Promise.race([promise, timeout<T>(ms, message)]);
|
||||
}
|
||||
22
libs/computer/typescript/tsconfig.json
Normal file
22
libs/computer/typescript/tsconfig.json
Normal 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"]
|
||||
}
|
||||
10
libs/computer/typescript/tsdown.config.ts
Normal file
10
libs/computer/typescript/tsdown.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ["./src/index.ts"],
|
||||
platform: "node",
|
||||
dts: true,
|
||||
external: ["child_process", "util"],
|
||||
},
|
||||
]);
|
||||
3
libs/computer/typescript/vitest.config.ts
Normal file
3
libs/computer/typescript/vitest.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({})
|
||||
Reference in New Issue
Block a user