pf 0.0.5 → 0.0.6
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 +95 -44
- package/dist/index.js +41 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">🌲<br/><code>pf</code></h1>
|
|
3
|
+
<p align="center">Human- and agent-friendly Git multitasking. Powered by worktrees.
|
|
4
|
+
<br/>
|
|
5
|
+
by <a href="https://x.com/colinhacks">@colinhacks</a>
|
|
6
|
+
</p>
|
|
7
|
+
</p>
|
|
8
|
+
<br/>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT" rel="nofollow"><img src="https://img.shields.io/github/license/pullfrog/pf" alt="License"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/pf" rel="nofollow"><img src="https://img.shields.io/npm/dw/pf.svg" alt="npm"></a>
|
|
13
|
+
<a href="https://github.com/pullfrog/pf" rel="nofollow"><img src="https://img.shields.io/github/stars/pullfrog/pf" alt="stars"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<br/>
|
|
17
|
+
<br/>
|
|
18
|
+
<br/>
|
|
4
19
|
|
|
5
20
|
## Install
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
|
-
npm i -g pf
|
|
23
|
+
$ npm i -g pf
|
|
9
24
|
```
|
|
10
25
|
|
|
11
|
-
<br
|
|
26
|
+
<br/>
|
|
12
27
|
|
|
13
|
-
## How
|
|
28
|
+
## How `pf` works
|
|
14
29
|
|
|
15
|
-
There have been many attempts to nail a DX for parallel work in the agentic coding era. Most are thin wrappers over `git worktree`.
|
|
30
|
+
There have been many attempts to nail a DX for parallel work in the agentic coding era. Most are thin wrappers over `git worktree`. Instead `pf` introduces a new paradigm: the *workshell*.
|
|
31
|
+
|
|
32
|
+
**A _workshell_ is an ephemeral worktree whose lifecycle is bound to a subshell.**
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
Here's how it works (key points in **bold**).
|
|
18
35
|
|
|
19
36
|
- You open a Git branch with `pf open <branch>` or create a new one with `pf new <branch>`.
|
|
20
37
|
- An ephemeral worktree is created for this branch (in `.git/pf/worktrees`) and **opened in a fresh subshell**.
|
|
21
38
|
- You are now in an fresh checkout of your repo that is isolated on disk. Make changes with your agent/editor of choice and commit them.
|
|
22
|
-
- You close the subshell with `pf close
|
|
23
|
-
- Your changes still exist on the associated branch.
|
|
39
|
+
- You close the subshell with `pf close`. **The associated worktree is auto-pruned**.
|
|
40
|
+
- Your changes still exist on the associated branch, as Git commits/branches are shared among all worktrees. The worktree is destroyed—but your commits aren't.
|
|
24
41
|
|
|
25
|
-
That's it. **Ephemeral worktrees whose lifecycle is bound to a subshell.** When the subshell exits, the worktree is destroyed—but your commits aren't.
|
|
42
|
+
<!-- That's it. **Ephemeral worktrees whose lifecycle is bound to a subshell.** When the subshell exits, the worktree is destroyed—but your commits aren't. -->
|
|
26
43
|
|
|
27
44
|
<!--
|
|
28
45
|
## How does it work
|
|
@@ -38,30 +55,30 @@ Here's how it works (key points in **bold**).
|
|
|
38
55
|
- **The worktree is auto-pruned** — This is key. The lifecycle of the worktree is tied to the subshell. When the subshell is closed, the worktree is destroyed. But *the commits you made inside the subshell* still exist.
|
|
39
56
|
- **Merge in your changes** — Merge/rebase your branch as you normally would, or push to GitHub to open a PR. -->
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
🙅♂️ **Never stash again** — You can `pf open` a branch even with uncommitted changes. When you exit the subshell, things will be exactly the same as they were. ☕️
|
|
58
|
+
<br/>
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
## Features
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
**Maintains branch semantics** — As with regular `git checkout`, `pf close` won't let you close the subshell if you have unstaged/uncommitted changes. Vanilla worktrees are too forgiving in this regard; it makes it far too easy to leave half-finished work in a forgotten corner of your file system.
|
|
62
|
+
This approach has some nice properties.
|
|
50
63
|
|
|
51
|
-
|
|
64
|
+
- 🖥️ **Tab-local workspaces** — Normally a `git checkout`/`git switch` changes your active branch for all terminals. With workshells, you can functionality open branches *in the current tab only*.
|
|
65
|
+
- 🌳 **Full isolation** — Each workshell is isolated on disk, so the changes you make don't interfere anything else you're doing.
|
|
66
|
+
- 🙅♂️ **Never stash again** — You can `pf open` a branch even with uncommitted changes. When you exit the subshell, things will be exactly the same as they were. ☕️
|
|
67
|
+
- **Consistent with branch semantics** — As with regular `git switch`, `pf close` won't let you close the subshell if you have unstaged/uncommitted changes. This is a feature, not a bug! Regular worktrees make it easy to lose your work in a forgotten corner of your file system.
|
|
68
|
+
- 🤖 **Agent-ready** — Spin up parallel workshells so multiple agents can work simultaneously without conflicts.
|
|
52
69
|
|
|
53
|
-
<br
|
|
70
|
+
<br/>
|
|
54
71
|
|
|
55
72
|
## Quickstart
|
|
56
73
|
|
|
57
|
-
This section is entirely linear and self-contained.
|
|
58
|
-
|
|
59
|
-
First, install `pf`.
|
|
74
|
+
This section is entirely linear and self-contained. Try running all these commands in order to get a feel for how `pf` works. First, install `pf`.
|
|
60
75
|
|
|
61
76
|
```bash
|
|
62
77
|
$ npm i -g pf
|
|
63
78
|
```
|
|
64
79
|
|
|
80
|
+
<br/>
|
|
81
|
+
|
|
65
82
|
Then clone a repo (any repo works):
|
|
66
83
|
|
|
67
84
|
```bash
|
|
@@ -69,6 +86,8 @@ $ git clone git@github.com:colinhacks/zod.git
|
|
|
69
86
|
$ cd zod
|
|
70
87
|
```
|
|
71
88
|
|
|
89
|
+
<br/>
|
|
90
|
+
|
|
72
91
|
After cloning, the `main` branch is checked out. Let's say we want to start work on a new feature:
|
|
73
92
|
|
|
74
93
|
```bash
|
|
@@ -79,6 +98,8 @@ Opened branch in ephemeral subshell at .git/pf/worktrees/zod@feat-1
|
|
|
79
98
|
Type 'pf close' to return.
|
|
80
99
|
```
|
|
81
100
|
|
|
101
|
+
<br/>
|
|
102
|
+
|
|
82
103
|
You're now in a workshell. Check where you are:
|
|
83
104
|
|
|
84
105
|
```bash
|
|
@@ -89,12 +110,16 @@ $ git branch --show-current
|
|
|
89
110
|
feat-1
|
|
90
111
|
```
|
|
91
112
|
|
|
113
|
+
<br/>
|
|
114
|
+
|
|
92
115
|
Now let's make some changes. (You can also open the repo in an IDE, start an agent run, etc.)
|
|
93
116
|
|
|
94
117
|
```bash
|
|
95
118
|
$ touch a.txt
|
|
96
119
|
```
|
|
97
120
|
|
|
121
|
+
<br/>
|
|
122
|
+
|
|
98
123
|
Now let's try to close the workshell.
|
|
99
124
|
|
|
100
125
|
```bash
|
|
@@ -104,13 +129,17 @@ $ pf close
|
|
|
104
129
|
pf close -f
|
|
105
130
|
```
|
|
106
131
|
|
|
132
|
+
<br/>
|
|
133
|
+
|
|
107
134
|
We aren't able to close because we have uncommitted changes. Let's commit them.
|
|
108
135
|
|
|
109
136
|
```bash
|
|
110
137
|
$ git add -A && git commit -am "Add a.txt"
|
|
111
138
|
```
|
|
112
139
|
|
|
113
|
-
|
|
140
|
+
<br/>
|
|
141
|
+
|
|
142
|
+
Now we can try closing again. Since our changes can be fast-forwarded from the base branch, `pf` offers to auto-merge the changes.
|
|
114
143
|
|
|
115
144
|
```bash
|
|
116
145
|
$ pf close
|
|
@@ -124,16 +153,12 @@ $ pf close
|
|
|
124
153
|
✓ Merged 'feat-1' into 'main'
|
|
125
154
|
```
|
|
126
155
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
If you type `never`, you can permanently disable the auto-merge prompt.
|
|
130
|
-
|
|
131
|
-
<br/><br/>
|
|
156
|
+
<br/>
|
|
132
157
|
|
|
133
158
|
## CLI
|
|
134
159
|
|
|
135
|
-
```
|
|
136
|
-
pf v0.x.y -
|
|
160
|
+
```sh
|
|
161
|
+
pf v0.x.y - Human- and agent-friendly Git multitasking
|
|
137
162
|
|
|
138
163
|
Usage: pf <command> [options]
|
|
139
164
|
|
|
@@ -143,28 +168,33 @@ Commands:
|
|
|
143
168
|
close Exit current workshell
|
|
144
169
|
ls List orphaned worktrees
|
|
145
170
|
status Show current branch
|
|
146
|
-
rm <branch>
|
|
171
|
+
rm <branch> Remove a branch's worktree
|
|
147
172
|
|
|
148
173
|
Options:
|
|
149
174
|
--help, -h Show help
|
|
150
175
|
--version, -v Show version
|
|
151
176
|
```
|
|
152
177
|
|
|
178
|
+
<br />
|
|
179
|
+
|
|
153
180
|
### List orphaned worktrees
|
|
154
181
|
|
|
155
182
|
Normally the worktree will be auto-pruned when you close its associated workshell. If a worktree is left behind for some reason, you can list them with `pf ls`.
|
|
156
183
|
|
|
157
184
|
```sh
|
|
158
185
|
$ pf ls
|
|
159
|
-
|
|
160
|
-
│ branch
|
|
161
|
-
|
|
162
|
-
│ main
|
|
163
|
-
│ feat-1
|
|
164
|
-
|
|
186
|
+
┌────────┬───────────┬───────────────┐
|
|
187
|
+
│ branch │ status │ created │
|
|
188
|
+
├────────┼───────────┼───────────────┤
|
|
189
|
+
│ main * │ clean │ - │
|
|
190
|
+
│ feat-1 │ 1 changed │ 5 minutes ago │
|
|
191
|
+
└────────┴───────────┴───────────────┘
|
|
165
192
|
```
|
|
166
193
|
|
|
167
|
-
|
|
194
|
+
<br />
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
### Open a branch in a workshell
|
|
168
198
|
|
|
169
199
|
You can open any existing Git branch in a workshell.
|
|
170
200
|
|
|
@@ -175,6 +205,8 @@ $ pf open feat-1
|
|
|
175
205
|
Type 'pf close' to return.
|
|
176
206
|
```
|
|
177
207
|
|
|
208
|
+
<br />
|
|
209
|
+
|
|
178
210
|
### Show current branch status
|
|
179
211
|
|
|
180
212
|
```sh
|
|
@@ -184,25 +216,44 @@ worktree: /path/to/zod
|
|
|
184
216
|
status: clean
|
|
185
217
|
```
|
|
186
218
|
|
|
187
|
-
|
|
219
|
+
<br />
|
|
220
|
+
|
|
221
|
+
### Remove a worktree
|
|
222
|
+
|
|
223
|
+
Remove the worktree for a branch (the branch itself is kept):
|
|
188
224
|
|
|
189
225
|
```sh
|
|
190
226
|
$ pf rm feat-1
|
|
191
227
|
|
|
192
|
-
✓
|
|
228
|
+
✓ Pruned worktree for feat-1
|
|
193
229
|
```
|
|
194
230
|
|
|
231
|
+
<br />
|
|
232
|
+
|
|
195
233
|
### Closing a workshell
|
|
196
234
|
|
|
197
235
|
This closes the current workshell and auto-prunes the associated worktree. Your changes survive in your branch commits.
|
|
198
236
|
|
|
199
237
|
```sh
|
|
200
|
-
pf close
|
|
238
|
+
$ pf close
|
|
239
|
+
✓ Back in main
|
|
240
|
+
Pruned worktree. Your changes are still in the 'feat-1' branch.
|
|
241
|
+
To merge your changes:
|
|
242
|
+
git merge feat-1
|
|
243
|
+
|
|
244
|
+
Auto-merge? (y/n/never) y
|
|
245
|
+
|
|
246
|
+
✓ Merged 'feat-1' into 'main'
|
|
201
247
|
```
|
|
202
248
|
|
|
249
|
+
If the branch hasn't been pushed to a remote, you'll be prompted to auto-merge:
|
|
250
|
+
|
|
251
|
+
- `y` — merge into base branch
|
|
252
|
+
- `n` — skip (branch is kept, merge manually later)
|
|
253
|
+
- `never` — permanently disable auto-merge prompt
|
|
203
254
|
|
|
204
|
-
That command will fail if you have unstaged/uncommited changes. Use the `--force`/`-f` flag to force close the workshell; **this will discard
|
|
255
|
+
That command will fail if you have unstaged/uncommited changes. Use the `--force`/`-f` flag to force close the workshell; **this will discard uncommitted changes**.
|
|
205
256
|
|
|
206
257
|
```sh
|
|
207
|
-
pf close --force
|
|
258
|
+
$ pf close --force
|
|
208
259
|
```
|
package/dist/index.js
CHANGED
|
@@ -6337,19 +6337,6 @@ function newCommand(branchName, fromBranch) {
|
|
|
6337
6337
|
import { mkdirSync as mkdirSync4 } from "fs";
|
|
6338
6338
|
import { basename as basename4, dirname as dirname4, join as join4, relative as relative2 } from "path";
|
|
6339
6339
|
function openCommand(branch) {
|
|
6340
|
-
if (branch === "root") {
|
|
6341
|
-
const mainWorktree2 = getMainWorktree();
|
|
6342
|
-
const rootBranch = getWorktreeBranch(mainWorktree2);
|
|
6343
|
-
console.log();
|
|
6344
|
-
console.log(success(bold(rootBranch)), dim(`(root worktree)`));
|
|
6345
|
-
console.log(dim("Type 'pf close' to return."));
|
|
6346
|
-
console.log();
|
|
6347
|
-
spawnShell(mainWorktree2);
|
|
6348
|
-
console.log();
|
|
6349
|
-
console.log(success("Exited root worktree"));
|
|
6350
|
-
console.log();
|
|
6351
|
-
return;
|
|
6352
|
-
}
|
|
6353
6340
|
if (isInsideWorktree()) {
|
|
6354
6341
|
const currentBranch = getCurrentBranch();
|
|
6355
6342
|
console.log();
|
|
@@ -7259,51 +7246,50 @@ function lsCommand(plain = false) {
|
|
|
7259
7246
|
import { execSync as execSync3 } from "child_process";
|
|
7260
7247
|
import { resolve as resolve2 } from "path";
|
|
7261
7248
|
function rmCommand(branch, force = false) {
|
|
7262
|
-
|
|
7263
|
-
|
|
7249
|
+
const mainWorktree = getMainWorktree();
|
|
7250
|
+
const mainBranch = getWorktreeBranch(mainWorktree);
|
|
7251
|
+
if (branch === mainBranch) {
|
|
7252
|
+
console.error(`Error: cannot remove root worktree`);
|
|
7264
7253
|
process.exit(1);
|
|
7265
7254
|
}
|
|
7266
7255
|
const worktreePath = getWorktreeForBranch(branch);
|
|
7267
|
-
if (worktreePath) {
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
} catch {
|
|
7288
|
-
}
|
|
7289
|
-
}
|
|
7256
|
+
if (!worktreePath) {
|
|
7257
|
+
console.error(`Error: no worktree found for branch '${branch}'`);
|
|
7258
|
+
process.exit(1);
|
|
7259
|
+
}
|
|
7260
|
+
const cwd = process.cwd();
|
|
7261
|
+
const absWtPath = resolve2(worktreePath);
|
|
7262
|
+
const absCwd = resolve2(cwd);
|
|
7263
|
+
if (absCwd === absWtPath || absCwd.startsWith(absWtPath + "/")) {
|
|
7264
|
+
console.error("Error: cannot remove current worktree");
|
|
7265
|
+
console.error(dim(` Try: `) + cyan(`pf close`));
|
|
7266
|
+
process.exit(1);
|
|
7267
|
+
}
|
|
7268
|
+
const status = getWorktreeStatus(worktreePath);
|
|
7269
|
+
const isDirty = status !== "clean" && status !== "";
|
|
7270
|
+
if (isDirty && !force) {
|
|
7271
|
+
console.error(warn("Uncommitted changes found. Commit, stash, or reset your changes first."));
|
|
7272
|
+
console.error(dim(` Or run: `) + cyan(`pf rm ${branch} -f`));
|
|
7273
|
+
process.exit(1);
|
|
7274
|
+
}
|
|
7275
|
+
if (isDirty && force) {
|
|
7290
7276
|
try {
|
|
7291
|
-
|
|
7277
|
+
execSync3(`git -C "${worktreePath}" reset --hard HEAD`, { stdio: "ignore" });
|
|
7278
|
+
execSync3(`git -C "${worktreePath}" clean -fd`, { stdio: "ignore" });
|
|
7292
7279
|
} catch {
|
|
7293
7280
|
}
|
|
7294
|
-
pruneWorktrees();
|
|
7295
|
-
const store = loadStore();
|
|
7296
|
-
const worktreeId = getWorktreeId(worktreePath);
|
|
7297
|
-
removeWorktreeMeta(store, worktreeId);
|
|
7298
|
-
saveStore(store);
|
|
7299
7281
|
}
|
|
7300
7282
|
try {
|
|
7301
|
-
|
|
7283
|
+
removeGitWorktree(worktreePath);
|
|
7302
7284
|
} catch {
|
|
7303
|
-
console.error(`Warning: failed to delete branch ${branch}`);
|
|
7304
7285
|
}
|
|
7286
|
+
pruneWorktrees();
|
|
7287
|
+
const store = loadStore();
|
|
7288
|
+
const worktreeId = getWorktreeId(worktreePath);
|
|
7289
|
+
removeWorktreeMeta(store, worktreeId);
|
|
7290
|
+
saveStore(store);
|
|
7305
7291
|
console.log();
|
|
7306
|
-
console.log(success(`
|
|
7292
|
+
console.log(success(`Pruned worktree for ${bold(branch)}`));
|
|
7307
7293
|
}
|
|
7308
7294
|
|
|
7309
7295
|
// commands/status.ts
|
|
@@ -7387,10 +7373,6 @@ function precloseCommand(force) {
|
|
|
7387
7373
|
process.exit(0);
|
|
7388
7374
|
}
|
|
7389
7375
|
if (force) {
|
|
7390
|
-
console.log();
|
|
7391
|
-
if (!confirm("Force-closing will discard your unstaged changes. Continue?")) {
|
|
7392
|
-
process.exit(1);
|
|
7393
|
-
}
|
|
7394
7376
|
try {
|
|
7395
7377
|
execSync5("git reset --hard HEAD", { stdio: "ignore" });
|
|
7396
7378
|
execSync5("git clean -fd", { stdio: "ignore" });
|
|
@@ -7405,7 +7387,7 @@ function precloseCommand(force) {
|
|
|
7405
7387
|
}
|
|
7406
7388
|
|
|
7407
7389
|
// index.ts
|
|
7408
|
-
var VERSION = "0.0.
|
|
7390
|
+
var VERSION = "0.0.6";
|
|
7409
7391
|
function printHelp() {
|
|
7410
7392
|
const dim2 = import_picocolors3.default.dim;
|
|
7411
7393
|
const cyan2 = import_picocolors3.default.cyan;
|
|
@@ -7420,7 +7402,7 @@ ${import_picocolors3.default.bold("Commands:")}
|
|
|
7420
7402
|
${green2("close")} Exit current subshell
|
|
7421
7403
|
${green2("ls")} List open branches
|
|
7422
7404
|
${green2("status")} Show current branch
|
|
7423
|
-
${green2("rm")} ${dim2("<branch>")}
|
|
7405
|
+
${green2("rm")} ${dim2("<branch>")} Remove a branch's worktree
|
|
7424
7406
|
|
|
7425
7407
|
${import_picocolors3.default.bold("Options:")}
|
|
7426
7408
|
${cyan2("--help")}, ${cyan2("-h")} Show help
|
|
@@ -7511,20 +7493,20 @@ ${import_picocolors3.default.bold("Description:")}
|
|
|
7511
7493
|
function printRmHelp() {
|
|
7512
7494
|
const dim2 = import_picocolors3.default.dim;
|
|
7513
7495
|
const cyan2 = import_picocolors3.default.cyan;
|
|
7514
|
-
console.log(`${import_picocolors3.default.bold("pf rm")} -
|
|
7496
|
+
console.log(`${import_picocolors3.default.bold("pf rm")} - Remove a branch's worktree
|
|
7515
7497
|
|
|
7516
7498
|
${import_picocolors3.default.bold("Usage:")} ${cyan2("pf rm")} ${dim2("<branch>")} ${dim2("[options]")}
|
|
7517
7499
|
|
|
7518
7500
|
${import_picocolors3.default.bold("Arguments:")}
|
|
7519
|
-
${dim2("<branch>")} Branch to
|
|
7501
|
+
${dim2("<branch>")} Branch whose worktree to remove ${dim2("(required)")}
|
|
7520
7502
|
|
|
7521
7503
|
${import_picocolors3.default.bold("Options:")}
|
|
7522
|
-
${cyan2("-f")}, ${cyan2("--force")} Discard uncommitted changes and
|
|
7504
|
+
${cyan2("-f")}, ${cyan2("--force")} Discard uncommitted changes and remove
|
|
7523
7505
|
|
|
7524
7506
|
${import_picocolors3.default.bold("Description:")}
|
|
7525
|
-
|
|
7526
|
-
Fails if
|
|
7527
|
-
Cannot
|
|
7507
|
+
Removes the worktree for the specified branch. The branch itself is kept.
|
|
7508
|
+
Fails if worktree has uncommitted changes (use -f/--force to discard).
|
|
7509
|
+
Cannot remove a worktree you're currently inside.`);
|
|
7528
7510
|
}
|
|
7529
7511
|
function printStatusHelp() {
|
|
7530
7512
|
const cyan2 = import_picocolors3.default.cyan;
|
|
@@ -7602,9 +7584,6 @@ switch (cmd) {
|
|
|
7602
7584
|
}
|
|
7603
7585
|
openCommand(args[1]);
|
|
7604
7586
|
break;
|
|
7605
|
-
case "root":
|
|
7606
|
-
openCommand("root");
|
|
7607
|
-
break;
|
|
7608
7587
|
case "ls":
|
|
7609
7588
|
if (isHelp(args[1])) {
|
|
7610
7589
|
printLsHelp();
|