Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/commands/open.ts
Original file line number Diff line number Diff line change
@@ -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 <path>" and "branch refs/heads/<branchName>"
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);
}
}
172 changes: 110 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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 <packageManager>", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)")
.option("-e, --editor <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 <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 <packageManager>",
"Package manager to use for installing dependencies (npm, pnpm, bun, etc.)"
)
.option(
"-e, --editor <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("<branchName>", "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("<branchName>", "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("<prNumber>", "GitHub Pull Request number to create a worktree from")
.option("-p, --path <path>", "Specify a custom path for the worktree (defaults to repoName-branchName)")
.option("-i, --install <packageManager>", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)")
.option("-e, --editor <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(
"<prNumber>",
"GitHub Pull Request number to create a worktree from"
)
.option(
"-p, --path <path>",
"Specify a custom path for the worktree (defaults to repoName-branchName)"
)
.option(
"-i, --install <packageManager>",
"Package manager to use for installing dependencies (npm, pnpm, bun, etc.)"
)
.option(
"-e, --editor <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("<editorName>", "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>",
"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(
"<editorName>",
"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);
program.parse(process.argv);