@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +237 -2
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +165 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/archive.d.ts +3 -0
  9. package/dist/commands/archive.d.ts.map +1 -0
  10. package/dist/commands/archive.js +55 -0
  11. package/dist/commands/archive.js.map +1 -0
  12. package/dist/commands/config.d.ts +2 -0
  13. package/dist/commands/config.d.ts.map +1 -0
  14. package/dist/commands/config.js +54 -0
  15. package/dist/commands/config.js.map +1 -0
  16. package/dist/commands/context.d.ts +34 -0
  17. package/dist/commands/context.d.ts.map +1 -0
  18. package/dist/commands/context.js +53 -0
  19. package/dist/commands/context.js.map +1 -0
  20. package/dist/commands/daemon.d.ts +3 -0
  21. package/dist/commands/daemon.d.ts.map +1 -0
  22. package/dist/commands/daemon.js +81 -0
  23. package/dist/commands/daemon.js.map +1 -0
  24. package/dist/commands/doctor.d.ts +3 -0
  25. package/dist/commands/doctor.d.ts.map +1 -0
  26. package/dist/commands/doctor.js +119 -0
  27. package/dist/commands/doctor.js.map +1 -0
  28. package/dist/commands/gc.d.ts +3 -0
  29. package/dist/commands/gc.d.ts.map +1 -0
  30. package/dist/commands/gc.js +98 -0
  31. package/dist/commands/gc.js.map +1 -0
  32. package/dist/commands/status.d.ts +4 -0
  33. package/dist/commands/status.d.ts.map +1 -0
  34. package/dist/commands/status.js +52 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/watch.d.ts +10 -0
  37. package/dist/commands/watch.d.ts.map +1 -0
  38. package/dist/commands/watch.js +41 -0
  39. package/dist/commands/watch.js.map +1 -0
  40. package/dist/core/apply.d.ts +47 -0
  41. package/dist/core/apply.d.ts.map +1 -0
  42. package/dist/core/apply.js +223 -0
  43. package/dist/core/apply.js.map +1 -0
  44. package/dist/core/archive.d.ts +69 -0
  45. package/dist/core/archive.d.ts.map +1 -0
  46. package/dist/core/archive.js +202 -0
  47. package/dist/core/archive.js.map +1 -0
  48. package/dist/core/config.d.ts +69 -0
  49. package/dist/core/config.d.ts.map +1 -0
  50. package/dist/core/config.js +261 -0
  51. package/dist/core/config.js.map +1 -0
  52. package/dist/core/decide.d.ts +102 -0
  53. package/dist/core/decide.d.ts.map +1 -0
  54. package/dist/core/decide.js +148 -0
  55. package/dist/core/decide.js.map +1 -0
  56. package/dist/core/forge.d.ts +39 -0
  57. package/dist/core/forge.d.ts.map +1 -0
  58. package/dist/core/forge.js +190 -0
  59. package/dist/core/forge.js.map +1 -0
  60. package/dist/core/git.d.ts +63 -0
  61. package/dist/core/git.d.ts.map +1 -0
  62. package/dist/core/git.js +194 -0
  63. package/dist/core/git.js.map +1 -0
  64. package/dist/core/plan.d.ts +75 -0
  65. package/dist/core/plan.d.ts.map +1 -0
  66. package/dist/core/plan.js +288 -0
  67. package/dist/core/plan.js.map +1 -0
  68. package/dist/core/render.d.ts +14 -0
  69. package/dist/core/render.d.ts.map +1 -0
  70. package/dist/core/render.js +128 -0
  71. package/dist/core/render.js.map +1 -0
  72. package/dist/core/scan.d.ts +22 -0
  73. package/dist/core/scan.d.ts.map +1 -0
  74. package/dist/core/scan.js +99 -0
  75. package/dist/core/scan.js.map +1 -0
  76. package/dist/core/sessions.d.ts +37 -0
  77. package/dist/core/sessions.d.ts.map +1 -0
  78. package/dist/core/sessions.js +300 -0
  79. package/dist/core/sessions.js.map +1 -0
  80. package/dist/core/size.d.ts +3 -0
  81. package/dist/core/size.d.ts.map +1 -0
  82. package/dist/core/size.js +11 -0
  83. package/dist/core/size.js.map +1 -0
  84. package/dist/daemon/lock.d.ts +10 -0
  85. package/dist/daemon/lock.d.ts.map +1 -0
  86. package/dist/daemon/lock.js +64 -0
  87. package/dist/daemon/lock.js.map +1 -0
  88. package/dist/daemon/service.d.ts +34 -0
  89. package/dist/daemon/service.d.ts.map +1 -0
  90. package/dist/daemon/service.js +257 -0
  91. package/dist/daemon/service.js.map +1 -0
  92. package/dist/daemon/state.d.ts +19 -0
  93. package/dist/daemon/state.d.ts.map +1 -0
  94. package/dist/daemon/state.js +32 -0
  95. package/dist/daemon/state.js.map +1 -0
  96. package/dist/index.d.ts +26 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +19 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/util/bytes.d.ts +4 -0
  101. package/dist/util/bytes.d.ts.map +1 -0
  102. package/dist/util/bytes.js +14 -0
  103. package/dist/util/bytes.js.map +1 -0
  104. package/dist/util/color.d.ts +16 -0
  105. package/dist/util/color.d.ts.map +1 -0
  106. package/dist/util/color.js +32 -0
  107. package/dist/util/color.js.map +1 -0
  108. package/dist/util/exec.d.ts +27 -0
  109. package/dist/util/exec.d.ts.map +1 -0
  110. package/dist/util/exec.js +57 -0
  111. package/dist/util/exec.js.map +1 -0
  112. package/dist/util/log.d.ts +29 -0
  113. package/dist/util/log.d.ts.map +1 -0
  114. package/dist/util/log.js +72 -0
  115. package/dist/util/log.js.map +1 -0
  116. package/dist/util/paths.d.ts +18 -0
  117. package/dist/util/paths.d.ts.map +1 -0
  118. package/dist/util/paths.js +87 -0
  119. package/dist/util/paths.js.map +1 -0
  120. package/dist/util/prompt.d.ts +3 -0
  121. package/dist/util/prompt.d.ts.map +1 -0
  122. package/dist/util/prompt.js +15 -0
  123. package/dist/util/prompt.js.map +1 -0
  124. package/dist/util/redact.d.ts +15 -0
  125. package/dist/util/redact.d.ts.map +1 -0
  126. package/dist/util/redact.js +44 -0
  127. package/dist/util/redact.js.map +1 -0
  128. package/dist/util/time.d.ts +13 -0
  129. package/dist/util/time.d.ts.map +1 -0
  130. package/dist/util/time.js +77 -0
  131. package/dist/util/time.js.map +1 -0
  132. package/docs/integrations.md +128 -0
  133. package/docs/safety.md +62 -0
  134. package/package.json +69 -4
  135. package/src/cli.ts +175 -0
  136. package/src/commands/archive.ts +72 -0
  137. package/src/commands/config.ts +57 -0
  138. package/src/commands/context.ts +86 -0
  139. package/src/commands/daemon.ts +86 -0
  140. package/src/commands/doctor.ts +131 -0
  141. package/src/commands/gc.ts +108 -0
  142. package/src/commands/status.ts +68 -0
  143. package/src/commands/watch.ts +48 -0
  144. package/src/core/apply.ts +276 -0
  145. package/src/core/archive.ts +269 -0
  146. package/src/core/config.ts +339 -0
  147. package/src/core/decide.ts +230 -0
  148. package/src/core/forge.ts +243 -0
  149. package/src/core/git.ts +254 -0
  150. package/src/core/plan.ts +431 -0
  151. package/src/core/render.ts +168 -0
  152. package/src/core/scan.ts +105 -0
  153. package/src/core/sessions.ts +339 -0
  154. package/src/core/size.ts +10 -0
  155. package/src/daemon/lock.ts +65 -0
  156. package/src/daemon/service.ts +321 -0
  157. package/src/daemon/state.ts +49 -0
  158. package/src/index.ts +36 -0
  159. package/src/util/bytes.ts +11 -0
  160. package/src/util/color.ts +33 -0
  161. package/src/util/exec.ts +80 -0
  162. package/src/util/log.ts +88 -0
  163. package/src/util/paths.ts +84 -0
  164. package/src/util/prompt.ts +13 -0
  165. package/src/util/redact.ts +43 -0
  166. 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
