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.
Files changed (3) hide show
  1. package/README.md +95 -44
  2. package/dist/index.js +41 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,28 +1,45 @@
1
- # `pf` — Human- and agent-friendly Git multitasking
2
-
3
- <br/><br/>
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/><br/>
26
+ <br/>
12
27
 
13
- ## How does it work
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`. That's not what `pf` is (though worktrees are used internally).
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
- Instead `pf` introduces a new paradigm: the *workshell*. A workshell is **an ephemeral worktree whose lifecycle is bound to a subshell**. Here's how it works (key points in **bold**).
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`, which **auto-prunes the associated worktree**.
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
- This approach has some nice properties.
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
- 🖥️ **Tab-local checkouts** — 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*.
60
+ ## Features
46
61
 
47
- 🌳 **Isolated workspaces** Each workshell is isolated on disk, so the changes you make don't affect anything else you're doing.
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
- 🤖 **Agent-ready** — Spin up parallel workshells so multiple agents can work simultaneously without conflicts.
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/><br/>
70
+ <br/>
54
71
 
55
72
  ## Quickstart
56
73
 
57
- This section is entirely linear and self-contained. You can run all these commands in order to get a feel for how `pf` works.
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
- Now we can close again. On a successful close, you'll be prompted to merge your changes into the base branch.
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
- If you type `n`, you can manually handle your own merging logic. The changes you made are still available in the `feat-1` branch.
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 - Open branches in workshells
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> Delete a branch and its worktree
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 │ status │ created
161
- ├──────────────────────┼────────┼─────────────────┤
162
- │ main │ clean │ -
163
- │ feat-1 * clean │ 5 minutes ago
164
- └──────────────────────┴────────┴─────────────────┘
186
+ ┌────────┬───────────┬───────────────┐
187
+ │ branch │ status │ created
188
+ ├────────┼───────────┼───────────────┤
189
+ │ main * │ clean │ -
190
+ │ feat-1 │ 1 changed │ 5 minutes ago
191
+ └────────┴───────────┴───────────────┘
165
192
  ```
166
193
 
167
- ### Open a branch
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
- ### Remove a branch
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
- Deleted feat-1
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 your changes**.
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
- if (!branchExists(branch)) {
7263
- console.error(`Error: branch '${branch}' not found`);
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
- const cwd = process.cwd();
7269
- const absWtPath = resolve2(worktreePath);
7270
- const absCwd = resolve2(cwd);
7271
- if (absCwd.startsWith(absWtPath)) {
7272
- console.error("Error: cannot remove current branch's worktree");
7273
- console.error(dim(` Try: `) + cyan(`pf close`));
7274
- process.exit(1);
7275
- }
7276
- const status = getWorktreeStatus(worktreePath);
7277
- const isDirty = status !== "clean" && status !== "";
7278
- if (isDirty && !force) {
7279
- console.error(warn("Uncommitted changes found. Commit, stash, or reset your changes first."));
7280
- console.error(dim(` Or run: `) + cyan(`pf rm ${branch} -f`));
7281
- process.exit(1);
7282
- }
7283
- if (isDirty && force) {
7284
- try {
7285
- execSync3(`git -C "${worktreePath}" reset --hard HEAD`, { stdio: "ignore" });
7286
- execSync3(`git -C "${worktreePath}" clean -fd`, { stdio: "ignore" });
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
- removeGitWorktree(worktreePath);
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
- deleteBranch(branch);
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(`Deleted ${bold(branch)}`));
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.5";
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>")} Delete a branch and its worktree
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")} - Delete a branch and its worktree
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 delete ${dim2("(required)")}
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 delete
7504
+ ${cyan2("-f")}, ${cyan2("--force")} Discard uncommitted changes and remove
7523
7505
 
7524
7506
  ${import_picocolors3.default.bold("Description:")}
7525
- Deletes the specified branch and removes its worktree if one exists.
7526
- Fails if branch has uncommitted changes (use -f/--force to discard).
7527
- Cannot delete a branch you're currently inside.`);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pf",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Agent- and human-friendly Git multitasking, powered by worktrees",
5
5
  "type": "module",
6
6
  "license": "MIT",