@henryqw/pi-herdr-done 0.2.2 → 0.3.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 CHANGED
@@ -12,16 +12,19 @@ pi install npm:@henryqw/pi-herdr-done
12
12
 
13
13
  | Surface | Type | Purpose |
14
14
  | --- | --- | --- |
15
- | `/done` | command | Remove the current worktree checkout and close only this session's tab. |
15
+ | `/done` | command | Remove the current worktree checkout, fast-forward the parent workspace, and close only this session's tab. |
16
16
  | `/done --force` | command | Same, even when the worktree is dirty. |
17
17
 
18
18
  Both forms wait for Pi to become idle. `/done` asks for confirmation first; `/done --force` skips it because the flag already states intent. Normal removal runs:
19
19
 
20
20
  ```bash
21
21
  git worktree remove .
22
+ git -C <parent> pull --ff-only
22
23
  herdr tab close "$HERDR_TAB_ID"
23
24
  ```
24
25
 
26
+ The parent pull runs only when this session ran in a linked worktree with a non-bare primary; it is skipped when the parent has diverged (`--ff-only` fails safely) or when another Herdr tab is working in the parent. Concurrent completions serialize on a lock around the parent checkout.
27
+
25
28
  Only the current session's tab closes, so sibling tabs in the same workspace survive. Command requires Pi running inside Herdr with `HERDR_ENV=1` and `HERDR_TAB_ID` set.
26
29
 
27
30
  Dirty worktrees make `/done` fail. Commit or discard changes, or use `/done --force` to explicitly delete them.
@@ -20,7 +20,7 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
20
20
  };
21
21
 
22
22
  pi.registerCommand("done", {
23
- description: "Close and remove the current Herdr worktree",
23
+ description: "Remove the current Herdr worktree, then fast-forward its parent workspace",
24
24
  handler: async (args, ctx) => {
25
25
  const option = args.trim();
26
26
  if (option && option !== "--force") throw new Error("Usage: /done [--force]");
@@ -37,17 +37,27 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
37
37
 
38
38
  await ctx.waitForIdle();
39
39
  const checkout = (await execOrThrow("git", ["rev-parse", "--show-toplevel"], ctx.cwd)).trim();
40
+ // First record of the NUL-delimited list is always the main worktree;
41
+ // -z keeps paths containing newlines parseable.
42
+ const worktreeFields = (await execOrThrow("git", ["worktree", "list", "--porcelain", "-z"], checkout)).split("\0");
43
+ const mainCheckout = worktreeFields[0]?.startsWith("worktree ") ? worktreeFields[0].slice("worktree ".length) : undefined;
44
+ if (!mainCheckout) throw new Error("git worktree list returned no main worktree.");
45
+ // A bare primary has no working tree to pull into.
46
+ const parentIsBare = mainCheckout !== checkout &&
47
+ worktreeFields.slice(1, worktreeFields.indexOf("")).includes("bare");
40
48
 
41
49
  const release = await lock(checkout);
42
50
  try {
43
51
  const snapshot = await herdr.json(["api", "snapshot"], { cwd: ctx.cwd });
44
52
  const panes = (snapshot.result as { snapshot?: { panes?: SnapshotPane[] } } | undefined)?.snapshot?.panes;
45
53
  if (!Array.isArray(panes)) throw new Error("herdr api snapshot returned no panes.");
54
+ const guarded = mainCheckout === checkout ? [checkout] : [checkout, mainCheckout];
55
+ const isGuarded = (cwd: string) =>
56
+ guarded.some((root) => cwd === root || cwd.startsWith(`${root}/`));
46
57
  const dependents = [...new Set(panes
47
58
  .filter((pane): pane is SnapshotPane & { tab_id: string; cwd: string } =>
48
59
  typeof pane.tab_id === "string" && pane.tab_id !== tabId &&
49
- typeof pane.cwd === "string" &&
50
- (pane.cwd === checkout || pane.cwd.startsWith(`${checkout}/`)))
60
+ typeof pane.cwd === "string" && isGuarded(pane.cwd))
51
61
  .map((pane) => pane.tab_id))];
52
62
  if (dependents.length > 0) {
53
63
  throw new Error(`Worktree still used by Herdr tabs ${dependents.join(", ")}; close them first.`);
@@ -59,6 +69,16 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
59
69
  } finally {
60
70
  await release();
61
71
  }
72
+ if (!parentIsBare && mainCheckout !== checkout) {
73
+ // Serialize concurrent completions pulling the same parent checkout.
74
+ // Run outside the removed checkout because its directory no longer exists.
75
+ const releasePull = await lock(mainCheckout);
76
+ try {
77
+ await execOrThrow("git", ["pull", "--ff-only"], mainCheckout);
78
+ } finally {
79
+ await releasePull();
80
+ }
81
+ }
62
82
  // Close only this session's tab so unrelated tabs survive.
63
83
  // Run outside the removed checkout because its directory no longer exists.
64
84
  await herdr.run(["tab", "close", tabId], { cwd: tmpdir() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-done",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Close and remove the current Herdr worktree from Pi.",
5
5
  "keywords": [
6
6
  "pi-package",