- # Temporary Holding Version
1
+ # treekeeper
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![ci](https://github.com/yoavshmariahu/treekeeper/actions/workflows/ci.yml/badge.svg)](https://github.com/yoavshmariahu/treekeeper/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40yoavsh%2Ftreekeeper?color=0b7)](https://www.npmjs.com/package/@yoavsh/treekeeper)
5
+ [![node](https://img.shields.io/node/v/%40yoavsh%2Ftreekeeper)](https://nodejs.org)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![dependencies](https://img.shields.io/badge/dependencies-none-0b7)](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,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): Promise<number>;
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC;;EAEtC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;;;EAGnB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;;;;;;;;;EAStB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;EAqBrB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;;;;;;EAMtB,KAAK,CAAC,IAAI,CAAC,8EAA8E,CAAC;EAC1F,KAAK,CAAC,IAAI,CAAC,wEAAwE,CAAC;CACrF,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;YACjB,IAAI,EAAE,IAAI;YACV,gBAAgB,EAAE,IAAI;YACtB,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE;gBACP,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC1B,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gBACpC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBACxB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBACzB,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gBACtC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gBACxC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC1B,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC5B,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBAC1B,eAAe,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBACpC,gBAAgB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;gBACrC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC7B,eAAe,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACnC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACzB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACxC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC5B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gBACrC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;aAC7B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAI,GAAa,CAAC,OAAO,gCAAgC,CAAC,CAAC;QAChF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;IAEvC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,CAAC,OAAO,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC;IAE9C,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAa;QACtB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,YAAY,EAAE,MAAM,CAAC,eAAe,CAAC;QACrC,aAAa,EAAE,MAAM,CAAC,gBAAgB,CAAC;QACvC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;QACzB,YAAY,EAAE,MAAM,CAAC,eAAe,CAAC;QACrC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,IAAI;KACnB,CAAC;IAEF,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,IAAI,CAAC;QACV,KAAK,OAAO;YACV,OAAO,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAChC,KAAK,QAAQ;YACX,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9B,KAAK,OAAO;YACV,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACpC,KAAK,SAAS;YACZ,OAAO,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,KAAK,QAAQ;YACX,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;QAC7B,KAAK,QAAQ;YACX,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAC7C,KAAK,MAAM;YACT,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3B,OAAO,CAAC,CAAC;QACX;YACE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,OAAO,iCAAiC,CAAC,CAAC;YACnF,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED,KAAK,UAAU,OAAO;IACpB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QACjF,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,MAAM,CAAE,GAA6B,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,WAAW,GACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;IAC7B,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AAE/E,IAAI,WAAW,EAAE,CAAC;IAChB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SACxB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAgB,GAAa,CAAC,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7E,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from "./context.js";
2
+ export declare function archiveCommand(args: string[], flags: CliFlags): Promise<number>;
3
+ //# sourceMappingURL=archive.d.ts.map
@@ -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,2 @@
1
+ export declare function configCommand(args: string[]): Promise<number>;
2
+ //# sourceMappingURL=config.d.ts.map
@@ -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"}