import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { detectPlatform, getBinaryName, getBinaryPath } from './platform'; import { SnapshotParams, SnapshotResponse, TabClickParams, TabLockParams, TabUnlockParams, CreateTabParams, CreateTabResponse, PinchtabOptions, } from './types'; export * from './types'; export * from './platform'; export class Pinchtab { private baseUrl: string; private timeout: number; private port: number; private process: ChildProcess | null = null; private binaryPath: string | null = null; constructor(options: PinchtabOptions = {}) { this.port = options.port || 9867; this.baseUrl = options.baseUrl || `http://localhost:${this.port}`; this.timeout = options.timeout || 30000; } /** * Start the Pinchtab server process */ async start(binaryPath?: string): Promise { if (this.process) { throw new Error('Pinchtab process already running'); } if (!binaryPath) { binaryPath = await this.getBinaryPathInternal(); } this.binaryPath = binaryPath; return new Promise((resolve, reject) => { this.process = spawn(binaryPath, ['serve', `--port=${this.port}`], { stdio: 'inherit', }); this.process.on('error', (err) => { reject(new Error(`Failed to start Pinchtab: ${err.message}`)); }); // Give the server a moment to start setTimeout(resolve, 500); }); } /** * Stop the Pinchtab server process */ async stop(): Promise { if (this.process) { return new Promise((resolve) => { this.process?.kill(); this.process = null; resolve(); }); } } /** * Take a snapshot of the current tab */ async snapshot(params?: SnapshotParams): Promise { return this.request('/snapshot', params); } /** * Click on a UI element */ async click(params: TabClickParams): Promise { await this.request('/tab/click', params); } /** * Lock a tab */ async lock(params: TabLockParams): Promise { await this.request('/tab/lock', params); } /** * Unlock a tab */ async unlock(params: TabUnlockParams): Promise { await this.request('/tab/unlock', params); } /** * Create a new tab */ async createTab(params: CreateTabParams): Promise { return this.request('/tab/create', params); } /** * Make a request to the Pinchtab API */ // eslint-disable-next-line @typescript-eslint/no-explicit-any private async request(path: string, body?: any): Promise { const url = `${this.baseUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal as AbortSignal, }); if (!response.ok) { const error = await response.text(); throw new Error(`${response.status}: ${error}`); } return response.json() as Promise; } finally { clearTimeout(timeoutId); } } /** * Get the path to the Pinchtab binary */ private async getBinaryPathInternal(): Promise { const platform = detectPlatform(); const binaryName = getBinaryName(platform); // Try version-specific path first const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8')); const binaryPath = getBinaryPath(binaryName, pkg.version); if (!fs.existsSync(binaryPath)) { throw new Error( `Pinchtab binary not found at ${binaryPath}.\n` + `Please run: npm rebuild pinchtab\n` + `Or set PINCHTAB_BINARY_PATH=/path/to/binary` ); } return binaryPath; } } export default Pinchtab;