Skip to content

Commit 342b320

Browse files
Merge pull request #17 from corpooo/feat/add-open-command
Feat - Add `open` command
2 parents f0140f2 + 37e7aec commit 342b320

2 files changed

Lines changed: 217 additions & 62 deletions

File tree

src/commands/open.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { execa } from "execa";
2+
import chalk from "chalk";
3+
import { stat } from "node:fs/promises";
4+
import { resolve } from "node:path";
5+
import { getDefaultEditor } from "../config.js";
6+
7+
export async function openWorktreeHandler(
8+
pathOrBranch: string = "",
9+
options: { editor?: string }
10+
) {
11+
try {
12+
// 1. Validate we're in a git repo
13+
await execa("git", ["rev-parse", "--is-inside-work-tree"]);
14+
15+
if (!pathOrBranch) {
16+
console.error(
17+
chalk.red("You must specify a path or branch name for the worktree.")
18+
);
19+
process.exit(1);
20+
}
21+
22+
// If the user gave us a path, we can open directly.
23+
// If user gave us a branch name, we parse `git worktree list` to find the matching path.
24+
let targetPath = pathOrBranch;
25+
26+
// Try to see if it's a valid path
27+
let isDirectory = false;
28+
try {
29+
const stats = await stat(pathOrBranch);
30+
isDirectory = stats.isDirectory();
31+
} catch {
32+
isDirectory = false;
33+
}
34+
35+
if (!isDirectory) {
36+
// If it's not a directory, assume it's a branch name:
37+
const { stdout } = await execa("git", [
38+
"worktree",
39+
"list",
40+
"--porcelain",
41+
]);
42+
// The --porcelain output is structured. We'll parse lines and find the "worktree <path>" and "branch refs/heads/<branchName>"
43+
const entries = stdout.split("\n");
44+
let currentPath: string | null = null;
45+
let foundPath = false;
46+
for (const line of entries) {
47+
if (line.startsWith("worktree ")) {
48+
currentPath = line.replace("worktree ", "").trim();
49+
} else if (line.startsWith("branch ")) {
50+
const fullBranchRef = line.replace("branch ", "").trim(); // e.g. refs/heads/my-branch
51+
const shortBranch = fullBranchRef.replace("refs/heads/", "");
52+
if (shortBranch === pathOrBranch && currentPath) {
53+
targetPath = currentPath;
54+
foundPath = true;
55+
break;
56+
}
57+
}
58+
}
59+
60+
if (!foundPath) {
61+
console.error(
62+
chalk.red(`Could not find a worktree for branch "${pathOrBranch}".`)
63+
);
64+
process.exit(1);
65+
}
66+
}
67+
68+
// Verify the target path exists and is a git worktree
69+
try {
70+
await stat(targetPath);
71+
await stat(resolve(targetPath, ".git")); // Check if it's a git worktree
72+
} catch (error) {
73+
console.error(
74+
chalk.red(
75+
`The path "${targetPath}" does not exist or is not a git worktree.`
76+
)
77+
);
78+
process.exit(1);
79+
}
80+
81+
// Open in the specified editor (or use configured default)
82+
const configuredEditor = getDefaultEditor();
83+
const editorCommand = options.editor || configuredEditor;
84+
console.log(chalk.blue(`Opening ${targetPath} in ${editorCommand}...`));
85+
86+
try {
87+
await execa(editorCommand, [targetPath], { stdio: "inherit" });
88+
console.log(
89+
chalk.green(`Successfully opened worktree in ${editorCommand}.`)
90+
);
91+
} catch (editorError) {
92+
console.error(
93+
chalk.red(
94+
`Failed to open editor "${editorCommand}". Please ensure it's installed and in your PATH.`
95+
)
96+
);
97+
process.exit(1);
98+
}
99+
} catch (error) {
100+
if (error instanceof Error) {
101+
console.error(chalk.red("Failed to open worktree:"), error.message);
102+
} else {
103+
console.error(chalk.red("Failed to open worktree:"), error);
104+
}
105+
process.exit(1);
106+
}
107+
}

