@yoavsh/treekeeper 0.0.0-stage → 0.1.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +165 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/archive.d.ts +3 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +55 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +54 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/context.d.ts +34 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +53 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +81 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +119 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gc.d.ts +3 -0
- package/dist/commands/gc.d.ts.map +1 -0
- package/dist/commands/gc.js +98 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/status.d.ts +4 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +52 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/watch.d.ts +10 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +41 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/core/apply.d.ts +47 -0
- package/dist/core/apply.d.ts.map +1 -0
- package/dist/core/apply.js +223 -0
- package/dist/core/apply.js.map +1 -0
- package/dist/core/archive.d.ts +69 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +202 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/config.d.ts +69 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +261 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/decide.d.ts +102 -0
- package/dist/core/decide.d.ts.map +1 -0
- package/dist/core/decide.js +148 -0
- package/dist/core/decide.js.map +1 -0
- package/dist/core/forge.d.ts +39 -0
- package/dist/core/forge.d.ts.map +1 -0
- package/dist/core/forge.js +190 -0
- package/dist/core/forge.js.map +1 -0
- package/dist/core/git.d.ts +63 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +194 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/plan.d.ts +75 -0
- package/dist/core/plan.d.ts.map +1 -0
- package/dist/core/plan.js +288 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/render.d.ts +14 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +128 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/scan.d.ts +22 -0
- package/dist/core/scan.d.ts.map +1 -0
- package/dist/core/scan.js +99 -0
- package/dist/core/scan.js.map +1 -0
- package/dist/core/sessions.d.ts +37 -0
- package/dist/core/sessions.d.ts.map +1 -0
- package/dist/core/sessions.js +300 -0
- package/dist/core/sessions.js.map +1 -0
- package/dist/core/size.d.ts +3 -0
- package/dist/core/size.d.ts.map +1 -0
- package/dist/core/size.js +11 -0
- package/dist/core/size.js.map +1 -0
- package/dist/daemon/lock.d.ts +10 -0
- package/dist/daemon/lock.d.ts.map +1 -0
- package/dist/daemon/lock.js +64 -0
- package/dist/daemon/lock.js.map +1 -0
- package/dist/daemon/service.d.ts +34 -0
- package/dist/daemon/service.d.ts.map +1 -0
- package/dist/daemon/service.js +257 -0
- package/dist/daemon/service.js.map +1 -0
- package/dist/daemon/state.d.ts +19 -0
- package/dist/daemon/state.d.ts.map +1 -0
- package/dist/daemon/state.js +32 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/util/bytes.d.ts +4 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +14 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/color.d.ts +16 -0
- package/dist/util/color.d.ts.map +1 -0
- package/dist/util/color.js +32 -0
- package/dist/util/color.js.map +1 -0
- package/dist/util/exec.d.ts +27 -0
- package/dist/util/exec.d.ts.map +1 -0
- package/dist/util/exec.js +57 -0
- package/dist/util/exec.js.map +1 -0
- package/dist/util/log.d.ts +29 -0
- package/dist/util/log.d.ts.map +1 -0
- package/dist/util/log.js +72 -0
- package/dist/util/log.js.map +1 -0
- package/dist/util/paths.d.ts +18 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +87 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/util/prompt.d.ts +3 -0
- package/dist/util/prompt.d.ts.map +1 -0
- package/dist/util/prompt.js +15 -0
- package/dist/util/prompt.js.map +1 -0
- package/dist/util/redact.d.ts +15 -0
- package/dist/util/redact.d.ts.map +1 -0
- package/dist/util/redact.js +44 -0
- package/dist/util/redact.js.map +1 -0
- package/dist/util/time.d.ts +13 -0
- package/dist/util/time.d.ts.map +1 -0
- package/dist/util/time.js +77 -0
- package/dist/util/time.js.map +1 -0
- package/docs/integrations.md +128 -0
- package/docs/safety.md +62 -0
- package/package.json +69 -4
- package/src/cli.ts +175 -0
- package/src/commands/archive.ts +72 -0
- package/src/commands/config.ts +57 -0
- package/src/commands/context.ts +86 -0
- package/src/commands/daemon.ts +86 -0
- package/src/commands/doctor.ts +131 -0
- package/src/commands/gc.ts +108 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/watch.ts +48 -0
- package/src/core/apply.ts +276 -0
- package/src/core/archive.ts +269 -0
- package/src/core/config.ts +339 -0
- package/src/core/decide.ts +230 -0
- package/src/core/forge.ts +243 -0
- package/src/core/git.ts +254 -0
- package/src/core/plan.ts +431 -0
- package/src/core/render.ts +168 -0
- package/src/core/scan.ts +105 -0
- package/src/core/sessions.ts +339 -0
- package/src/core/size.ts +10 -0
- package/src/daemon/lock.ts +65 -0
- package/src/daemon/service.ts +321 -0
- package/src/daemon/state.ts +49 -0
- package/src/index.ts +36 -0
- package/src/util/bytes.ts +11 -0
- package/src/util/color.ts +33 -0
- package/src/util/exec.ts +80 -0
- package/src/util/log.ts +88 -0
- package/src/util/paths.ts +84 -0
- package/src/util/prompt.ts +13 -0
- package/src/util/redact.ts +43 -0
- package/src/util/time.ts +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `treekeeper gc` — plan and apply worktree and branch cleanup, dry run by default
|
|
8
|
+
- Detects finished work from git ancestry and from merged or closed pull requests (via `gh`), so squash merges are caught
|
|
9
|
+
- Holds back worktrees with live agent sessions (Claude Code, Codex, Aider), live processes, recent changes, or uncommitted work
|
|
10
|
+
- Archives unmerged commits, uncommitted diffs and untracked files before any removal, with a `RESTORE.md`
|
|
11
|
+
- `treekeeper daemon install` — launchd (macOS) and systemd user timers (Linux)
|
|
12
|
+
- `status`, `watch`, `archive`, `config` and `doctor` commands
|
|
13
|
+
- `--json` output for any harness to read
|
|
14
|
+
- Credential scrubbing for all output and cached data
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoav Shmariahu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,238 @@
|
|
|
1
|
-
#
|
|
1
|
+
# treekeeper
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/yoavshmariahu/treekeeper/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@yoavsh/treekeeper)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](package.json)
|
|
8
|
+
|
|
9
|
+
**Agents leave worktrees behind. treekeeper takes them away — once the work is actually finished, and never before.**
|
|
10
|
+
|
|
11
|
+
A coding agent that works in a git worktree per task is wonderful right up to the point where your disk is 40 abandoned checkouts deep, each with its own `node_modules`. The branches pile up too, until `git branch` is a wall of `agent/fix-the-thing-a3f91c`.
|
|
12
|
+
|
|
13
|
+
treekeeper watches your worktrees, your git history, your pull requests and your agent sessions, and reclaims the ones whose work is already merged. It archives anything unique first, so running it unattended is a reasonable thing to do.
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
$ treekeeper gc --all
|
|
17
|
+
thinker (main)
|
|
18
|
+
RECLAIM ~/src/thinker/.worktrees/sh-links [agent/sh-links] 59M PR #747 merged
|
|
19
|
+
keep ~/src/thinker/.worktrees/bedrock [agent/bedrock] claude-code activity 3m ago
|
|
20
|
+
keep ~/src/thinker/.worktrees/retry-wip [agent/retry-wip] PR #751 is still open
|
|
21
|
+
DELETE 156 merged branches: agent/approve-by-offer agent/astra-compression +154 more
|
|
22
|
+
|
|
23
|
+
would reclaim 1 worktree (59M) and delete 156 branches · 2 kept
|
|
24
|
+
re-run with --apply to do it
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @yoavsh/treekeeper # or: npx @yoavsh/treekeeper gc --all
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Needs Node 20+ and `git`. Install [`gh`](https://cli.github.com) too — without it treekeeper cannot see squash merges, which is how most agent branches land.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
treekeeper gc --all # dry run: what could go, and why
|
|
39
|
+
treekeeper gc --all --apply # do it (asks first)
|
|
40
|
+
treekeeper daemon install # ...or never think about it again
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`treekeeper gc` is always a dry run until you pass `--apply`. The daemon sweeps every 15 minutes in the background.
|
|
44
|
+
|
|
45
|
+
## What counts as "finished"
|
|
46
|
+
|
|
47
|
+
This is the whole design, so it is worth being precise. A worktree is removable only when **one** of these is true:
|
|
48
|
+
|
|
49
|
+
| Signal | How it is detected | Default |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Branch is merged into the default branch | `git merge-base --is-ancestor` | reclaim |
|
|
52
|
+
| Pull request was **merged** (incl. squash/rebase) | `gh pr list`, head SHA matched to the local tip | reclaim |
|
|
53
|
+
| Pull request was **closed** without merging | `gh pr list` | reclaim after a 7-day grace, commits archived first |
|
|
54
|
+
| PR merged, but the local branch has extra commits | PR head SHA ≠ local tip | **keep** (opt in with `onExtraCommits`) |
|
|
55
|
+
| Pull request still open | `gh pr list` | keep |
|
|
56
|
+
| Nothing says the work landed | — | keep |
|
|
57
|
+
|
|
58
|
+
**treekeeper never removes a worktree whose commits are not either on the default branch or attached to a merged or closed pull request.** `--force` does not relax that. It is the one invariant everything else is built on.
|
|
59
|
+
|
|
60
|
+
Then, even for finished work, it holds back if anyone is still there:
|
|
61
|
+
|
|
62
|
+
- an agent session touched the worktree in the last 20 minutes (`sessions.grace`)
|
|
63
|
+
- a running process has its working directory inside it
|
|
64
|
+
- the worktree was touched more recently than `minAge` (30 minutes)
|
|
65
|
+
- it is your current directory, the repo's main worktree, `git worktree lock`ed, on a protected branch, or matched by `protectPaths`
|
|
66
|
+
|
|
67
|
+
Uncommitted changes do **not** save a worktree whose work is merged — they get archived and the worktree goes. Set `onDirty: "keep"` if you would rather it stayed.
|
|
68
|
+
|
|
69
|
+
## Nothing is unrecoverable
|
|
70
|
+
|
|
71
|
+
Before anything is removed, treekeeper writes an archive to `~/.treekeeper/archive/<repo>/<branch>-<timestamp>/`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
commits.bundle git bundle of commits not on the default branch
|
|
75
|
+
uncommitted.patch git diff HEAD --binary
|
|
76
|
+
untracked.tar.gz untracked, non-ignored files
|
|
77
|
+
meta.json what was removed, when, and why
|
|
78
|
+
RESTORE.md the exact commands to bring it back
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A cleanly merged worktree has nothing unique in it, so it only gets `meta.json` — an audit record. Anything with real work in it gets the full set.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
treekeeper archive list # what has been removed lately
|
|
85
|
+
treekeeper archive prune # drop archives past the retention window (14d)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## It works with any agent harness
|
|
89
|
+
|
|
90
|
+
treekeeper decides from **state, not hooks**: git, your forge, and whatever the agents left on disk. So it works with harnesses it has never heard of, and it keeps working when a harness changes its hook API.
|
|
91
|
+
|
|
92
|
+
**1. Run the daemon and forget it.** Nothing to integrate.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
treekeeper daemon install # launchd on macOS, systemd --user on Linux
|
|
96
|
+
treekeeper daemon status
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**2. Or call it from a harness hook** — any harness that can run a shell command:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx -y treekeeper gc --all --apply --yes --quiet
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**3. Or read the plan as JSON** and decide for yourself:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
treekeeper gc --all --json | jq '.summary'
|
|
109
|
+
treekeeper gc --all --json | jq -r '.repos[].worktrees[] | select(.verdict.action=="reclaim") | .path'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [docs/integrations.md](docs/integrations.md) for ready-made snippets (Claude Code, Codex CLI, Cursor, Aider, CI).
|
|
113
|
+
|
|
114
|
+
### Knowing when an agent is still working
|
|
115
|
+
|
|
116
|
+
| Detector | Signal |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `claude-code` | transcript mtime in `~/.claude/projects/<slug>/` |
|
|
119
|
+
| `codex` | rollout transcript in `~/.codex/sessions/` whose `cwd` is the worktree |
|
|
120
|
+
| `aider` | `.aider.chat.history.md` in the worktree |
|
|
121
|
+
| `process` | `lsof` — a live process whose cwd is inside the worktree |
|
|
122
|
+
| `mtime` | any recently modified file in the worktree |
|
|
123
|
+
|
|
124
|
+
The last two are the catch-all that covers every other harness: an agent that is working is a process in a directory, writing files. Add your own with a shell command:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"sessions": {
|
|
129
|
+
"commands": [
|
|
130
|
+
{ "name": "my-harness", "command": "pgrep -f \"my-agent --cwd $TREEKEEPER_WORKTREE\"" }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Exit code 0 means "still working".
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
`~/.treekeeper/config.json` (run `treekeeper config init` for a starter, `treekeeper config show` for the effective values). JSON with `//` comments and trailing commas allowed.
|
|
141
|
+
|
|
142
|
+
```jsonc
|
|
143
|
+
{
|
|
144
|
+
"roots": ["~/src"], // where to look for repos
|
|
145
|
+
"maxDepth": 3,
|
|
146
|
+
"minAge": "30m", // untouched this long before it can go
|
|
147
|
+
"closedPrGrace": "7d", // extra hold for closed-unmerged PRs
|
|
148
|
+
"onDirty": "archive", // archive | keep
|
|
149
|
+
"onClosedPr": "archive", // archive | keep
|
|
150
|
+
"onExtraCommits": "keep", // archive | keep
|
|
151
|
+
"protectBranches": ["main", "master", "trunk", "develop", "dev", "release/*"],
|
|
152
|
+
"protectPaths": [], // paths or globs that are never touched
|
|
153
|
+
"pruneBranches": true, // also delete merged branches with no worktree
|
|
154
|
+
"pruneRemotes": false, // git remote prune origin after a sweep
|
|
155
|
+
"forge": "github", // github | none (none = git ancestry only)
|
|
156
|
+
"forgeCacheTtl": "10m",
|
|
157
|
+
"interval": "15m", // daemon sweep interval
|
|
158
|
+
"sessions": {
|
|
159
|
+
"grace": "20m",
|
|
160
|
+
"detectors": ["claude-code", "codex", "aider", "process", "mtime"],
|
|
161
|
+
"extraPaths": [], // mtime of these counts as activity; {worktree} is substituted
|
|
162
|
+
"commands": []
|
|
163
|
+
},
|
|
164
|
+
"archive": {
|
|
165
|
+
"enabled": true,
|
|
166
|
+
"dir": "~/.treekeeper/archive",
|
|
167
|
+
"retention": "14d",
|
|
168
|
+
"maxUntrackedBytes": 67108864
|
|
169
|
+
},
|
|
170
|
+
"logFile": "~/.treekeeper/treekeeper.log",
|
|
171
|
+
"logLevel": "info"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A repo can override any of it, or opt out entirely, with its own `.treekeeper.json`:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{ "enabled": false }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Commands
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
treekeeper gc [path...] Plan a sweep (default command). --apply to carry it out.
|
|
185
|
+
treekeeper status What is reclaimable right now, plus daemon state.
|
|
186
|
+
treekeeper watch Sweep on an interval in the foreground.
|
|
187
|
+
treekeeper daemon <sub> install | uninstall | start | stop | status | logs
|
|
188
|
+
treekeeper archive <sub> list | prune | path
|
|
189
|
+
treekeeper config <sub> show | path | init
|
|
190
|
+
treekeeper doctor Check git, gh, lsof, config and detectors.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Useful flags: `--all`, `--apply`, `-y/--yes`, `--json`, `-v/--verbose` (show what was kept and why), `--force`, `--min-age 2h`, `--branches-only`, `--worktrees-only`, `--forge none`, `--refresh`, `--root <dir>`.
|
|
194
|
+
|
|
195
|
+
Every removal is appended as JSON to `~/.treekeeper/treekeeper.log`, so there is always an answer to "what happened to that directory".
|
|
196
|
+
|
|
197
|
+
## Why not just `git worktree prune`?
|
|
198
|
+
|
|
199
|
+
`git worktree prune` only clears records for directories you already deleted — it never frees a byte. `git branch --merged` misses squash merges, which is how most agent branches land, and neither knows whether an agent is still working in a checkout. treekeeper answers the question those commands cannot: *is this work done, and is anyone still in there?*
|
|
200
|
+
|
|
201
|
+
## Programmatic use
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { loadConfig, createForge, discoverRepos, buildPlan, applyPlan, Logger } from "@yoavsh/treekeeper";
|
|
205
|
+
|
|
206
|
+
const { config } = await loadConfig();
|
|
207
|
+
const logger = new Logger({ level: "info" });
|
|
208
|
+
const repos = await discoverRepos({ roots: config.roots, maxDepth: config.maxDepth });
|
|
209
|
+
const forge = createForge({ kind: config.forge, cacheTtl: config.forgeCacheTtl, logger });
|
|
210
|
+
|
|
211
|
+
const plan = await buildPlan({ config, repos, forge, logger });
|
|
212
|
+
if (plan.summary.worktreesReclaimable > 0) {
|
|
213
|
+
await applyPlan({ plan, config, logger });
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`decideWorktree(facts, policy)` is a pure function if you only want the ruleset.
|
|
218
|
+
|
|
219
|
+
## Safety notes
|
|
220
|
+
|
|
221
|
+
- One sweep at a time per machine, enforced by a lock file, so a scheduled sweep and a manual one cannot race.
|
|
222
|
+
- Every candidate is re-verified immediately before removal; if the branch, HEAD or contents moved since the plan was built, it is left alone.
|
|
223
|
+
- Credentials are scrubbed from all output — remotes with embedded tokens are common, and the log is persistent.
|
|
224
|
+
- Removals go through `git worktree remove`, never a blind `rm -rf`.
|
|
225
|
+
|
|
226
|
+
The full threat model, the archive format and the recovery procedure are in
|
|
227
|
+
[docs/safety.md](docs/safety.md).
|
|
228
|
+
|
|
229
|
+
## Contributing
|
|
230
|
+
|
|
231
|
+
Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
232
|
+
Security problems go through [private reporting](SECURITY.md), not a public issue.
|
|
233
|
+
`npm test` runs the suite against real git repositories, so it needs `git` on
|
|
234
|
+
your PATH and takes about 30 seconds.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
[MIT](LICENSE) © Yoav Shmariahu
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA0DA,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1D"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { archiveCommand } from "./commands/archive.js";
|
|
5
|
+
import { configCommand } from "./commands/config.js";
|
|
6
|
+
import { daemonCommand } from "./commands/daemon.js";
|
|
7
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
8
|
+
import { gcCommand } from "./commands/gc.js";
|
|
9
|
+
import { statusCommand } from "./commands/status.js";
|
|
10
|
+
import { watchCommand } from "./commands/watch.js";
|
|
11
|
+
import { color } from "./util/color.js";
|
|
12
|
+
const HELP = `${color.bold("treekeeper")} — reclaim disk from finished agent worktrees
|
|
13
|
+
|
|
14
|
+
${color.bold("USAGE")}
|
|
15
|
+
treekeeper <command> [options] [path...]
|
|
16
|
+
|
|
17
|
+
${color.bold("COMMANDS")}
|
|
18
|
+
gc [path...] Plan a sweep (default). Add --apply to carry it out.
|
|
19
|
+
status What is reclaimable right now, plus daemon state.
|
|
20
|
+
watch Sweep on an interval in the foreground.
|
|
21
|
+
daemon <sub> install | uninstall | start | stop | status | logs
|
|
22
|
+
archive <sub> list | prune | path
|
|
23
|
+
config <sub> show | path | init
|
|
24
|
+
doctor Check git, gh, lsof, config and detectors.
|
|
25
|
+
|
|
26
|
+
${color.bold("OPTIONS")}
|
|
27
|
+
--apply Actually remove things (default is a dry run)
|
|
28
|
+
-y, --yes Skip the confirmation prompt
|
|
29
|
+
--all Every repo under the configured roots
|
|
30
|
+
--json Machine-readable output on stdout
|
|
31
|
+
-q, --quiet Summary only
|
|
32
|
+
-v, --verbose Show kept worktrees and debug logging
|
|
33
|
+
--force Bypass the age gate and the dirty / closed-PR holds
|
|
34
|
+
--min-age <dur> Override minAge, e.g. 2h
|
|
35
|
+
--session-grace <d> Override how long after agent activity a worktree is held
|
|
36
|
+
--forge <kind> github | none (none = git ancestry only)
|
|
37
|
+
--refresh Ignore the cached pull-request data
|
|
38
|
+
--branches-only Prune branches, leave every worktree alone
|
|
39
|
+
--worktrees-only Remove worktrees, do not prune other branches
|
|
40
|
+
--sizes Measure every worktree, not just the removable ones
|
|
41
|
+
--root <dir> Override roots (repeatable)
|
|
42
|
+
--interval <dur> Interval for watch / daemon install
|
|
43
|
+
--config <file> Use a specific config file
|
|
44
|
+
-h, --help This text
|
|
45
|
+
--version Version
|
|
46
|
+
|
|
47
|
+
${color.bold("EXAMPLES")}
|
|
48
|
+
treekeeper gc --all # what could go, across every repo
|
|
49
|
+
treekeeper gc --all --apply --yes # the sweep a harness hook should run
|
|
50
|
+
treekeeper gc --json | jq .summary # read the plan from a script
|
|
51
|
+
treekeeper daemon install # sweep on a schedule, forever
|
|
52
|
+
|
|
53
|
+
${color.gray("A worktree is only ever removed when its work is merged, or its pull request")}
|
|
54
|
+
${color.gray("was merged or closed — and anything unique is archived before it goes.")}
|
|
55
|
+
`;
|
|
56
|
+
export async function main(argv) {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = parseArgs({
|
|
60
|
+
args: argv,
|
|
61
|
+
allowPositionals: true,
|
|
62
|
+
strict: true,
|
|
63
|
+
options: {
|
|
64
|
+
apply: { type: "boolean" },
|
|
65
|
+
yes: { type: "boolean", short: "y" },
|
|
66
|
+
all: { type: "boolean" },
|
|
67
|
+
json: { type: "boolean" },
|
|
68
|
+
quiet: { type: "boolean", short: "q" },
|
|
69
|
+
verbose: { type: "boolean", short: "v" },
|
|
70
|
+
force: { type: "boolean" },
|
|
71
|
+
refresh: { type: "boolean" },
|
|
72
|
+
sizes: { type: "boolean" },
|
|
73
|
+
"branches-only": { type: "boolean" },
|
|
74
|
+
"worktrees-only": { type: "boolean" },
|
|
75
|
+
"min-age": { type: "string" },
|
|
76
|
+
"session-grace": { type: "string" },
|
|
77
|
+
forge: { type: "string" },
|
|
78
|
+
root: { type: "string", multiple: true },
|
|
79
|
+
interval: { type: "string" },
|
|
80
|
+
config: { type: "string" },
|
|
81
|
+
help: { type: "boolean", short: "h" },
|
|
82
|
+
version: { type: "boolean" },
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
process.stderr.write(`${err.message}\nTry \`treekeeper --help\`.\n`);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
const { values, positionals } = parsed;
|
|
91
|
+
if (values.version) {
|
|
92
|
+
process.stdout.write((await version()) + "\n");
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
const [command = "gc", ...rest] = positionals;
|
|
96
|
+
if (values.help) {
|
|
97
|
+
process.stdout.write(HELP);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const flags = {
|
|
101
|
+
apply: values.apply,
|
|
102
|
+
yes: values.yes,
|
|
103
|
+
json: values.json,
|
|
104
|
+
quiet: values.quiet,
|
|
105
|
+
verbose: values.verbose,
|
|
106
|
+
all: values.all,
|
|
107
|
+
force: values.force,
|
|
108
|
+
refresh: values.refresh,
|
|
109
|
+
sizes: values.sizes,
|
|
110
|
+
branchesOnly: values["branches-only"],
|
|
111
|
+
worktreesOnly: values["worktrees-only"],
|
|
112
|
+
minAge: values["min-age"],
|
|
113
|
+
sessionGrace: values["session-grace"],
|
|
114
|
+
forge: values.forge,
|
|
115
|
+
interval: values.interval,
|
|
116
|
+
config: values.config,
|
|
117
|
+
roots: values.root,
|
|
118
|
+
};
|
|
119
|
+
switch (command) {
|
|
120
|
+
case "gc":
|
|
121
|
+
case "clean":
|
|
122
|
+
return gcCommand(rest, flags);
|
|
123
|
+
case "status":
|
|
124
|
+
return statusCommand(flags);
|
|
125
|
+
case "watch":
|
|
126
|
+
return watchCommand(flags);
|
|
127
|
+
case "daemon":
|
|
128
|
+
return daemonCommand(rest, flags);
|
|
129
|
+
case "archive":
|
|
130
|
+
return archiveCommand(rest, flags);
|
|
131
|
+
case "config":
|
|
132
|
+
return configCommand(rest);
|
|
133
|
+
case "doctor":
|
|
134
|
+
return doctorCommand(values.json === true);
|
|
135
|
+
case "help":
|
|
136
|
+
process.stdout.write(HELP);
|
|
137
|
+
return 0;
|
|
138
|
+
default:
|
|
139
|
+
process.stderr.write(`unknown command "${command}". Try \`treekeeper --help\`.\n`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function version() {
|
|
144
|
+
try {
|
|
145
|
+
const text = await readFile(new URL("../package.json", import.meta.url), "utf8");
|
|
146
|
+
const pkg = JSON.parse(text);
|
|
147
|
+
return String(pkg.version ?? "0.0.0");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return "0.0.0";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const isDirectRun = process.argv[1] !== undefined &&
|
|
154
|
+
(import.meta.url.endsWith("/cli.js") || import.meta.url.endsWith("/cli.ts"));
|
|
155
|
+
if (isDirectRun) {
|
|
156
|
+
main(process.argv.slice(2))
|
|
157
|
+
.then((code) => {
|
|
158
|
+
process.exitCode = code;
|
|
159
|
+
})
|
|
160
|
+
.catch((err) => {
|
|
161
|
+
process.stderr.write(`treekeeper: ${err.stack ?? String(err)}\n`);
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/commands/archive.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAI7C,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CA4DrF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { listArchives, pruneArchives } from "../core/archive.js";
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { dirSizeKb } from "../core/size.js";
|
|
4
|
+
import { formatBytes, formatKb } from "../util/bytes.js";
|
|
5
|
+
import { color } from "../util/color.js";
|
|
6
|
+
import { shortenPath } from "../util/paths.js";
|
|
7
|
+
import { formatAgo, parseDuration } from "../util/time.js";
|
|
8
|
+
const USAGE = `usage: treekeeper archive <list|prune|path>`;
|
|
9
|
+
export async function archiveCommand(args, flags) {
|
|
10
|
+
const { config } = await loadConfig();
|
|
11
|
+
const sub = args[0] ?? "list";
|
|
12
|
+
if (sub === "path") {
|
|
13
|
+
process.stdout.write(config.archive.dir + "\n");
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (sub === "prune") {
|
|
17
|
+
const retention = parseDuration(config.archive.retention);
|
|
18
|
+
const result = await pruneArchives(config.archive.dir, retention);
|
|
19
|
+
process.stdout.write(result.removed.length === 0
|
|
20
|
+
? `nothing older than ${config.archive.retention}\n`
|
|
21
|
+
: `pruned ${result.removed.length} archive(s), freed ${formatKb(result.freedKb)}\n`);
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
if (sub !== "list") {
|
|
25
|
+
process.stderr.write(USAGE + "\n");
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const records = await listArchives(config.archive.dir);
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
process.stdout.write(JSON.stringify(records, null, 2) + "\n");
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
if (records.length === 0) {
|
|
34
|
+
process.stdout.write(`no archives in ${shortenPath(config.archive.dir)}\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const total = (await dirSizeKb(config.archive.dir)) ?? 0;
|
|
38
|
+
process.stdout.write(`${color.bold(String(records.length))} archive(s) in ${shortenPath(config.archive.dir)} ` +
|
|
39
|
+
color.gray(`(${formatKb(total)}, retention ${config.archive.retention})\n\n`));
|
|
40
|
+
for (const r of records) {
|
|
41
|
+
const bits = [
|
|
42
|
+
r.bundle ? `${r.bundle.commits} unmerged commit${r.bundle.commits === 1 ? "" : "s"}` : null,
|
|
43
|
+
r.patch ? `diff ${formatBytes(r.patch.bytes)}` : null,
|
|
44
|
+
r.untracked
|
|
45
|
+
? `${r.untracked.count} untracked${r.untracked.truncated ? " (list only)" : ""}`
|
|
46
|
+
: null,
|
|
47
|
+
].filter(Boolean);
|
|
48
|
+
process.stdout.write(`${color.cyan(r.id)} ${color.gray(formatAgo(Date.parse(r.createdAt)))}\n` +
|
|
49
|
+
` ${color.gray(r.reason)}${bits.length > 0 ? ` ${color.yellow(bits.join(", "))}` : color.gray(" (nothing unique)")}\n` +
|
|
50
|
+
` ${color.gray(shortenPath(r.dir))}\n`);
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(color.gray("\neach archive has a RESTORE.md with the exact commands to bring it back\n"));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../../src/commands/archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAQA,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDnE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { defaultConfig, globalConfigPath, loadConfig } from "../core/config.js";
|
|
4
|
+
import { color } from "../util/color.js";
|
|
5
|
+
import { shortenPath } from "../util/paths.js";
|
|
6
|
+
const USAGE = `usage: treekeeper config <show|path|init>`;
|
|
7
|
+
export async function configCommand(args) {
|
|
8
|
+
const sub = args[0] ?? "show";
|
|
9
|
+
if (sub === "path") {
|
|
10
|
+
process.stdout.write(globalConfigPath() + "\n");
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
if (sub === "show") {
|
|
14
|
+
const { config, sources, warnings } = await loadConfig();
|
|
15
|
+
process.stdout.write(JSON.stringify(config, null, 2) + "\n");
|
|
16
|
+
if (sources.length === 0) {
|
|
17
|
+
process.stderr.write(color.gray(`(defaults — no file at ${shortenPath(globalConfigPath())})\n`));
|
|
18
|
+
}
|
|
19
|
+
for (const w of warnings)
|
|
20
|
+
process.stderr.write(color.yellow(`warn ${w}\n`));
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (sub === "init") {
|
|
24
|
+
const path = globalConfigPath();
|
|
25
|
+
const starter = {
|
|
26
|
+
$schema: "https://github.com/yoavshmariahu/treekeeper",
|
|
27
|
+
roots: defaultConfig().roots,
|
|
28
|
+
minAge: "30m",
|
|
29
|
+
interval: "15m",
|
|
30
|
+
onDirty: "archive",
|
|
31
|
+
onClosedPr: "archive",
|
|
32
|
+
protectBranches: defaultConfig().protectBranches,
|
|
33
|
+
protectPaths: [],
|
|
34
|
+
sessions: { grace: "20m" },
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
await mkdir(dirname(path), { recursive: true });
|
|
38
|
+
await writeFile(path, JSON.stringify(starter, null, 2) + "\n", { flag: "wx" });
|
|
39
|
+
process.stdout.write(`wrote ${shortenPath(path)}\n`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err.code === "EEXIST") {
|
|
44
|
+
process.stderr.write(`${shortenPath(path)} already exists\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
process.stderr.write(`could not write ${path}: ${err.message}\n`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write(USAGE + "\n");
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Config } from "../core/config.js";
|
|
2
|
+
import type { Forge } from "../core/forge.js";
|
|
3
|
+
import { Logger } from "../util/log.js";
|
|
4
|
+
export interface CliFlags {
|
|
5
|
+
apply?: boolean;
|
|
6
|
+
yes?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
all?: boolean;
|
|
11
|
+
force?: boolean;
|
|
12
|
+
minAge?: string;
|
|
13
|
+
sessionGrace?: string;
|
|
14
|
+
forge?: string;
|
|
15
|
+
refresh?: boolean;
|
|
16
|
+
branchesOnly?: boolean;
|
|
17
|
+
worktreesOnly?: boolean;
|
|
18
|
+
sizes?: boolean;
|
|
19
|
+
interval?: string;
|
|
20
|
+
config?: string;
|
|
21
|
+
roots?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface Context {
|
|
24
|
+
config: Config;
|
|
25
|
+
logger: Logger;
|
|
26
|
+
forge: Forge;
|
|
27
|
+
warnings: string[];
|
|
28
|
+
configSources: string[];
|
|
29
|
+
}
|
|
30
|
+
/** Load config, apply CLI overrides, and build the shared logger and forge. */
|
|
31
|
+
export declare function createContext(flags: CliFlags): Promise<Context>;
|
|
32
|
+
/** Which repos a command should operate on, from positionals, `--all`, or cwd. */
|
|
33
|
+
export declare function resolveRepos(ctx: Context, positionals: string[], flags: CliFlags): Promise<string[]>;
|
|
34
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,+EAA+E;AAC/E,wBAAsB,aAAa,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CA2BrE;AAED,kFAAkF;AAClF,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,QAAQ,GACd,OAAO,CAAC,MAAM,EAAE,CAAC,CAcnB"}
|