@henryqw/pi-herdr-done 0.2.0 → 0.2.2

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,17 @@ pi install npm:@henryqw/pi-herdr-done
12
12
 
13
13
  | Surface | Type | Purpose |
14
14
  | --- | --- | --- |
15
- | `/done` | command | Close current Herdr worktree workspace and remove its clean checkout. |
16
- | `/done --force` | command | Close workspace and remove checkout even when worktree is dirty. |
15
+ | `/done` | command | Remove the current worktree checkout and close only this session's tab. |
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
- herdr worktree remove --workspace "$HERDR_WORKSPACE_ID"
21
+ git worktree remove .
22
+ herdr tab close "$HERDR_TAB_ID"
22
23
  ```
23
24
 
24
- Herdr closes the worktree workspace and removes its checkout. Command requires Pi running inside Herdr with `HERDR_ENV=1` and `HERDR_WORKSPACE_ID` set.
25
+ 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.
25
26
 
26
27
  Dirty worktrees make `/done` fail. Commit or discard changes, or use `/done --force` to explicitly delete them.
27
28
 
@@ -1,10 +1,24 @@
1
+ import { tmpdir } from "node:os";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import { createHerdrClient } from "@henryqw/pi-herdr";
4
+ import { lock } from "proper-lockfile";
5
+
6
+ type ExecResult = { stdout: string; stderr: string; code: number; killed?: boolean };
7
+
8
+ type SnapshotPane = { tab_id?: unknown; cwd?: unknown };
3
9
 
4
10
  export default function herdrDoneExtension(pi: ExtensionAPI): void {
5
11
  const herdr = createHerdrClient<{ cwd: string }>((command, args, options) =>
6
12
  pi.exec(command, [...args], options));
7
13
 
14
+ const execOrThrow = async (command: string, args: string[], cwd: string): Promise<string> => {
15
+ const result = await pi.exec(command, args, { cwd }) as unknown as ExecResult;
16
+ if (result.code !== 0 || result.killed) {
17
+ throw new Error(`${command} ${args[0]} failed: ${result.stderr.trim() || "killed"}`);
18
+ }
19
+ return result.stdout;
20
+ };
21
+
8
22
  pi.registerCommand("done", {
9
23
  description: "Close and remove the current Herdr worktree",
10
24
  handler: async (args, ctx) => {
@@ -13,8 +27,8 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
13
27
  if (process.env.HERDR_ENV !== "1") {
14
28
  throw new Error("/done requires the current Pi session inside Herdr (HERDR_ENV=1).");
15
29
  }
16
- const workspaceId = process.env.HERDR_WORKSPACE_ID?.trim();
17
- if (!workspaceId) throw new Error("HERDR_WORKSPACE_ID is missing.");
30
+ const tabId = process.env.HERDR_TAB_ID?.trim();
31
+ if (!tabId) throw new Error("HERDR_TAB_ID is missing.");
18
32
 
19
33
  if (option !== "--force") {
20
34
  const confirmed = await ctx.ui.confirm("Done", "Close and remove the current Herdr worktree?");
@@ -22,10 +36,32 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
22
36
  }
23
37
 
24
38
  await ctx.waitForIdle();
25
- await herdr.run([
26
- "worktree", "remove", "--workspace", workspaceId,
27
- ...(option === "--force" ? ["--force"] : []),
28
- ], { cwd: ctx.cwd });
39
+ const checkout = (await execOrThrow("git", ["rev-parse", "--show-toplevel"], ctx.cwd)).trim();
40
+
41
+ const release = await lock(checkout);
42
+ try {
43
+ const snapshot = await herdr.json(["api", "snapshot"], { cwd: ctx.cwd });
44
+ const panes = (snapshot.result as { snapshot?: { panes?: SnapshotPane[] } } | undefined)?.snapshot?.panes;
45
+ if (!Array.isArray(panes)) throw new Error("herdr api snapshot returned no panes.");
46
+ const dependents = [...new Set(panes
47
+ .filter((pane): pane is SnapshotPane & { tab_id: string; cwd: string } =>
48
+ typeof pane.tab_id === "string" && pane.tab_id !== tabId &&
49
+ typeof pane.cwd === "string" &&
50
+ (pane.cwd === checkout || pane.cwd.startsWith(`${checkout}/`)))
51
+ .map((pane) => pane.tab_id))];
52
+ if (dependents.length > 0) {
53
+ throw new Error(`Worktree still used by Herdr tabs ${dependents.join(", ")}; close them first.`);
54
+ }
55
+
56
+ await execOrThrow("git", [
57
+ "worktree", "remove", ...(option === "--force" ? ["--force"] : []), checkout,
58
+ ], ctx.cwd);
59
+ } finally {
60
+ await release();
61
+ }
62
+ // Close only this session's tab so unrelated tabs survive.
63
+ // Run outside the removed checkout because its directory no longer exists.
64
+ await herdr.run(["tab", "close", tabId], { cwd: tmpdir() });
29
65
  },
30
66
  });
31
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-done",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Close and remove the current Herdr worktree from Pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -27,7 +27,8 @@
27
27
  "@earendil-works/pi-coding-agent": "^0.84.2"
28
28
  },
29
29
  "dependencies": {
30
- "@henryqw/pi-herdr": "^0.1.2"
30
+ "@henryqw/pi-herdr": "^0.1.2",
31
+ "proper-lockfile": "^4.1.2"
31
32
  },
32
33
  "repository": {
33
34
  "type": "git",