src/index.ts

Lines changed: 110 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,85 +9,133 @@ import { mergeWorktreeHandler } from "./commands/merge.js";
99
import { purgeWorktreesHandler } from "./commands/purge.js";
1010
import { configHandler } from "./commands/config.js";
1111
import { prWorktreeHandler } from "./commands/pr.js";
12+
import { openWorktreeHandler } from "./commands/open.js";
1213

1314
const program = new Command();
1415

1516
program
16-
.name("wt")
17-
.description("Manage git worktrees and open them in the Cursor editor.")
18-
.version("1.0.0");
17+
.name("wt")
18+
.description("Manage git worktrees and open them in the Cursor editor.")
19+
.version("1.0.0");
1920

2021
program
21-
.command("new")
22-
.argument("[branchName]", "Name of the branch to base this worktree on")
23-
.option("-p, --path <path>", "Relative path/folder name for new worktree")
24-
.option("-c, --checkout", "Create new branch if it doesn't exist and checkout automatically", false)
25-
.option("-i, --install <packageManager>", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)")
26-
.option("-e, --editor <editor>", "Editor to use for opening the worktree (e.g., code, webstorm, windsurf, etc.)")
27-
.description("Create a new worktree for the specified branch, install dependencies if specified, and open in editor.")
28-
.action(newWorktreeHandler);
22+
.command("new")
23+
.argument("[branchName]", "Name of the branch to base this worktree on")
24+
.option("-p, --path <path>", "Relative path/folder name for new worktree")
25+
.option(
26+
"-c, --checkout",
27+
"Create new branch if it doesn't exist and checkout automatically",
28+
false
29+
)
30+
.option(
31+
"-i, --install <packageManager>",
32+
"Package manager to use for installing dependencies (npm, pnpm, bun, etc.)"
33+
)
34+
.option(
35+
"-e, --editor <editor>",
36+
"Editor to use for opening the worktree (e.g., code, webstorm, windsurf, etc.)"
37+
)
38+
.description(
39+
"Create a new worktree for the specified branch, install dependencies if specified, and open in editor."
40+
)
41+
.action(newWorktreeHandler);
2942

3043
program
31-
.command("list")
32-
.alias("ls")
33-
.description("List all existing worktrees for this repository.")
34-
.action(listWorktreesHandler);
44+
.command("list")
45+
.alias("ls")
46+
.description("List all existing worktrees for this repository.")
47+
.action(listWorktreesHandler);
3548

3649
program
37-
.command("remove")
38-
.alias("rm")
39-
.argument("[pathOrBranch]", "Path of the worktree or branch to remove.")
40-
.option("-f, --force", "Force removal of worktree and deletion of the folder", false)
41-
.description("Remove a specified worktree. Cleans up the .git/worktrees references.")
42-
.action(removeWorktreeHandler);
50+
.command("remove")
51+
.alias("rm")
52+
.argument("[pathOrBranch]", "Path of the worktree or branch to remove.")
53+
.option(
54+
"-f, --force",
55+
"Force removal of worktree and deletion of the folder",
56+
false
57+
)
58+
.description(
59+
"Remove a specified worktree. Cleans up the .git/worktrees references."
60+
)
61+
.action(removeWorktreeHandler);
4362

4463
program
45-
.command("merge")
46-
.argument("<branchName>", "Name of the branch to merge from")
47-
.option("-f, --force", "Force removal of worktree after merge", false)
48-
.description("Commit changes in the target branch and merge them into the current branch, then remove the branch/worktree")
49-
.action(mergeWorktreeHandler);
64+
.command("merge")
65+
.argument("<branchName>", "Name of the branch to merge from")
66+
.option("-f, --force", "Force removal of worktree after merge", false)
67+
.description(
68+
"Commit changes in the target branch and merge them into the current branch, then remove the branch/worktree"
69+
)
70+
.action(mergeWorktreeHandler);
5071

