import * as readline from "node:readline"; import type { CloseSessionOptions, CloseSessionResult } from "../types"; import { runCommand, getSessionPath, getWorktreesForSession } from "./utils"; /** * Prompts the user for input on the terminal. * * @param question The question to display. * @returns The user's answer. */ async function promptUser(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer); }); }); } /** * Closes a multi-model tmux session and optionally cleans up worktrees and branches. * * When `cleanupWorktrees` is true, removes git worktrees, creates archive tags * for recovery, and deletes the associated branches. * * @param options Close configuration including session name and cleanup flags. * @returns Result with details about what was cleaned up. * * @example * ```ts * const result = await closeMultiModel({ * sessionName: "compare", * cleanupWorktrees: true, * force: true, * }); * if (result.success) { * console.log(`Closed ${result.sessionName}, removed ${result.worktreesRemoved?.length} worktrees`); * } * ``` */ export async function closeMultiModel(options: CloseSessionOptions): Promise { const worktreesRemoved: string[] = []; const branchesDeleted: string[] = []; const tagsCreated: string[] = []; const warnings: string[] = []; try { // Check if session exists const { ok: sessionExists } = await runCommand(["tmux", "has-session", "-t", options.sessionName]); // Kill tmux session if it exists if (sessionExists) { await runCommand(["tmux", "kill-session", "-t", options.sessionName]); } // Get list of worktrees for this session before killing tmux const worktrees = await getWorktreesForSession(options.sessionName); // If cleanup requested and not forced, ask for confirmation if (options.cleanupWorktrees && !options.force && worktrees.length > 0) { console.log(`\nThe following worktrees and branches will be removed:`); worktrees.forEach((wt) => console.log(` - ${wt.path} (branch: ${wt.branch})`)); const answer = await promptUser("\nDo you want to proceed? (y/N): "); if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") { return { success: false, sessionName: options.sessionName, error: "Cleanup cancelled by user", }; } } let cleanupPerformed = false; // Cleanup worktrees and optionally branches if (options.cleanupWorktrees) { for (const worktree of worktrees) { try { // Remove worktree await runCommand(["git", "worktree", "remove", "-f", worktree.path]); worktreesRemoved.push(worktree.path); // Create an archive tag before deleting the branch for safe recovery const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const archiveTag = `archive/${worktree.branch}-${timestamp}`; const tagResult = await runCommand(["git", "tag", archiveTag, worktree.branch]); if (tagResult.ok) { tagsCreated.push(archiveTag); } else { const warningMsg = `Failed to create tag ${archiveTag} for branch ${worktree.branch}.`; warnings.push(warningMsg); console.warn(`Warning: ${warningMsg}`); } await runCommand(["git", "branch", "-D", worktree.branch]); branchesDeleted.push(worktree.branch); } catch (err) { console.warn(`Warning: Failed to cleanup worktree ${worktree.path}: ${err}`); } } // Also remove the base directory const worktreeBase = getSessionPath(options.sessionName); try { await runCommand(["rm", "-rf", worktreeBase]); } catch { // Ignore errors } cleanupPerformed = worktreesRemoved.length > 0; } return { success: true, sessionName: options.sessionName, cleanupPerformed, worktreesRemoved, branchesDeleted, tagsCreated, warnings, }; } catch (error) { return { success: false, sessionName: options.sessionName, error: String(error), worktreesRemoved, branchesDeleted, tagsCreated, warnings, }; } }