vibetunnel/web/src/server/fwd.ts
Peter Steinberger d4b7962800
Refactor notification preferences system (#469)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alex Fallah <alexfallah7@gmail.com>
2025-07-27 13:32:11 +02:00

755 lines
28 KiB
TypeScript
Executable file

#!/usr/bin/env pnpm exec tsx --no-deprecation
/**
* VibeTunnel Forward (fwd.ts)
*
* A simple command-line tool that spawns a PTY session and forwards it
* using the VibeTunnel PTY infrastructure.
*
* Usage:
* pnpm exec tsx src/fwd.ts <command> [args...]
* pnpm exec tsx src/fwd.ts claude --resume
*/
import chalk from 'chalk';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { promisify } from 'util';
import { type SessionInfo, TitleMode } from '../shared/types.js';
import { PtyManager } from './pty/index.js';
import { SessionManager } from './pty/session-manager.js';
import { VibeTunnelSocketClient } from './pty/socket-client.js';
import { ActivityDetector } from './utils/activity-detector.js';
import { checkAndPatchClaude } from './utils/claude-patcher.js';
import { detectGitInfo } from './utils/git-info.js';
import {
closeLogger,
createLogger,
parseVerbosityLevel,
setLogFilePath,
setVerbosityLevel,
VerbosityLevel,
} from './utils/logger.js';
import { generateSessionName } from './utils/session-naming.js';
import { generateTitleSequence } from './utils/terminal-title.js';
import { parseVerbosityFromEnv } from './utils/verbosity-parser.js';
import { BUILD_DATE, GIT_COMMIT, VERSION } from './version.js';
const logger = createLogger('fwd');
const _execFile = promisify(require('child_process').execFile);
function showUsage() {
console.log(chalk.blue(`VibeTunnel Forward v${VERSION}`) + chalk.gray(` (${BUILD_DATE})`));
console.log('');
console.log('Usage:');
console.log(
' pnpm exec tsx src/fwd.ts [--session-id <id>] [--title-mode <mode>] [--verbosity <level>] <command> [args...]'
);
console.log('');
console.log('Options:');
console.log(' --session-id <id> Use a pre-generated session ID');
console.log(' --title-mode <mode> Terminal title mode: none, filter, static, dynamic');
console.log(' (defaults to none for most commands, dynamic for claude)');
console.log(' --update-title <title> Update session title and exit (requires --session-id)');
console.log(
' --verbosity <level> Set logging verbosity: silent, error, warn, info, verbose, debug'
);
console.log(' (defaults to error)');
console.log(' --log-file <path> Override default log file location');
console.log(' (defaults to ~/.vibetunnel/log.txt)');
console.log('');
console.log('Title Modes:');
console.log(' none - No title management (default)');
console.log(' filter - Block all title changes from applications');
console.log(' static - Show working directory and command');
console.log(' dynamic - Show directory, command, and activity (auto-selected for claude)');
console.log('');
console.log('Verbosity Levels:');
console.log(` ${chalk.gray('silent')} - No output except critical errors`);
console.log(` ${chalk.red('error')} - Only errors ${chalk.gray('(default)')}`);
console.log(` ${chalk.yellow('warn')} - Errors and warnings`);
console.log(` ${chalk.green('info')} - Errors, warnings, and informational messages`);
console.log(` ${chalk.blue('verbose')} - All messages except debug`);
console.log(` ${chalk.magenta('debug')} - All messages including debug`);
console.log('');
console.log(
`Quick verbosity: ${chalk.cyan('-q (quiet), -v (verbose), -vv (extra), -vvv (debug)')}`
);
console.log('');
console.log('Environment Variables:');
console.log(' VIBETUNNEL_TITLE_MODE=<mode> Set default title mode');
console.log(' VIBETUNNEL_CLAUDE_DYNAMIC_TITLE=1 Force dynamic title for Claude');
console.log(' VIBETUNNEL_LOG_LEVEL=<level> Set default verbosity level');
console.log(' VIBETUNNEL_DEBUG=1 Enable debug mode (legacy)');
console.log('');
console.log('Examples:');
console.log(' pnpm exec tsx src/fwd.ts claude --resume');
console.log(' pnpm exec tsx src/fwd.ts --title-mode static bash -l');
console.log(' pnpm exec tsx src/fwd.ts --title-mode filter vim');
console.log(' pnpm exec tsx src/fwd.ts --session-id abc123 claude');
console.log(' pnpm exec tsx src/fwd.ts --update-title "New Title" --session-id abc123');
console.log(' pnpm exec tsx src/fwd.ts --verbosity silent npm test');
console.log('');
console.log('The command will be spawned in the current working directory');
console.log('and managed through the VibeTunnel PTY infrastructure.');
}
export async function startVibeTunnelForward(args: string[]) {
// Parse verbosity from environment variables
let verbosityLevel = parseVerbosityFromEnv();
// Set debug mode on logger for backward compatibility
if (verbosityLevel === VerbosityLevel.DEBUG) {
logger.setDebugMode(true);
}
// Parse command line arguments
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
showUsage();
closeLogger();
process.exit(0);
}
logger.debug(chalk.blue(`VibeTunnel Forward v${VERSION}`) + chalk.gray(` (${BUILD_DATE})`));
logger.debug(`Full command: ${args.join(' ')}`);
// Parse command line arguments
let sessionId: string | undefined;
let titleMode: TitleMode = TitleMode.NONE;
let updateTitle: string | undefined;
let logFilePath: string | undefined;
let remainingArgs = args;
// Check environment variables for title mode
if (process.env.VIBETUNNEL_TITLE_MODE) {
const envMode = process.env.VIBETUNNEL_TITLE_MODE.toLowerCase();
if (Object.values(TitleMode).includes(envMode as TitleMode)) {
titleMode = envMode as TitleMode;
logger.debug(`Title mode set from environment: ${titleMode}`);
}
}
// Force dynamic mode for Claude via environment variable
if (
process.env.VIBETUNNEL_CLAUDE_DYNAMIC_TITLE === '1' ||
process.env.VIBETUNNEL_CLAUDE_DYNAMIC_TITLE === 'true'
) {
titleMode = TitleMode.DYNAMIC;
logger.debug('Forced dynamic title mode for Claude via environment variable');
}
// Parse flags
while (remainingArgs.length > 0) {
if (remainingArgs[0] === '--session-id' && remainingArgs.length > 1) {
sessionId = remainingArgs[1];
remainingArgs = remainingArgs.slice(2);
} else if (remainingArgs[0] === '--update-title' && remainingArgs.length > 1) {
updateTitle = remainingArgs[1];
remainingArgs = remainingArgs.slice(2);
} else if (remainingArgs[0] === '--title-mode' && remainingArgs.length > 1) {
const mode = remainingArgs[1].toLowerCase();
if (Object.values(TitleMode).includes(mode as TitleMode)) {
titleMode = mode as TitleMode;
} else {
logger.error(`Invalid title mode: ${remainingArgs[1]}`);
logger.error(`Valid modes: ${Object.values(TitleMode).join(', ')}`);
closeLogger();
process.exit(1);
}
remainingArgs = remainingArgs.slice(2);
} else if (remainingArgs[0] === '--verbosity' && remainingArgs.length > 1) {
const parsedLevel = parseVerbosityLevel(remainingArgs[1]);
if (parsedLevel !== undefined) {
verbosityLevel = parsedLevel;
} else {
logger.error(`Invalid verbosity level: ${remainingArgs[1]}`);
logger.error('Valid levels: silent, error, warn, info, verbose, debug');
closeLogger();
process.exit(1);
}
remainingArgs = remainingArgs.slice(2);
} else if (remainingArgs[0] === '--log-file' && remainingArgs.length > 1) {
logFilePath = remainingArgs[1];
remainingArgs = remainingArgs.slice(2);
} else {
// Not a flag, must be the start of the command
break;
}
}
// Handle -- separator (used by some shells as end-of-options marker)
// This allows commands like: fwd -- command-with-dashes
if (remainingArgs[0] === '--' && remainingArgs.length > 1) {
remainingArgs = remainingArgs.slice(1);
}
// Apply log file path if set
if (logFilePath !== undefined) {
setLogFilePath(logFilePath);
logger.debug(`Log file path set to: ${logFilePath}`);
}
// Apply verbosity level if set
if (verbosityLevel !== undefined) {
setVerbosityLevel(verbosityLevel);
if (verbosityLevel >= VerbosityLevel.INFO) {
logger.log(`Verbosity level set to: ${VerbosityLevel[verbosityLevel].toLowerCase()}`);
}
}
// Handle special case: --update-title mode
if (updateTitle !== undefined) {
if (!sessionId) {
logger.error('--update-title requires --session-id');
closeLogger();
process.exit(1);
}
// Initialize session manager
const controlPath = path.join(os.homedir(), '.vibetunnel', 'control');
const sessionManager = new SessionManager(controlPath);
// Validate session ID format for security
if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
logger.error(
`Invalid session ID format: "${sessionId}". Session IDs must only contain letters, numbers, hyphens (-), and underscores (_).`
);
closeLogger();
process.exit(1);
}
try {
// Load existing session info
const sessionInfo = sessionManager.loadSessionInfo(sessionId);
if (!sessionInfo) {
logger.error(`Session ${sessionId} not found`);
closeLogger();
process.exit(1);
}
// Sanitize the title - limit length and filter out problematic characters
const sanitizedTitle = updateTitle
.substring(0, 256) // Limit length
.split('')
.filter((char) => {
const code = char.charCodeAt(0);
// Allow printable characters (space to ~) and extended ASCII/Unicode
return code >= 32 && code !== 127 && (code < 128 || code > 159);
})
.join('');
// Update the title via IPC if session is active
const socketPath = path.join(controlPath, sessionId, 'ipc.sock');
// Check if IPC socket exists (session is active)
if (fs.existsSync(socketPath)) {
logger.debug(`IPC socket found, sending title update via IPC`);
// Connect to IPC socket and send update-title command
const socketClient = new VibeTunnelSocketClient(socketPath, {
autoReconnect: false, // One-shot operation
});
try {
await socketClient.connect();
// Send update-title command
const sent = socketClient.updateTitle(sanitizedTitle);
if (sent) {
logger.log(`Session title updated to: ${sanitizedTitle}`);
// IPC update succeeded, server will handle the file update
socketClient.disconnect();
closeLogger();
process.exit(0);
} else {
logger.warn(`Failed to send title update via IPC, falling back to file update`);
}
// Disconnect after sending
socketClient.disconnect();
} catch (ipcError) {
logger.warn(`IPC connection failed: ${ipcError}, falling back to file update`);
}
} else {
logger.debug(`No IPC socket found, session might not be active`);
}
// Only update the file if IPC failed or socket doesn't exist
sessionInfo.name = sanitizedTitle;
sessionManager.saveSessionInfo(sessionId, sessionInfo);
logger.log(`Session title updated to: ${sanitizedTitle}`);
closeLogger();
process.exit(0);
} catch (error) {
logger.error(
`Failed to update session title: ${error instanceof Error ? error.message : String(error)}`
);
closeLogger();
process.exit(1);
}
}
let command = remainingArgs;
if (command.length === 0) {
logger.error('No command specified');
showUsage();
closeLogger();
process.exit(1);
}
// Check if this is Claude and patch it if necessary (only in debug mode)
if (process.env.VIBETUNNEL_DEBUG === '1' || process.env.VIBETUNNEL_DEBUG === 'true') {
const patchedCommand = checkAndPatchClaude(command);
if (patchedCommand !== command) {
command = patchedCommand;
logger.debug(`Command updated after patching`);
}
}
// Auto-select dynamic mode for Claude if no mode was explicitly set
if (titleMode === TitleMode.NONE) {
// Check all command arguments for Claude
const isClaudeCommand = command.some((arg) => arg.toLowerCase().includes('claude'));
if (isClaudeCommand) {
titleMode = TitleMode.DYNAMIC;
logger.log(chalk.cyan('✓ Auto-selected dynamic title mode for Claude'));
logger.debug(`Detected Claude in command: ${command.join(' ')}`);
}
}
const cwd = process.cwd();
// Initialize PTY manager with fallback support
const controlPath = path.join(os.homedir(), '.vibetunnel', 'control');
logger.debug(`Control path: ${controlPath}`);
// Initialize PtyManager before creating instance
await PtyManager.initialize().catch((error) => {
logger.error('Failed to initialize PTY manager:', error);
closeLogger();
process.exit(1);
});
const ptyManager = new PtyManager(controlPath);
// Store original terminal dimensions
// For external spawns, wait a moment for terminal to fully initialize
const isExternalSpawn = process.env.VIBETUNNEL_SESSION_ID !== undefined;
let originalCols: number | undefined;
let originalRows: number | undefined;
if (isExternalSpawn) {
// Give terminal window time to fully initialize its dimensions
await new Promise((resolve) => setTimeout(resolve, 100));
// For external spawns, try to get the actual terminal size
// If stdout isn't properly connected, don't use fallback values
if (process.stdout.isTTY && process.stdout.columns && process.stdout.rows) {
originalCols = process.stdout.columns;
originalRows = process.stdout.rows;
logger.debug(`External spawn using actual terminal size: ${originalCols}x${originalRows}`);
} else {
// Don't pass dimensions - let PTY use terminal's natural size
logger.debug('External spawn: terminal dimensions not available, using terminal defaults');
}
} else {
// For non-external spawns, use reasonable defaults
originalCols = process.stdout.columns || 120;
originalRows = process.stdout.rows || 40;
logger.debug(`Regular spawn with dimensions: ${originalCols}x${originalRows}`);
}
try {
// Create a human-readable session name
const sessionName = generateSessionName(command, cwd);
// Pre-generate session ID if not provided
const finalSessionId = sessionId || `fwd_${Date.now()}`;
logger.log(`Creating session for command: ${command.join(' ')}`);
logger.debug(`Session ID: ${finalSessionId}, working directory: ${cwd}`);
// Log title mode if not default
if (titleMode !== TitleMode.NONE) {
const modeDescriptions = {
[TitleMode.FILTER]: 'Terminal title changes will be blocked',
[TitleMode.STATIC]: 'Terminal title will show path and command',
[TitleMode.DYNAMIC]: 'Terminal title will show path, command, and activity',
};
logger.log(chalk.cyan(`${modeDescriptions[titleMode]}`));
}
// Detect Git information
const gitInfo = await detectGitInfo(cwd);
// Variables that need to be accessible in cleanup
let sessionFileWatcher: fs.FSWatcher | undefined;
let fileWatchDebounceTimer: NodeJS.Timeout | undefined;
const sessionOptions: Parameters<typeof ptyManager.createSession>[1] = {
sessionId: finalSessionId,
name: sessionName,
workingDir: cwd,
titleMode: titleMode,
forwardToStdout: true,
gitRepoPath: gitInfo.gitRepoPath,
gitBranch: gitInfo.gitBranch,
gitAheadCount: gitInfo.gitAheadCount,
gitBehindCount: gitInfo.gitBehindCount,
gitHasChanges: gitInfo.gitHasChanges,
gitIsWorktree: gitInfo.gitIsWorktree,
gitMainRepoPath: gitInfo.gitMainRepoPath,
onExit: async (exitCode: number) => {
// Show exit message
logger.log(
chalk.yellow(`\n✓ VibeTunnel session ended`) + chalk.gray(` (exit code: ${exitCode})`)
);
// Remove resize listener
process.stdout.removeListener('resize', resizeHandler);
// Restore terminal settings and clean up stdin
if (process.stdin.isTTY) {
logger.debug('Restoring terminal to normal mode');
process.stdin.setRawMode(false);
}
process.stdin.pause();
process.stdin.removeAllListeners();
// Destroy stdin to ensure it doesn't keep the process alive
if (process.stdin.destroy) {
process.stdin.destroy();
}
// Restore original stdout.write if we hooked it
if (cleanupStdout) {
cleanupStdout();
}
// Clean up file watchers
if (sessionFileWatcher) {
sessionFileWatcher.close();
sessionFileWatcher = undefined;
logger.debug('Closed session file watcher');
}
if (fileWatchDebounceTimer) {
clearTimeout(fileWatchDebounceTimer);
}
// Stop watching the file
fs.unwatchFile(sessionJsonPath);
// Clean up only this session, not all sessions
logger.debug(`Cleaning up session ${finalSessionId}`);
try {
await ptyManager.killSession(finalSessionId);
} catch (error) {
// Session might already be cleaned up
logger.debug(`Session ${finalSessionId} cleanup error (likely already cleaned):`, error);
}
// Force exit
closeLogger();
process.exit(exitCode || 0);
},
};
// Only add dimensions if they're available (for non-external spawns or when TTY is properly connected)
if (originalCols !== undefined && originalRows !== undefined) {
sessionOptions.cols = originalCols;
sessionOptions.rows = originalRows;
}
const result = await ptyManager.createSession(command, sessionOptions);
// Get session info
const session = ptyManager.getSession(result.sessionId);
if (!session) {
throw new Error('Session not found after creation');
}
// Log session info with version
logger.log(chalk.green(`✓ VibeTunnel session started`) + chalk.gray(` (v${VERSION})`));
logger.log(chalk.gray('Command:'), command.join(' '));
logger.log(chalk.gray('Control directory:'), path.join(controlPath, result.sessionId));
logger.log(chalk.gray('Build:'), `${BUILD_DATE} | Commit: ${GIT_COMMIT}`);
// Connect to the session's IPC socket
const socketPath = path.join(controlPath, result.sessionId, 'ipc.sock');
const socketClient = new VibeTunnelSocketClient(socketPath, {
autoReconnect: true,
heartbeatInterval: 30000, // 30 seconds
});
// Wait for socket connection
try {
await socketClient.connect();
logger.debug('Connected to session IPC socket');
} catch (error) {
logger.error('Failed to connect to session socket:', error);
throw error;
}
// Set up terminal resize handler
const resizeHandler = () => {
const cols = process.stdout.columns || 80;
const rows = process.stdout.rows || 24;
logger.debug(`Terminal resized to ${cols}x${rows}`);
// Send resize command through socket
if (!socketClient.resize(cols, rows)) {
logger.error('Failed to send resize command');
}
};
// Listen for terminal resize events
process.stdout.on('resize', resizeHandler);
// Set up file watcher for session.json changes (for external updates)
const sessionJsonPath = path.join(controlPath, result.sessionId, 'session.json');
let lastKnownSessionName = result.sessionInfo.name;
// Set up file watcher with retry logic
const setupFileWatcher = async (retryCount = 0) => {
const maxRetries = 5;
const retryDelay = 500 * 2 ** retryCount; // Exponential backoff
try {
// Check if file exists
if (!fs.existsSync(sessionJsonPath)) {
if (retryCount < maxRetries) {
logger.debug(
`Session file not found, retrying in ${retryDelay}ms (attempt ${retryCount + 1}/${maxRetries})`
);
setTimeout(() => setupFileWatcher(retryCount + 1), retryDelay);
return;
} else {
logger.warn(`Session file not found after ${maxRetries} attempts: ${sessionJsonPath}`);
return;
}
}
logger.log(`Setting up file watcher for session name changes`);
// Function to check and update title if session name changed
const checkSessionNameChange = () => {
try {
// Check file still exists before reading
if (!fs.existsSync(sessionJsonPath)) {
return;
}
const sessionContent = fs.readFileSync(sessionJsonPath, 'utf-8');
const updatedInfo = JSON.parse(sessionContent) as SessionInfo;
// Check if session name changed
if (updatedInfo.name !== lastKnownSessionName) {
logger.debug(
`[File Watch] Session name changed from "${lastKnownSessionName}" to "${updatedInfo.name}"`
);
lastKnownSessionName = updatedInfo.name;
// Always update terminal title when session name changes
// Generate new title sequence based on title mode
let titleSequence: string;
if (titleMode === TitleMode.NONE || titleMode === TitleMode.FILTER) {
// For NONE and FILTER modes, just use the session name
titleSequence = `\x1B]2;${updatedInfo.name}\x07`;
} else {
// For STATIC and DYNAMIC, use the full format with path and command
titleSequence = generateTitleSequence(cwd, command, updatedInfo.name);
}
// Write title sequence to terminal
process.stdout.write(titleSequence);
logger.log(`Updated terminal title to "${updatedInfo.name}" via file watcher`);
}
} catch (error) {
logger.error('Failed to check session.json:', error);
}
};
// Use fs.watchFile for more reliable file monitoring (polling-based)
fs.watchFile(sessionJsonPath, { interval: 500 }, (curr, prev) => {
logger.debug(`[File Watch] File stats changed - mtime: ${curr.mtime} vs ${prev.mtime}`);
if (curr.mtime !== prev.mtime) {
checkSessionNameChange();
}
});
// Also use fs.watch as a fallback for immediate notifications
try {
const sessionDir = path.dirname(sessionJsonPath);
sessionFileWatcher = fs.watch(sessionDir, (eventType, filename) => {
// Only log in debug mode to avoid noise
logger.debug(`[File Watch] Directory event: ${eventType} on ${filename || 'unknown'}`);
// Check if it's our file
// On macOS, filename might be undefined, so we can't filter properly
// In that case, skip fs.watch events and rely on fs.watchFile instead
if (filename && (filename === 'session.json' || filename === 'session.json.tmp')) {
// Debounce rapid changes
if (fileWatchDebounceTimer) {
clearTimeout(fileWatchDebounceTimer);
}
fileWatchDebounceTimer = setTimeout(checkSessionNameChange, 100);
}
});
} catch (error) {
logger.warn('Failed to set up fs.watch, relying on fs.watchFile:', error);
}
logger.log(`File watcher successfully set up with polling fallback`);
// Clean up watcher on error if it was created
sessionFileWatcher?.on('error', (error) => {
logger.error('File watcher error:', error);
sessionFileWatcher?.close();
sessionFileWatcher = undefined;
});
} catch (error) {
logger.error('Failed to set up file watcher:', error);
if (retryCount < maxRetries) {
setTimeout(() => setupFileWatcher(retryCount + 1), retryDelay);
}
}
};
// Start setting up the file watcher after a short delay
setTimeout(() => setupFileWatcher(), 500);
// Set up activity detector for Claude status updates
let activityDetector: ActivityDetector | undefined;
let cleanupStdout: (() => void) | undefined;
if (titleMode === TitleMode.DYNAMIC) {
activityDetector = new ActivityDetector(command, sessionId);
// Hook into stdout to detect Claude status
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
let isProcessingActivity = false;
// Create a proper override that handles all overloads
const _stdoutWriteOverride = function (
this: NodeJS.WriteStream,
chunk: string | Uint8Array,
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
callback?: (err?: Error | null) => void
): boolean {
// Handle the overload: write(chunk, callback)
if (typeof encodingOrCallback === 'function') {
callback = encodingOrCallback;
encodingOrCallback = undefined;
}
if (isProcessingActivity) {
if (callback) {
return originalStdoutWrite.call(
this,
chunk,
encodingOrCallback as BufferEncoding | undefined,
callback
);
} else if (encodingOrCallback && typeof encodingOrCallback === 'string') {
return originalStdoutWrite.call(this, chunk, encodingOrCallback);
} else {
return originalStdoutWrite.call(this, chunk);
}
}
isProcessingActivity = true;
try {
// Process output through activity detector
if (activityDetector && typeof chunk === 'string') {
const { filteredData, activity } = activityDetector.processOutput(chunk);
// Send status update if detected
if (activity.specificStatus) {
socketClient.sendStatus(activity.specificStatus.app, activity.specificStatus.status);
}
// Call original with correct arguments
if (callback) {
return originalStdoutWrite.call(
this,
filteredData,
encodingOrCallback as BufferEncoding | undefined,
callback
);
} else if (encodingOrCallback && typeof encodingOrCallback === 'string') {
return originalStdoutWrite.call(this, filteredData, encodingOrCallback);
} else {
return originalStdoutWrite.call(this, filteredData);
}
}
// Pass through as-is if not string or no detector
if (callback) {
return originalStdoutWrite.call(
this,
chunk,
encodingOrCallback as BufferEncoding | undefined,
callback
);
} else if (encodingOrCallback && typeof encodingOrCallback === 'string') {
return originalStdoutWrite.call(this, chunk, encodingOrCallback);
} else {
return originalStdoutWrite.call(this, chunk);
}
} finally {
isProcessingActivity = false;
}
};
// Apply the override
process.stdout.write = _stdoutWriteOverride as typeof process.stdout.write;
// Store reference for cleanup
cleanupStdout = () => {
process.stdout.write = originalStdoutWrite;
};
// Ensure cleanup happens on process exit
process.on('exit', cleanupStdout);
process.on('SIGINT', cleanupStdout);
process.on('SIGTERM', cleanupStdout);
}
// Set up raw mode for terminal input
if (process.stdin.isTTY) {
logger.debug('Setting terminal to raw mode for input forwarding');
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding('utf8');
// Forward stdin through socket
process.stdin.on('data', (data: string) => {
// Send through socket
if (!socketClient.sendStdin(data)) {
logger.error('Failed to send stdin data');
}
});
// Handle socket events
socketClient.on('disconnect', (error) => {
logger.error('Socket disconnected:', error?.message || 'Unknown error');
process.exit(1);
});
socketClient.on('error', (error) => {
logger.error('Socket error:', error);
});
// The process will stay alive because stdin is in raw mode and resumed
} catch (error) {
logger.error('Failed to create or manage session:', error);
closeLogger();
process.exit(1);
}
}