@henryqw/pi-herdr-done 0.4.2 → 0.5.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 +5 -4
- package/extensions/done.ts +32 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,20 +12,21 @@ pi install npm:@henryqw/pi-herdr-done
|
|
|
12
12
|
|
|
13
13
|
| Surface | Type | Purpose |
|
|
14
14
|
| --- | --- | --- |
|
|
15
|
-
| `/done` | command | Remove the current worktree checkout,
|
|
16
|
-
| `/done --force` | command | Same, even when the worktree is dirty or used by
|
|
15
|
+
| `/done` | command | Remove the current worktree checkout, close its Herdr workspace's tabs, and fast-forward the parent workspace. |
|
|
16
|
+
| `/done --force` | command | Same, even when the worktree is dirty or used by tabs in another workspace. |
|
|
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
|
+
herdr tab close <other-tabs-in-$HERDR_WORKSPACE_ID>
|
|
22
23
|
git -C <parent> pull --ff-only
|
|
23
24
|
herdr tab close "$HERDR_TAB_ID"
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
The parent pull runs only when this session ran in a linked worktree with a non-bare primary. It fails safely when the parent has diverged. Parent tabs do not block worktree removal or the parent pull. Once removal succeeds, the current
|
|
27
|
+
The parent pull runs only when this session ran in a linked worktree with a non-bare primary. It fails safely when the parent has diverged. Parent tabs do not block worktree removal or the parent pull. Once removal succeeds, every tab in the current Herdr workspace closes even when the parent pull fails. Concurrent completions serialize on a lock around the parent checkout.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Tabs in the current Herdr workspace close automatically. When a tab from another workspace still uses the current checkout, `/done` refuses and lists it by name; use `/done --force` to remove the checkout regardless. Command requires Pi running inside Herdr with `HERDR_ENV=1`, `HERDR_WORKSPACE_ID`, and `HERDR_TAB_ID` set.
|
|
29
30
|
|
|
30
31
|
Dirty worktrees make `/done` fail. Commit or discard changes, or use `/done --force` to explicitly delete them.
|
|
31
32
|
|
package/extensions/done.ts
CHANGED
|
@@ -5,11 +5,10 @@ import { createHerdrClient, withWorktreeLock } from "@henryqw/pi-herdr";
|
|
|
5
5
|
type ExecResult = { stdout: string; stderr: string; code: number; killed?: boolean };
|
|
6
6
|
|
|
7
7
|
type SnapshotPane = { tab_id?: unknown; cwd?: unknown };
|
|
8
|
-
type TabEntry = { tab_id?: unknown; label?: unknown };
|
|
8
|
+
type TabEntry = { tab_id?: unknown; workspace_id?: unknown; label?: unknown };
|
|
9
9
|
|
|
10
10
|
export default function herdrDoneExtension(pi: ExtensionAPI): void {
|
|
11
|
-
const herdr = createHerdrClient<{ cwd: string }>((
|
|
12
|
-
pi.exec(command, [...args], options));
|
|
11
|
+
const herdr = createHerdrClient<{ cwd: string }>(pi.exec.bind(pi));
|
|
13
12
|
|
|
14
13
|
const execOrThrow = async (command: string, args: string[], cwd: string): Promise<string> => {
|
|
15
14
|
const result = await pi.exec(command, args, { cwd }) as unknown as ExecResult;
|
|
@@ -20,7 +19,7 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
|
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
pi.registerCommand("done", {
|
|
23
|
-
description: "Remove the current Herdr worktree
|
|
22
|
+
description: "Remove the current Herdr worktree and close its workspace tabs",
|
|
24
23
|
handler: async (args, ctx) => {
|
|
25
24
|
const option = args.trim();
|
|
26
25
|
if (option && option !== "--force") throw new Error("Usage: /done [--force]");
|
|
@@ -29,9 +28,11 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
|
|
|
29
28
|
}
|
|
30
29
|
const tabId = process.env.HERDR_TAB_ID?.trim();
|
|
31
30
|
if (!tabId) throw new Error("HERDR_TAB_ID is missing.");
|
|
31
|
+
const workspaceId = process.env.HERDR_WORKSPACE_ID?.trim();
|
|
32
|
+
if (!workspaceId) throw new Error("HERDR_WORKSPACE_ID is missing.");
|
|
32
33
|
|
|
33
34
|
if (option !== "--force") {
|
|
34
|
-
const confirmed = await ctx.ui.confirm("Done", "Close and remove the current
|
|
35
|
+
const confirmed = await ctx.ui.confirm("Done", "Close its Herdr tabs and remove the current worktree?");
|
|
35
36
|
if (!confirmed) return;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -46,28 +47,40 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
|
|
|
46
47
|
const parentIsBare = mainCheckout !== checkout &&
|
|
47
48
|
worktreeFields.slice(1, worktreeFields.indexOf("")).includes("bare");
|
|
48
49
|
|
|
50
|
+
let siblingTabIds: string[] = [];
|
|
49
51
|
await withWorktreeLock(checkout, async () => {
|
|
50
52
|
// With --force, skip the dependents check entirely and let git remove the checkout.
|
|
53
|
+
let dependentIds: string[] = [];
|
|
51
54
|
if (option !== "--force") {
|
|
52
55
|
const snapshot = await herdr.json(["api", "snapshot"], { cwd: ctx.cwd });
|
|
53
56
|
const panes = (snapshot.result as { snapshot?: { panes?: SnapshotPane[] } } | undefined)?.snapshot?.panes;
|
|
54
57
|
if (!Array.isArray(panes)) throw new Error("herdr api snapshot returned no panes.");
|
|
55
|
-
|
|
58
|
+
dependentIds = [...new Set(panes
|
|
56
59
|
.filter((pane): pane is SnapshotPane & { tab_id: string; cwd: string } =>
|
|
57
60
|
typeof pane.tab_id === "string" && pane.tab_id !== tabId && typeof pane.cwd === "string" &&
|
|
58
61
|
(pane.cwd === checkout || pane.cwd.startsWith(`${checkout}/`)))
|
|
59
62
|
.map((pane) => pane.tab_id))];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
}
|
|
64
|
+
const listing = await herdr.json(["tab", "list"], { cwd: ctx.cwd });
|
|
65
|
+
const tabs = (listing.result as { tabs?: TabEntry[] } | undefined)?.tabs;
|
|
66
|
+
if (!Array.isArray(tabs)) throw new Error("herdr tab list returned no tabs.");
|
|
67
|
+
const tabsById = new Map(tabs
|
|
68
|
+
.filter((tab): tab is TabEntry & { tab_id: string } => typeof tab.tab_id === "string")
|
|
69
|
+
.map((tab) => [tab.tab_id, tab]));
|
|
70
|
+
if (tabsById.get(tabId)?.workspace_id !== workspaceId) {
|
|
71
|
+
throw new Error(`Herdr tab ${tabId} does not belong to workspace ${workspaceId}.`);
|
|
72
|
+
}
|
|
73
|
+
siblingTabIds = tabs
|
|
74
|
+
.filter((tab): tab is TabEntry & { tab_id: string } =>
|
|
75
|
+
typeof tab.tab_id === "string" && tab.tab_id !== tabId && tab.workspace_id === workspaceId)
|
|
76
|
+
.map((tab) => tab.tab_id);
|
|
77
|
+
const blockers = dependentIds.filter((id) => tabsById.get(id)?.workspace_id !== workspaceId);
|
|
78
|
+
if (blockers.length > 0) {
|
|
79
|
+
const names = blockers.map((id) => {
|
|
80
|
+
const label = tabsById.get(id)?.label;
|
|
81
|
+
return typeof label === "string" ? label : id;
|
|
82
|
+
});
|
|
83
|
+
throw new Error(`Worktree still used by Herdr tabs ${names.join(", ")}; close them first.`);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
await execOrThrow("git", [
|
|
@@ -75,13 +88,14 @@ export default function herdrDoneExtension(pi: ExtensionAPI): void {
|
|
|
75
88
|
], ctx.cwd);
|
|
76
89
|
});
|
|
77
90
|
try {
|
|
91
|
+
await Promise.all(siblingTabIds.map((id) => herdr.run(["tab", "close", id], { cwd: tmpdir() })));
|
|
78
92
|
if (!parentIsBare && mainCheckout !== checkout) {
|
|
79
93
|
// Serialize concurrent completions pulling the same parent checkout.
|
|
80
94
|
// Run outside the removed checkout because its directory no longer exists.
|
|
81
95
|
await withWorktreeLock(mainCheckout, () => execOrThrow("git", ["pull", "--ff-only"], mainCheckout));
|
|
82
96
|
}
|
|
83
97
|
} finally {
|
|
84
|
-
// Close
|
|
98
|
+
// Close this session's tab last, even if sibling cleanup or the parent pull fails.
|
|
85
99
|
await herdr.run(["tab", "close", tabId], { cwd: tmpdir() });
|
|
86
100
|
}
|
|
87
101
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-herdr-done",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Close and remove the current Herdr worktree from Pi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@earendil-works/pi-coding-agent": "^0.84.2"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@henryqw/pi-herdr": "^0.
|
|
30
|
+
"@henryqw/pi-herdr": "^0.3.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|