worktree-tool
Version:
A command-line tool for managing Git worktrees with integrated tmux/shell session management
327 lines • 10.6 kB
JavaScript
import { execFile } from "child_process";
import { promisify } from "util";
import { ENV_VARS } from "../core/constants.js";
import { getErrorMessage } from "../utils/error-handler.js";
import { PlatformError } from "../utils/errors.js";
import { sanitizeTmuxSession, sanitizeTmuxWindow } from "../utils/sanitize.js";
import { executeTmuxCommand, executeTmuxCommandSilent, executeTmuxCommandVoid, tmuxObjectExists, } from "./tmux-wrapper.js";
const execFileAsync = promisify(execFile);
/**
* Sanitize name for tmux compatibility
* Replace spaces with hyphens and remove special characters
*/
export const sanitizeTmuxName = sanitizeTmuxSession;
/**
* Sanitize a window name for tmux - allows spaces and colons
*/
export const sanitizeTmuxWindowName = sanitizeTmuxWindow;
/**
* Check if we're currently inside a tmux session
*/
export function isInsideTmux() {
return process.env.TMUX !== undefined;
}
/**
* Check if we're in a proper terminal that can attach to tmux
*/
export function canAttachToTmux() {
// Check if stdout is a TTY (terminal)
return !!process.stdout.isTTY;
}
/**
* Get the current tmux session name if inside tmux
*/
export async function getCurrentTmuxSession() {
if (!isInsideTmux()) {
return null;
}
try {
const result = await execFileAsync("tmux", ["display-message", "-p", "#{session_name}"]);
return result.stdout.trim();
}
catch {
return null;
}
}
/**
* Check if tmux is available on the system
*/
export async function isTmuxAvailable() {
// Check for test environment variable to disable tmux
if (process.env[ENV_VARS.DISABLE_TMUX] === "true") {
return false;
}
return executeTmuxCommandSilent(["-V"]);
}
/**
* Check if a tmux session exists
*/
export async function tmuxSessionExists(sessionName) {
return tmuxObjectExists(["has-session", "-t", sessionName]);
}
/**
* Create a tmux session
*/
export async function createTmuxSession(sessionName, startDirectory) {
const sanitizedName = sanitizeTmuxName(sessionName);
const args = ["new-session", "-d", "-s", sanitizedName];
// If a start directory is provided, use it
if (startDirectory) {
args.push("-c", startDirectory);
}
await executeTmuxCommandVoid(args, "Failed to create tmux session");
}
/**
* Get the number of windows in a tmux session
*/
export async function getTmuxWindowCount(sessionName) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
const result = await executeTmuxCommand([
"list-windows",
"-t",
sanitizedSession,
"-F",
"#{window_id}",
], "Failed to list tmux windows");
return result.trim().split("\n").filter((line) => line).length;
}
catch {
return 0;
}
}
/**
* Rename a tmux window
*/
export async function renameTmuxWindow(sessionName, windowIndex, newName) {
const sanitizedSession = sanitizeTmuxName(sessionName);
const sanitizedName = sanitizeTmuxName(newName);
await executeTmuxCommandVoid([
"rename-window",
"-t",
`${sanitizedSession}:${String(windowIndex)}`,
sanitizedName,
], "Failed to rename tmux window");
}
/**
* Send keys to change directory in a tmux window
*/
export async function tmuxSendKeys(sessionName, windowIndex, command) {
const sanitizedSession = sanitizeTmuxName(sessionName);
await executeTmuxCommandVoid([
"send-keys",
"-t",
`${sanitizedSession}:${String(windowIndex)}`,
command,
"Enter",
], "Failed to send keys to tmux");
}
/**
* Create a tmux window in an existing session
*/
export async function createTmuxWindow(sessionName, windowName, directory) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
const sanitizedWindow = sanitizeTmuxWindowName(windowName);
await execFileAsync("tmux", [
"new-window",
"-t",
sanitizedSession,
"-n",
sanitizedWindow,
"-c",
directory,
]);
}
catch (error) {
throw new PlatformError(`Failed to create tmux window: ${getErrorMessage(error)}`);
}
}
/**
* Create a tmux window with a command that runs immediately
*/
export async function createTmuxWindowWithCommand(sessionName, windowName, directory, command) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
const sanitizedWindow = sanitizeTmuxWindowName(windowName);
// Create window and run command directly
await execFileAsync("tmux", [
"new-window",
"-t",
sanitizedSession,
"-n",
sanitizedWindow,
"-c",
directory,
command,
]);
}
catch (error) {
throw new PlatformError(`Failed to create tmux window with command: ${getErrorMessage(error)}`);
}
}
/**
* Create a tmux session with an initial window running a command
*/
export async function createTmuxSessionWithWindow(sessionName, windowName, windowDirectory, command) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
const sanitizedWindow = sanitizeTmuxWindowName(windowName);
const args = [
"new-session",
"-d",
"-s",
sanitizedSession,
"-n",
sanitizedWindow,
"-c",
windowDirectory,
command,
];
await execFileAsync("tmux", args);
}
catch (error) {
throw new PlatformError(`Failed to create tmux session with window: ${getErrorMessage(error)}`);
}
}
/**
* Switch to a tmux window
*/
export async function switchToTmuxWindow(sessionName, windowName) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
const sanitizedWindow = sanitizeTmuxWindowName(windowName);
// Check if we're inside tmux and get current session
const currentSession = await getCurrentTmuxSession();
if (currentSession && currentSession === sanitizedSession) {
// We're in the same session, just switch windows
await execFileAsync("tmux", ["select-window", "-t", `${sanitizedSession}:${sanitizedWindow}`]);
}
else if (currentSession) {
// We're in a different tmux session, use switch-client
await execFileAsync("tmux", ["switch-client", "-t", `${sanitizedSession}:${sanitizedWindow}`]);
}
else {
// Not inside tmux, try to attach
await execFileAsync("tmux", ["attach-session", "-t", `${sanitizedSession}:${sanitizedWindow}`]);
}
}
catch (error) {
throw new PlatformError(`Failed to switch to tmux window: ${getErrorMessage(error)}`);
}
}
/**
* Attach to a tmux session, optionally switching to a specific window
*/
export async function attachToTmuxSession(sessionName, windowName) {
try {
const sanitizedSession = sanitizeTmuxName(sessionName);
if (!canAttachToTmux()) {
throw new PlatformError("Cannot attach to tmux session: not running in a terminal");
}
const args = ["attach-session", "-t"];
if (windowName) {
const sanitizedWindow = sanitizeTmuxWindowName(windowName);
args.push(`${sanitizedSession}:${sanitizedWindow}`);
}
else {
args.push(sanitizedSession);
}
// Use spawn instead of execFile for interactive attachment
const { spawn } = await import("child_process");
const tmux = spawn("tmux", args, {
stdio: "inherit",
shell: false,
});
await new Promise((resolve, reject) => {
tmux.on("exit", (code) => {
if (code === 0) {
resolve();
}
else {
reject(new PlatformError(`tmux exited with code ${String(code)}`));
}
});
tmux.on("error", (error) => {
reject(new PlatformError(`Failed to attach to tmux session: ${error.message}`));
});
});
}
catch (error) {
if (error instanceof PlatformError) {
throw error;
}
throw new PlatformError(`Failed to attach to tmux session: ${getErrorMessage(error)}`);
}
}
/**
* List all tmux sessions
*/
export async function listTmuxSessions() {
try {
const result = await executeTmuxCommand(["list-sessions", "-F", "#{session_name}"], "Failed to list tmux sessions");
return result.trim().split("\n").filter((name) => name.length > 0);
}
catch {
// No sessions exist or tmux not available
return [];
}
}
/**
* Kill a tmux session
*/
export async function killTmuxSession(sessionName) {
const sanitizedName = sanitizeTmuxName(sessionName);
await executeTmuxCommandVoid(["kill-session", "-t", sanitizedName], "Failed to kill tmux session");
}
/**
* TmuxManager class for managing tmux operations
*/
export class TmuxManager {
/**
* Create a new tmux window
*/
async createWindow(name, directory) {
// For exec command, we'll create a new window without a specific session
// This will create it in the current session if inside tmux, or a new session if not
try {
await execFileAsync("tmux", [
"new-window",
"-n",
name,
"-c",
directory,
]);
}
catch (error) {
throw new PlatformError(`Failed to create tmux window: ${getErrorMessage(error)}`);
}
}
/**
* Send keys to a tmux window
*/
async sendKeys(windowName, command, enter = false) {
try {
const args = ["send-keys", "-t", windowName, command];
if (enter) {
args.push("Enter");
}
await execFileAsync("tmux", args);
}
catch (error) {
throw new PlatformError(`Failed to send keys to tmux: ${getErrorMessage(error)}`);
}
}
/**
* Execute a command (placeholder for compatibility)
*/
async execute(args) {
try {
await execFileAsync("tmux", args);
}
catch (error) {
throw new PlatformError(`Failed to execute tmux command: ${getErrorMessage(error)}`);
}
}
}
//# sourceMappingURL=tmux.js.map