vibetunnel/web/src/server/services/multiplexer-manager.ts
Helmut Januschka dbba6127df
Add tmux integration to VibeTunnel (#460)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2025-07-30 02:25:54 +02:00

230 lines
6.9 KiB
TypeScript

import type {
MultiplexerSession,
MultiplexerStatus,
MultiplexerType,
TmuxPane,
TmuxWindow,
} from '../../shared/multiplexer-types.js';
import type { SessionCreateOptions } from '../../shared/types.js';
import { TitleMode } from '../../shared/types.js';
import type { PtyManager } from '../pty/pty-manager.js';
import { createLogger } from '../utils/logger.js';
import { ScreenManager } from './screen-manager.js';
import { TmuxManager } from './tmux-manager.js';
import { ZellijManager } from './zellij-manager.js';
const logger = createLogger('MultiplexerManager');
export class MultiplexerManager {
private static instance: MultiplexerManager;
private tmuxManager: TmuxManager;
private zellijManager: ZellijManager;
private screenManager: ScreenManager;
private ptyManager: PtyManager;
private constructor(ptyManager: PtyManager) {
this.ptyManager = ptyManager;
this.tmuxManager = TmuxManager.getInstance(ptyManager);
this.zellijManager = ZellijManager.getInstance(ptyManager);
this.screenManager = ScreenManager.getInstance();
}
static getInstance(ptyManager: PtyManager): MultiplexerManager {
if (!MultiplexerManager.instance) {
MultiplexerManager.instance = new MultiplexerManager(ptyManager);
}
return MultiplexerManager.instance;
}
/**
* Get available multiplexers and their sessions
*/
async getAvailableMultiplexers(): Promise<MultiplexerStatus> {
const [tmuxAvailable, zellijAvailable, screenAvailable] = await Promise.all([
this.tmuxManager.isAvailable(),
this.zellijManager.isAvailable(),
this.screenManager.isAvailable(),
]);
const result: MultiplexerStatus = {
tmux: {
available: tmuxAvailable,
type: 'tmux' as MultiplexerType,
sessions: [] as MultiplexerSession[],
},
zellij: {
available: zellijAvailable,
type: 'zellij' as MultiplexerType,
sessions: [] as MultiplexerSession[],
},
screen: {
available: screenAvailable,
type: 'screen' as MultiplexerType,
sessions: [] as MultiplexerSession[],
},
};
// Load sessions for available multiplexers
if (tmuxAvailable) {
try {
const tmuxSessions = await this.tmuxManager.listSessions();
result.tmux.sessions = tmuxSessions.map((session) => ({
...session,
type: 'tmux' as MultiplexerType,
}));
} catch (error) {
logger.error('Failed to list tmux sessions', { error });
}
}
if (zellijAvailable) {
try {
const zellijSessions = await this.zellijManager.listSessions();
result.zellij.sessions = zellijSessions.map((session) => ({
...session,
type: 'zellij' as MultiplexerType,
}));
} catch (error) {
logger.error('Failed to list zellij sessions', { error });
}
}
if (screenAvailable) {
try {
const screenSessions = await this.screenManager.listSessions();
result.screen.sessions = screenSessions.map((session) => ({
...session,
type: 'screen' as MultiplexerType,
}));
} catch (error) {
logger.error('Failed to list screen sessions', { error });
}
}
return result;
}
/**
* Get windows for a tmux session
*/
async getTmuxWindows(sessionName: string): Promise<TmuxWindow[]> {
return this.tmuxManager.listWindows(sessionName);
}
/**
* Get panes for a tmux window
*/
async getTmuxPanes(sessionName: string, windowIndex?: number): Promise<TmuxPane[]> {
return this.tmuxManager.listPanes(sessionName, windowIndex);
}
/**
* Create a new session
*/
async createSession(
type: MultiplexerType,
name: string,
options?: { command?: string[]; layout?: string }
): Promise<void> {
if (type === 'tmux') {
await this.tmuxManager.createSession(name, options?.command);
} else if (type === 'zellij') {
await this.zellijManager.createSession(name, options?.layout);
} else if (type === 'screen') {
// Screen expects a single command string, not an array
const command = options?.command ? options.command.join(' ') : undefined;
await this.screenManager.createSession(name, command);
} else {
throw new Error(`Unknown multiplexer type: ${type}`);
}
}
/**
* Attach to a session
*/
async attachToSession(
type: MultiplexerType,
sessionName: string,
options?: Partial<SessionCreateOptions> & { windowIndex?: number; paneIndex?: number }
): Promise<string> {
if (type === 'tmux') {
return this.tmuxManager.attachToTmux(
sessionName,
options?.windowIndex,
options?.paneIndex,
options
);
} else if (type === 'zellij') {
return this.zellijManager.attachToZellij(sessionName, options);
} else if (type === 'screen') {
// Screen doesn't support programmatic attach like tmux/zellij
// We need to create a new session that runs the attach command
const attachCmd = await this.screenManager.attachToSession(sessionName);
// Create a new PTY session that will run the screen attach command
const result = await this.ptyManager.createSession(attachCmd, {
...options,
titleMode: TitleMode.DYNAMIC,
});
return result.sessionId;
} else {
throw new Error(`Unknown multiplexer type: ${type}`);
}
}
/**
* Kill/delete a session
*/
async killSession(type: MultiplexerType, sessionName: string): Promise<void> {
if (type === 'tmux') {
await this.tmuxManager.killSession(sessionName);
} else if (type === 'zellij') {
await this.zellijManager.killSession(sessionName);
} else if (type === 'screen') {
await this.screenManager.killSession(sessionName);
} else {
throw new Error(`Unknown multiplexer type: ${type}`);
}
}
/**
* Kill a tmux window
*/
async killTmuxWindow(sessionName: string, windowIndex: number): Promise<void> {
await this.tmuxManager.killWindow(sessionName, windowIndex);
}
/**
* Kill a tmux pane
*/
async killTmuxPane(sessionName: string, paneId: string): Promise<void> {
await this.tmuxManager.killPane(sessionName, paneId);
}
/**
* Check which multiplexer we're currently inside
*/
getCurrentMultiplexer(): { type: MultiplexerType; session: string } | null {
if (this.tmuxManager.isInsideTmux()) {
const session = this.tmuxManager.getCurrentSession();
if (session) {
return { type: 'tmux', session };
}
}
if (this.zellijManager.isInsideZellij()) {
const session = this.zellijManager.getCurrentSession();
if (session) {
return { type: 'zellij', session };
}
}
if (this.screenManager.isInsideScreen()) {
const session = this.screenManager.getCurrentSession();
if (session) {
return { type: 'screen', session };
}
}
return null;
}
}