agent-ecosystem/src/main/ipc/tmux.ts

138 lines
3.8 KiB
TypeScript

import { TMUX_GET_STATUS } from '@preload/constants/ipcChannels';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process';
import type { TmuxPlatform, TmuxStatus, IpcResult } from '@shared/types';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
const logger = createLogger('IPC:tmux');
let cachedStatus: { value: TmuxStatus; at: number } | null = null;
let statusInFlight: Promise<TmuxStatus> | null = null;
const STATUS_CACHE_TTL_MS = 10_000;
function mapPlatform(platform: NodeJS.Platform): TmuxPlatform {
if (platform === 'darwin' || platform === 'linux' || platform === 'win32') {
return platform;
}
return 'unknown';
}
function execFileAsync(
command: string,
args: string[],
timeout: number
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
execFile(command, args, { timeout }, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve({ stdout: String(stdout), stderr: String(stderr) });
});
});
}
async function resolveBinaryPath(platform: TmuxPlatform): Promise<string | null> {
const locator = platform === 'win32' ? 'where' : 'which';
try {
const { stdout } = await execFileAsync(locator, ['tmux'], 2_000);
const firstLine = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean);
return firstLine ?? null;
} catch {
return null;
}
}
async function computeTmuxStatus(): Promise<TmuxStatus> {
const platform = mapPlatform(process.platform);
const nativeSupported = platform === 'darwin' || platform === 'linux';
const checkedAt = new Date().toISOString();
try {
const { stdout, stderr } = await execFileAsync('tmux', ['-V'], 3_000);
const version = (stdout || stderr).trim() || null;
const binaryPath = await resolveBinaryPath(platform);
return {
available: true,
version,
binaryPath,
platform,
nativeSupported,
checkedAt,
error: null,
};
} catch (error) {
const message = getErrorMessage(error);
const missing =
typeof error === 'object' &&
error !== null &&
'code' in error &&
((error as { code?: string }).code === 'ENOENT' ||
(error as { code?: string }).code === 'ENOEXEC');
if (missing) {
return {
available: false,
version: null,
binaryPath: null,
platform,
nativeSupported,
checkedAt,
error: null,
};
}
logger.warn(`tmux status check failed: ${message}`);
return {
available: false,
version: null,
binaryPath: null,
platform,
nativeSupported,
checkedAt,
error: message,
};
}
}
async function handleGetStatus(_event: IpcMainInvokeEvent): Promise<IpcResult<TmuxStatus>> {
try {
if (cachedStatus && Date.now() - cachedStatus.at < STATUS_CACHE_TTL_MS) {
return { success: true, data: cachedStatus.value };
}
if (!statusInFlight) {
statusInFlight = computeTmuxStatus()
.then((status) => {
cachedStatus = { value: status, at: Date.now() };
return status;
})
.finally(() => {
statusInFlight = null;
});
}
const status = await statusInFlight;
return { success: true, data: status };
} catch (error) {
const message = getErrorMessage(error);
logger.error('Error in tmux:getStatus:', message);
return { success: false, error: message };
}
}
export function registerTmuxHandlers(ipcMain: IpcMain): void {
ipcMain.handle(TMUX_GET_STATUS, handleGetStatus);
logger.info('tmux handlers registered');
}
export function removeTmuxHandlers(ipcMain: IpcMain): void {
ipcMain.removeHandler(TMUX_GET_STATUS);
logger.info('tmux handlers removed');
}