5172
program
52-
.command("purge")
53-
.description("Safely remove all worktrees except for the main branch, with confirmation.")
54-
.action(purgeWorktreesHandler);
73+
.command("purge")
74+
.description(
75+
"Safely remove all worktrees except for the main branch, with confirmation."
76+
)
77+
.action(purgeWorktreesHandler);
5578

5679
program
57-
.command("pr")
58-
.argument("<prNumber>", "GitHub Pull Request number to create a worktree from")
59-
.option("-p, --path <path>", "Specify a custom path for the worktree (defaults to repoName-branchName)")
60-
.option("-i, --install <packageManager>", "Package manager to use for installing dependencies (npm, pnpm, bun, etc.)")
61-
.option("-e, --editor <editor>", "Editor to use for opening the worktree (overrides default editor)")
62-
.description("Fetch the branch for a given GitHub PR number and create a worktree.")
63-
.action(prWorktreeHandler);
80+
.command("pr")
81+
.argument(
82+
"<prNumber>",
83+
"GitHub Pull Request number to create a worktree from"
84+
)
85+
.option(
86+
"-p, --path <path>",
87+
"Specify a custom path for the worktree (defaults to repoName-branchName)"
88+
)
89+
.option(
90+
"-i, --install <packageManager>",
91+
"Package manager to use for installing dependencies (npm, pnpm, bun, etc.)"
92+
)
93+
.option(
94+
"-e, --editor <editor>",
95+
"Editor to use for opening the worktree (overrides default editor)"
96+
)
97+
.description(
98+
"Fetch the branch for a given GitHub PR number and create a worktree."
99+
)
100+
.action(prWorktreeHandler);
64101

65102
program
66-
.command("config")
67-
.description("Manage CLI configuration settings.")
68-
.addCommand(
69-
new Command("set")
70-
.description("Set a configuration value.")
71-
.addCommand(
72-
new Command("editor")
73-
.argument("<editorName>", "Name of the editor command (e.g., code, cursor, webstorm)")
74-
.description("Set the default editor to open worktrees in.")
75-
.action((editorName) => configHandler('set', 'editor', editorName))
76-
)
77-
)
78-
.addCommand(
79-
new Command("get")
80-
.description("Get a configuration value.")
81-
.addCommand(
82-
new Command("editor")
83-
.description("Get the currently configured default editor.")
84-
.action(() => configHandler('get', 'editor'))
85-
)
103+
.command("open")
104+
.argument("[pathOrBranch]", "Path to worktree or branch name to open")
105+
.option(
106+
"-e, --editor <editor>",
107+
"Editor to use for opening the worktree (overrides default editor)"
108+
)
109+
.description("Open an existing worktree in the editor.")
110+
.action(openWorktreeHandler);
111+
112+
program
113+
.command("config")
114+
.description("Manage CLI configuration settings.")
115+
.addCommand(
116+
new Command("set").description("Set a configuration value.").addCommand(
117+
new Command("editor")
118+
.argument(
119+
"<editorName>",
120+
"Name of the editor command (e.g., code, cursor, webstorm)"
121+
)
122+
.description("Set the default editor to open worktrees in.")
123+
.action((editorName) => configHandler("set", "editor", editorName))
86124
)
87-
.addCommand(
88-
new Command("path")
89-
.description("Show the path to the configuration file.")
90-
.action(() => configHandler('path'))
91-
);
125+
)
126+
.addCommand(
127+
new Command("get")
128+
.description("Get a configuration value.")
129+
.addCommand(
130+
new Command("editor")
131+
.description("Get the currently configured default editor.")
132+
.action(() => configHandler("get", "editor"))
133+
)
134+
)
135+
.addCommand(
136+
new Command("path")
137+
.description("Show the path to the configuration file.")
138+
.action(() => configHandler("path"))
139+
);
92140

93-
program.parse(process.argv);
141+
program.parse(process.argv);

0 commit comments

Comments
 (0)