diff --git a/src/commands/open.ts b/src/commands/open.ts new file mode 100644 index 0000000..55dd5fa --- /dev/null +++ b/src/commands/open.ts @@ -0,0 +1,107 @@ +import { execa } from "execa"; +import chalk from "chalk"; +import { stat } from "node:fs/promises"; +import { resolve } from "node:path"; +import { getDefaultEditor } from "../config.js"; + +export async function openWorktreeHandler( + pathOrBranch: string = "", + options: { editor?: string } +) { + try { + // 1. Validate we're in a git repo + await execa("git", ["rev-parse", "--is-inside-work-tree"]); + + if (!pathOrBranch) { + console.error( + chalk.red("You must specify a path or branch name for the worktree.") + ); + process.exit(1); + } + + // If the user gave us a path, we can open directly. + // If user gave us a branch name, we parse `git worktree list` to find the matching path. + let targetPath = pathOrBranch; + + // Try to see if it's a valid path + let isDirectory = false; + try { + const stats = await stat(pathOrBranch); + isDirectory = stats.isDirectory(); + } catch { + isDirectory = false; + } + + if (!isDirectory) { + // If it's not a directory, assume it's a branch name: + const { stdout } = await execa("git", [ + "worktree", + "list", + "--porcelain", + ]); + // The --porcelain output is structured. We'll parse lines and find the "worktree " and "branch refs/heads/" + const entries = stdout.split("\n"); + let currentPath: string | null = null; + let foundPath = false; + for (const line of entries) { + if (line.startsWith("worktree ")) { + currentPath = line.replace("worktree ", "").trim(); + } else if (line.startsWith("branch ")) { + const fullBranchRef = line.replace("branch ", "").trim(); // e.g. refs/heads/my-branch + const shortBranch = fullBranchRef.replace("refs/heads/", ""); + if (shortBranch === pathOrBranch && currentPath) { + targetPath = currentPath; + foundPath = true; + break; + } + } + } + + if (!foundPath) { + console.error( + chalk.red(`Could not find a worktree for branch "${pathOrBranch}".`) + ); + process.exit(1); + } + } + + // Verify the target path exists and is a git worktree + try { + await stat(targetPath); + await stat(resolve(targetPath, ".git")); // Check if it's a git worktree + } catch (error) { + console.error( + chalk.red( + `The path "${targetPath}" does not exist or is not a git worktree.` + ) + ); + process.exit(1); + } + + // Open in the specified editor (or use configured default) + const configuredEditor = getDefaultEditor(); + const editorCommand = options.editor || configuredEditor; + console.log(chalk.blue(`Opening ${targetPath} in ${editorCommand}...`)); + + try { + await execa(editorCommand, [targetPath], { stdio: "inherit" }); + console.log( + chalk.green(`Successfully opened worktree in ${editorCommand}.`) + ); + } catch (editorError) { + console.error( + chalk.red( + `Failed to open editor "${editorCommand}". Please ensure it's installed and in your PATH.` + ) + ); + process.exit(1); + } + } catch (error) { + if (error instanceof Error) { + console.error(chalk.red("Failed to open worktree:"), error.message); + } else { + console.error(chalk.red("Failed to open worktree:"), error); + } + process.exit(1); + } +} diff --git a/src/index.ts b/src/index.ts index eae9f83..0315b2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,85 +9,133 @@ import { mergeWorktreeHandler } from "./commands/merge.js"; import { purgeWorktreesHandler } from "./commands/purge.js"; import { configHandler } from "./commands/config.js"; import { prWorktreeHandler } from "./commands/pr.js"; +import { openWorktreeHandler } from "./commands/open.js"; const program = new Command(); program - .name("wt") - .description("Manage git worktrees and open them in the Cursor editor.") - .version("1.0.0"); + .name("wt") + .description("Manage git worktrees and open them in the Cursor editor.") + .version("1.0.0"); program - .command("new") - .argument("[branchName]", "Name of the branch to base this worktree on") - .option("-p, --path ", "Relative path/folder name for new worktree") - .option("-c, --checkout", "Create new branch if it doesn't exist and checkout automatically", false) - .option("-i, --install ", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)") - .option("-e, --editor ", "Editor to use for opening the worktree (e.g., code, webstorm, windsurf, etc.)") - .description("Create a new worktree for the specified branch, install dependencies if specified, and open in editor.") - .action(newWorktreeHandler); + .command("new") + .argument("[branchName]", "Name of the branch to base this worktree on") + .option("-p, --path ", "Relative path/folder name for new worktree") + .option( + "-c, --checkout", + "Create new branch if it doesn't exist and checkout automatically", + false + ) + .option( + "-i, --install ", + "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)" + ) + .option( + "-e, --editor ", + "Editor to use for opening the worktree (e.g., code, webstorm, windsurf, etc.)" + ) + .description( + "Create a new worktree for the specified branch, install dependencies if specified, and open in editor." + ) + .action(newWorktreeHandler); program - .command("list") - .alias("ls") - .description("List all existing worktrees for this repository.") - .action(listWorktreesHandler); + .command("list") + .alias("ls") + .description("List all existing worktrees for this repository.") + .action(listWorktreesHandler); program - .command("remove") - .alias("rm") - .argument("[pathOrBranch]", "Path of the worktree or branch to remove.") - .option("-f, --force", "Force removal of worktree and deletion of the folder", false) - .description("Remove a specified worktree. Cleans up the .git/worktrees references.") - .action(removeWorktreeHandler); + .command("remove") + .alias("rm") + .argument("[pathOrBranch]", "Path of the worktree or branch to remove.") + .option( + "-f, --force", + "Force removal of worktree and deletion of the folder", + false + ) + .description( + "Remove a specified worktree. Cleans up the .git/worktrees references." + ) + .action(removeWorktreeHandler); program - .command("merge") - .argument("", "Name of the branch to merge from") - .option("-f, --force", "Force removal of worktree after merge", false) - .description("Commit changes in the target branch and merge them into the current branch, then remove the branch/worktree") - .action(mergeWorktreeHandler); + .command("merge") + .argument("", "Name of the branch to merge from") + .option("-f, --force", "Force removal of worktree after merge", false) + .description( + "Commit changes in the target branch and merge them into the current branch, then remove the branch/worktree" + ) + .action(mergeWorktreeHandler); program - .command("purge") - .description("Safely remove all worktrees except for the main branch, with confirmation.") - .action(purgeWorktreesHandler); + .command("purge") + .description( + "Safely remove all worktrees except for the main branch, with confirmation." + ) + .action(purgeWorktreesHandler); program - .command("pr") - .argument("", "GitHub Pull Request number to create a worktree from") - .option("-p, --path ", "Specify a custom path for the worktree (defaults to repoName-branchName)") - .option("-i, --install ", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)") - .option("-e, --editor ", "Editor to use for opening the worktree (overrides default editor)") - .description("Fetch the branch for a given GitHub PR number and create a worktree.") - .action(prWorktreeHandler); + .command("pr") + .argument( + "", + "GitHub Pull Request number to create a worktree from" + ) + .option( + "-p, --path ", + "Specify a custom path for the worktree (defaults to repoName-branchName)" + ) + .option( + "-i, --install ", + "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)" + ) + .option( + "-e, --editor ", + "Editor to use for opening the worktree (overrides default editor)" + ) + .description( + "Fetch the branch for a given GitHub PR number and create a worktree." + ) + .action(prWorktreeHandler); program - .command("config") - .description("Manage CLI configuration settings.") - .addCommand( - new Command("set") - .description("Set a configuration value.") - .addCommand( - new Command("editor") - .argument("", "Name of the editor command (e.g., code, cursor, webstorm)") - .description("Set the default editor to open worktrees in.") - .action((editorName) => configHandler('set', 'editor', editorName)) - ) - ) - .addCommand( - new Command("get") - .description("Get a configuration value.") - .addCommand( - new Command("editor") - .description("Get the currently configured default editor.") - .action(() => configHandler('get', 'editor')) - ) + .command("open") + .argument("[pathOrBranch]", "Path to worktree or branch name to open") + .option( + "-e, --editor ", + "Editor to use for opening the worktree (overrides default editor)" + ) + .description("Open an existing worktree in the editor.") + .action(openWorktreeHandler); + +program + .command("config") + .description("Manage CLI configuration settings.") + .addCommand( + new Command("set").description("Set a configuration value.").addCommand( + new Command("editor") + .argument( + "", + "Name of the editor command (e.g., code, cursor, webstorm)" + ) + .description("Set the default editor to open worktrees in.") + .action((editorName) => configHandler("set", "editor", editorName)) ) - .addCommand( - new Command("path") - .description("Show the path to the configuration file.") - .action(() => configHandler('path')) - ); + ) + .addCommand( + new Command("get") + .description("Get a configuration value.") + .addCommand( + new Command("editor") + .description("Get the currently configured default editor.") + .action(() => configHandler("get", "editor")) + ) + ) + .addCommand( + new Command("path") + .description("Show the path to the configuration file.") + .action(() => configHandler("path")) + ); -program.parse(process.argv); \ No newline at end of file +program.parse(process.argv);