worktree-add 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +25 -9
- package/dist/worktree/destination-directory.js +23 -11
- package/package.json +17 -5
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# worktree-add
|
|
2
|
+
|
|
3
|
+
Create a Git worktree next to your current repo for a branch, copy useful local files, install deps, and open it in your editor.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Running `worktree-add <branch>` from inside a repo:
|
|
8
|
+
|
|
9
|
+
1. Normalizes `<branch>` (supports `origin/foo`, `refs/heads/foo`, etc.).
|
|
10
|
+
2. Refuses if that branch is already checked out in any worktree.
|
|
11
|
+
3. Picks a destination next to your current repo: `../<repo>-<safe-branch>`.
|
|
12
|
+
4. If the destination exists, asks before moving it to the system trash.
|
|
13
|
+
5. Fetches `origin/<branch>` when needed and creates a git worktree:
|
|
14
|
+
- reuses an existing local branch
|
|
15
|
+
- or creates a tracking branch from `origin/<branch>`
|
|
16
|
+
- or creates a new branch from the current `HEAD`
|
|
17
|
+
6. Copies untracked / ignored files into the new worktree, skipping heavy stuff
|
|
18
|
+
(`node_modules`, `dist`, `.next`, caches, virtualenvs, etc.).
|
|
19
|
+
7. Detects your package manager and installs dependencies with lockfile‑safe flags
|
|
20
|
+
(`npm ci`, `pnpm install --frozen-lockfile`, `yarn install --immutable`, etc.).
|
|
21
|
+
8. If the project uses Next.js and supports it, runs `next typegen`.
|
|
22
|
+
9. Opens the new worktree in your editor.
|
|
23
|
+
|
|
24
|
+
Your original checkout is left untouched.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js ≥ 22.14.0
|
|
29
|
+
- Git with `git worktree` support
|
|
30
|
+
|
|
31
|
+
## Install / run
|
|
32
|
+
|
|
33
|
+
You usually don’t need a global install.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# inside /my/path/my-app
|
|
37
|
+
# one‑off
|
|
38
|
+
npx worktree-add feature/my-branch
|
|
39
|
+
|
|
40
|
+
# or install globally
|
|
41
|
+
pnpm add -g worktree-add # or: npm i -g worktree-add
|
|
42
|
+
worktree-add feature/my-branch
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run it from anywhere inside an existing worktree of the repo.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# inside /my/path/my-app
|
|
51
|
+
worktree-add <branch>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run this inside an existing worktree of your project—the tool discovers the repo root from your current directory and creates the sibling worktree next to it.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# inside /my/path/my-app
|
|
60
|
+
# reuses a local branch, tracks origin/<branch> if it exists, otherwise creates a new branch from current HEAD
|
|
61
|
+
worktree-add feature/login-form
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A new branch from the current HEAD is created only when the branch does not already
|
|
65
|
+
exist locally or on `origin/`.
|
|
66
|
+
|
|
67
|
+
Destination directory (assuming repo named `my-app`):
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
/my/path/my-app # current checkout
|
|
71
|
+
/my/path/my-app-feature-login-form # new worktree for branch "feature/login-form"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Editor control
|
|
75
|
+
|
|
76
|
+
By default the tool opens the new worktree in:
|
|
77
|
+
|
|
78
|
+
1. `--editor <command>` if passed
|
|
79
|
+
2. `WORKTREE_ADD_EDITOR` env var
|
|
80
|
+
3. otherwise `code`
|
|
81
|
+
|
|
82
|
+
Only simple command names are allowed (no `;`, `&`, pipes, etc.) to avoid shell injection. Examples:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
worktree-add feature/foo -e code
|
|
86
|
+
WORKTREE_ADD_EDITOR=vim worktree-add feature/foo
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If launching the editor fails, the worktree still stays created and ready.
|
|
90
|
+
|
|
91
|
+
Tip: add a shell helper with your preferred editor in your shell profile. Example snippet to add:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
worktree-add() { WORKTREE_ADD_EDITOR=cursor command worktree-add "$@" }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Add it to your shell profile:
|
|
98
|
+
|
|
99
|
+
- zsh: `~/.zshrc` or `~/.zprofile`
|
|
100
|
+
- bash: `~/.bashrc` or `~/.bash_profile`
|
|
101
|
+
- fish: `~/.config/fish/config.fish`
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,4 +6,10 @@
|
|
|
6
6
|
* Note: the destination is placed next to whichever worktree you run this from,
|
|
7
7
|
* not necessarily the original "main" checkout.
|
|
8
8
|
*/
|
|
9
|
+
interface ResolveEditorInput {
|
|
10
|
+
optionEditor?: string;
|
|
11
|
+
environmentEditor?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveEditor({ optionEditor, environmentEditor, }?: ResolveEditorInput): string;
|
|
14
|
+
export declare function isEditorCommandSafe(editor: string): boolean;
|
|
9
15
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,22 @@ import { copyUntrackedFiles } from "./worktree/untracked-file-copy.js";
|
|
|
15
15
|
import { fetchRemoteBranch, createWorktree } from "./git/worktree-creation.js";
|
|
16
16
|
import { handleExistingDirectory } from "./worktree/destination-directory.js";
|
|
17
17
|
import { setupProject } from "./project/setup.js";
|
|
18
|
+
export function resolveEditor({ optionEditor, environmentEditor, } = {}) {
|
|
19
|
+
const normalizedOption = optionEditor?.trim();
|
|
20
|
+
const normalizedEnvironment = environmentEditor?.trim();
|
|
21
|
+
return normalizedOption || normalizedEnvironment || "code";
|
|
22
|
+
}
|
|
23
|
+
export function isEditorCommandSafe(editor) {
|
|
24
|
+
const normalized = editor.normalize("NFKC");
|
|
25
|
+
const trimmed = normalized.trim();
|
|
26
|
+
// Reject empty or excessively long commands
|
|
27
|
+
if (trimmed.length === 0 || trimmed.length > 256) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Reject common shell metacharacters, whitespace, and control characters to prevent injection
|
|
31
|
+
const unsafeCharacters = /[\\;&|`$(){}<>\s]|\p{Cc}/u;
|
|
32
|
+
return !unsafeCharacters.test(trimmed);
|
|
33
|
+
}
|
|
18
34
|
async function main(branchRaw, options) {
|
|
19
35
|
const branch = normalizeBranchName(branchRaw);
|
|
20
36
|
// Prevent attempting to add a worktree for a branch that is already checked out
|
|
@@ -40,14 +56,12 @@ async function main(branchRaw, options) {
|
|
|
40
56
|
// Step 4: Install dependencies and run project-specific setup
|
|
41
57
|
await setupProject(destinationDirectory);
|
|
42
58
|
// Step 5: Open the new worktree in the editor
|
|
43
|
-
const editor =
|
|
59
|
+
const editor = resolveEditor({
|
|
60
|
+
optionEditor: options.editor,
|
|
61
|
+
environmentEditor: process.env.WORKTREE_ADD_EDITOR,
|
|
62
|
+
});
|
|
44
63
|
// Validate editor command to prevent injection attacks
|
|
45
|
-
if (editor
|
|
46
|
-
editor.includes("&") ||
|
|
47
|
-
editor.includes("|") ||
|
|
48
|
-
editor.includes("`") ||
|
|
49
|
-
editor.includes("$") ||
|
|
50
|
-
editor.includes("\n")) {
|
|
64
|
+
if (!isEditorCommandSafe(editor)) {
|
|
51
65
|
exitWithMessage("Invalid editor command: shell metacharacters not allowed.\n" +
|
|
52
66
|
"Please use a simple editor name (e.g., 'code', 'cursor', 'vim').");
|
|
53
67
|
}
|
|
@@ -65,7 +79,7 @@ const program = new Command()
|
|
|
65
79
|
.description("Create or reuse a Git worktree for a branch as a sibling of the current worktree")
|
|
66
80
|
.version(packageJson.version)
|
|
67
81
|
.argument("<branch>", "branch name for the worktree")
|
|
68
|
-
.option("-e, --editor <command>", "Editor to open the worktree with (default: WORKTREE_ADD_EDITOR env var or '
|
|
82
|
+
.option("-e, --editor <command>", "Editor to open the worktree with (default: WORKTREE_ADD_EDITOR env var or 'code')")
|
|
69
83
|
.action(async (branch, options) => {
|
|
70
84
|
try {
|
|
71
85
|
await main(branch, options);
|
|
@@ -75,4 +89,6 @@ const program = new Command()
|
|
|
75
89
|
process.exitCode = 1;
|
|
76
90
|
}
|
|
77
91
|
});
|
|
78
|
-
|
|
92
|
+
if (!process.env.VITEST) {
|
|
93
|
+
program.parse();
|
|
94
|
+
}
|
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for managing worktree directories
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Cross-platform trash functionality using the trash package.
|
|
8
|
+
*
|
|
9
|
+
* Platform-specific behavior:
|
|
10
|
+
* - macOS: Uses Finder's trash (~/.Trash)
|
|
11
|
+
* - Windows: Uses Recycle Bin
|
|
12
|
+
* - Linux: Uses freedesktop.org trash specification (~/.local/share/Trash)
|
|
13
|
+
*
|
|
14
|
+
* Note: On headless/CI systems, the trash directory is still created but
|
|
15
|
+
* may not be visible in a GUI. Files can be recovered by navigating to
|
|
16
|
+
* the platform-specific trash location.
|
|
17
|
+
*/
|
|
18
|
+
import trash from "trash";
|
|
7
19
|
import path from "node:path";
|
|
8
20
|
import { fileExists, confirm, exitWithMessage } from "../git/git.js";
|
|
9
21
|
/**
|
|
@@ -14,20 +26,20 @@ export async function handleExistingDirectory(destinationDirectory) {
|
|
|
14
26
|
if (!(await fileExists(destinationDirectory))) {
|
|
15
27
|
return;
|
|
16
28
|
}
|
|
17
|
-
const proceed = await confirm(`Directory '${path.basename(destinationDirectory)}' already exists.
|
|
29
|
+
const proceed = await confirm(`Directory '${path.basename(destinationDirectory)}' already exists. Move to trash and recreate? (You can restore it from your system trash if needed)`);
|
|
18
30
|
if (!proceed) {
|
|
19
31
|
console.log("Operation cancelled.");
|
|
20
32
|
// eslint-disable-next-line unicorn/no-process-exit -- User requested cancellation
|
|
21
33
|
process.exit(0);
|
|
22
34
|
}
|
|
23
|
-
//
|
|
24
|
-
console.log(`➤
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
exitWithMessage(`Failed to
|
|
35
|
+
// Move the existing directory to trash
|
|
36
|
+
console.log(`➤ Moving existing directory to trash...`);
|
|
37
|
+
try {
|
|
38
|
+
await trash(destinationDirectory);
|
|
39
|
+
console.log("✓ Directory moved to trash successfully");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error("Error details:", error);
|
|
43
|
+
exitWithMessage(`Failed to move existing directory to trash: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
44
|
}
|
|
33
45
|
}
|
package/package.json
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
"name": "worktree-add",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "1.
|
|
6
|
-
"description": "",
|
|
5
|
+
"version": "1.2.0",
|
|
6
|
+
"description": "Create a Git worktree next to your current repo for a branch, copy useful local files, install deps, and open it in your editor.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Jercik/worktree-add.git"
|
|
10
10
|
},
|
|
11
|
+
"homepage": "https://github.com/Jercik/worktree-add#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Jercik/worktree-add/issues"
|
|
14
|
+
},
|
|
11
15
|
"type": "module",
|
|
12
16
|
"bin": {
|
|
13
17
|
"worktree-add": "bin/worktree-add"
|
|
@@ -35,14 +39,22 @@
|
|
|
35
39
|
"knip": "knip",
|
|
36
40
|
"fta:check": "fta-check --threshold 55"
|
|
37
41
|
},
|
|
38
|
-
"keywords": [
|
|
42
|
+
"keywords": [
|
|
43
|
+
"git",
|
|
44
|
+
"worktree",
|
|
45
|
+
"cli",
|
|
46
|
+
"workflow",
|
|
47
|
+
"git-worktree",
|
|
48
|
+
"development"
|
|
49
|
+
],
|
|
39
50
|
"packageManager": "pnpm@10.22.0",
|
|
40
51
|
"engines": {
|
|
41
|
-
"node": ">=22.
|
|
52
|
+
"node": ">=22.14.0"
|
|
42
53
|
},
|
|
43
54
|
"dependencies": {
|
|
44
55
|
"commander": "^14.0.2",
|
|
45
|
-
"package-manager-detector": "^1.5.0"
|
|
56
|
+
"package-manager-detector": "^1.5.0",
|
|
57
|
+
"trash": "^10.0.0"
|
|
46
58
|
},
|
|
47
59
|
"devDependencies": {
|
|
48
60
|
"@commitlint/cli": "^20.1.0",
|