UNPKG

tinyagent

Version:

Connect your local shell to any device - access your dev environment from anywhere

304 lines 12.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShellClient = void 0; const ws_1 = __importDefault(require("ws")); const child_process_1 = require("child_process"); const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const path = __importStar(require("path")); const shared_types_1 = require("@remote-shell/shared-types"); const tunnel_manager_1 = require("./tunnel-manager"); class ShellClient { options; ws; shellProcess; serverProcess; tunnelManager; heartbeatInterval; reconnectTimeout; isConnected = false; spinner = (0, ora_1.default)(); terminalBuffer = ''; currentSize = { rows: 24, cols: 80 }; constructor(options) { this.options = options; if (options.createTunnel !== false) { this.tunnelManager = new tunnel_manager_1.TunnelManager(); } } async connect() { this.spinner.start('Connecting to relay server...'); const wsUrl = `${this.options.relayUrl}/ws/${this.options.sessionId}`; this.ws = new ws_1.default(wsUrl); this.ws.on('open', () => { this.spinner.succeed('Connected to relay server'); this.isConnected = true; this.sendMessage({ type: shared_types_1.MessageType.SESSION_INIT, sessionId: this.options.sessionId, timestamp: Date.now(), clientType: 'shell' }); this.startHeartbeat(); }); this.ws.on('message', (data) => { this.handleMessage(JSON.parse(data.toString())); }); this.ws.on('close', () => { this.isConnected = false; console.log(chalk_1.default.yellow('Connection closed')); this.cleanup(); this.scheduleReconnect(); }); this.ws.on('error', (error) => { this.spinner.fail(`WebSocket error: ${error.message}`); this.cleanup(); }); } handleMessage(message) { switch (message.type) { case shared_types_1.MessageType.SESSION_READY: console.log(chalk_1.default.green('Session ready')); this.startShell(); if (this.options.serverCommand) { this.startServer(); } break; case shared_types_1.MessageType.SHELL_DATA: const dataMsg = message; console.log(chalk_1.default.cyan(`[RECEIVED FROM MOBILE] ${JSON.stringify(dataMsg.data)}`)); if (this.shellProcess && this.shellProcess.stdin && !this.shellProcess.stdin.destroyed) { try { // Write raw data to Python PTY wrapper stdin const success = this.shellProcess.stdin.write(dataMsg.data); if (success) { console.log(chalk_1.default.green(`[SENT TO SHELL] ${JSON.stringify(dataMsg.data)}`)); } else { console.log(chalk_1.default.yellow('[WARNING] Shell stdin buffer full, data queued')); } } catch (error) { console.log(chalk_1.default.red(`[ERROR] Failed to write to shell: ${error}`)); } } else { console.log(chalk_1.default.red('[ERROR] Shell process stdin not available or destroyed')); } break; case shared_types_1.MessageType.SHELL_RESIZE: const resizeMsg = message; console.log(chalk_1.default.blue(`[INFO] Terminal resize: ${resizeMsg.cols}x${resizeMsg.rows}`)); this.currentSize = { rows: resizeMsg.rows, cols: resizeMsg.cols }; // For now, just store the size - we'll recreate the shell with proper size on reconnect // TODO: Implement proper resize handling with ioctl break; case shared_types_1.MessageType.COMMAND: const cmdMsg = message; this.handleCommand(cmdMsg); break; case shared_types_1.MessageType.SESSION_ERROR: console.error(chalk_1.default.red(`Session error: ${message.error}`)); break; } } startShell() { if (this.shellProcess) { console.log(chalk_1.default.yellow('Shell already started, sending current buffer to new client')); // Send the current terminal buffer to the newly connected client if (this.terminalBuffer) { this.sendMessage({ type: shared_types_1.MessageType.SHELL_DATA, sessionId: this.options.sessionId, timestamp: Date.now(), data: this.terminalBuffer }); } return; } console.log(chalk_1.default.blue(`Starting shell: ${this.options.shell}`)); // Use Python PTY wrapper with proper stdin handling const pythonScript = path.join(__dirname, '..', 'src', 'pty-wrapper.py'); // Let the PTY wrapper handle shell arguments for proper login shell this.shellProcess = (0, child_process_1.spawn)('python3', [pythonScript, this.options.shell], { env: { ...process.env, TERM: 'xterm-256color', LINES: String(this.currentSize.rows), COLUMNS: String(this.currentSize.cols), PYTHONUNBUFFERED: '1' }, stdio: ['pipe', 'pipe', 'pipe'] }); // Handle shell output this.shellProcess.stdout?.on('data', (data) => { const output = data.toString(); console.log(chalk_1.default.gray(`[SHELL OUTPUT] ${JSON.stringify(output)}`)); // Store in buffer for late-joining clients this.terminalBuffer += output; // Keep buffer size reasonable (last ~10KB) if (this.terminalBuffer.length > 10000) { this.terminalBuffer = this.terminalBuffer.slice(-10000); } this.sendMessage({ type: shared_types_1.MessageType.SHELL_DATA, sessionId: this.options.sessionId, timestamp: Date.now(), data: output }); }); this.shellProcess.stderr?.on('data', (data) => { console.error(chalk_1.default.red(`[SHELL ERROR] ${data.toString()}`)); }); this.shellProcess.on('exit', (code) => { console.log(chalk_1.default.yellow(`Shell exited with code ${code}`)); this.disconnect(); }); // Send initial terminal buffer if reconnecting if (this.terminalBuffer) { setTimeout(() => { this.sendMessage({ type: shared_types_1.MessageType.SHELL_DATA, sessionId: this.options.sessionId, timestamp: Date.now(), data: this.terminalBuffer }); }, 100); } } async startServer() { if (!this.options.serverCommand) return; console.log(chalk_1.default.blue(`Starting server: ${this.options.serverCommand}`)); this.serverProcess = (0, child_process_1.spawn)(this.options.serverCommand, [], { shell: true, env: { ...process.env, PORT: this.options.serverPort?.toString() } }); this.serverProcess.stdout?.on('data', (data) => { console.log(chalk_1.default.gray(`[SERVER] ${data.toString().trim()}`)); }); this.serverProcess.stderr?.on('data', (data) => { console.error(chalk_1.default.red(`[SERVER ERROR] ${data.toString().trim()}`)); }); if (this.tunnelManager && this.options.serverPort) { setTimeout(async () => { try { const tunnelUrl = await this.tunnelManager.createTunnel(this.options.serverPort); console.log(chalk_1.default.green(`Tunnel created: ${tunnelUrl}`)); this.sendMessage({ type: shared_types_1.MessageType.TUNNEL_URL, sessionId: this.options.sessionId, timestamp: Date.now(), url: tunnelUrl, port: this.options.serverPort }); } catch (error) { console.error(chalk_1.default.red(`Failed to create tunnel: ${error}`)); } }, 3000); // Wait for server to start } } handleCommand(message) { switch (message.command) { case 'start_server': if (!this.serverProcess) { this.startServer(); } break; case 'stop_server': if (this.serverProcess) { this.serverProcess.kill(); this.serverProcess = undefined; } break; case 'terminate': this.disconnect(); break; } } sendMessage(message) { if (this.ws && this.ws.readyState === ws_1.default.OPEN) { this.ws.send(JSON.stringify(message)); } } startHeartbeat() { this.heartbeatInterval = setInterval(() => { this.sendMessage({ type: shared_types_1.MessageType.HEARTBEAT, sessionId: this.options.sessionId, timestamp: Date.now() }); }, shared_types_1.HEARTBEAT_INTERVAL); } scheduleReconnect() { if (this.reconnectTimeout) return; this.reconnectTimeout = setTimeout(() => { console.log(chalk_1.default.blue('Attempting to reconnect...')); this.reconnectTimeout = undefined; this.connect(); }, shared_types_1.RECONNECT_DELAY); } cleanup() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } if (this.shellProcess) { this.shellProcess.kill(); } if (this.serverProcess) { this.serverProcess.kill(); } if (this.tunnelManager) { this.tunnelManager.closeTunnel(); } } disconnect() { this.cleanup(); if (this.ws) { this.ws.close(); } } } exports.ShellClient = ShellClient; //# sourceMappingURL=shell-client.js.map