@osdlc/repo-cleaner 0.0.0-stage → 0.5.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/CHANGELOG.md +191 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +301 -2
- package/SECURITY.md +21 -0
- package/bin/repo-cleaner.js +8 -0
- package/dist/apply.d.ts +62 -0
- package/dist/apply.js +316 -0
- package/dist/branches.d.ts +61 -0
- package/dist/branches.js +303 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +298 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +67 -0
- package/dist/discover.d.ts +32 -0
- package/dist/discover.js +88 -0
- package/dist/hooks.d.ts +15 -0
- package/dist/hooks.js +7 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +29 -0
- package/dist/plan.d.ts +101 -0
- package/dist/plan.js +107 -0
- package/dist/processes.d.ts +103 -0
- package/dist/processes.js +189 -0
- package/dist/prs.d.ts +26 -0
- package/dist/prs.js +82 -0
- package/dist/receipt.d.ts +114 -0
- package/dist/receipt.js +35 -0
- package/dist/render.d.ts +43 -0
- package/dist/render.js +219 -0
- package/dist/resolve.d.ts +46 -0
- package/dist/resolve.js +90 -0
- package/dist/sweep.d.ts +68 -0
- package/dist/sweep.js +129 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +77 -0
- package/dist/worktrees.d.ts +48 -0
- package/dist/worktrees.js +338 -0
- package/dist/write-json.d.ts +6 -0
- package/dist/write-json.js +40 -0
- package/package.json +77 -3
package/README.md
CHANGED
|
@@ -1,3 +1,302 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @osdlc/repo-cleaner
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
What has been merged into the default branch, and what can go away.
|
|
4
|
+
|
|
5
|
+
Point it at a repository and it inventories the local branches, worktrees and
|
|
6
|
+
remote branches, classifying each as `safe-to-delete`, `safe-to-keep` or
|
|
7
|
+
`needs-investigation` — with the evidence behind the verdict and the literal
|
|
8
|
+
command that would act on it.
|
|
9
|
+
|
|
10
|
+
The sweep never deletes anything. It emits commands, and can write the
|
|
11
|
+
deletable rows as a plan. `repo-cleaner apply` deletes the plan rows you
|
|
12
|
+
select, after checking each one again.
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ repo-cleaner
|
|
16
|
+
/Users/me/work/api (acme/api, default main, origin)
|
|
17
|
+
local_branch fix/login-redirect delete PR #412 merged
|
|
18
|
+
git update-ref -d refs/heads/fix/login-redirect 3f9c2e1d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d
|
|
19
|
+
local_branch spike/cache review mixed or absent signals
|
|
20
|
+
worktree wt/412 delete clean; PR #412 merged
|
|
21
|
+
git -C /Users/me/work/api worktree remove wt/412
|
|
22
|
+
remote_branch origin/fix/login-… delete PR #412 merged (auto-delete didn't fire)
|
|
23
|
+
git push --force-with-lease=refs/heads/fix/login-redirect:3f9c2e1d8b7a6c5d4e3f2a1b0c9d8e7f6a5b4c3d origin :refs/heads/fix/login-redirect
|
|
24
|
+
|
|
25
|
+
1 to delete, 1 to review, across 1 repository.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install @osdlc/repo-cleaner
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
ESM only. Compiled JavaScript with type declarations, for Node 22 or newer.
|
|
35
|
+
|
|
36
|
+
## How it decides
|
|
37
|
+
|
|
38
|
+
Two signals settle most items, and either alone is enough:
|
|
39
|
+
|
|
40
|
+
| Signal | Source |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| A `MERGED` pull request from this repository, into the default branch, whose head commit is the branch tip | `gh pr list`, one listing per repository |
|
|
43
|
+
| The branch tip is reachable from the default branch | `git merge-base --is-ancestor` |
|
|
44
|
+
|
|
45
|
+
Ancestry is measured against `<remote>/<default>`, so work merged only into an
|
|
46
|
+
unpushed local default branch does not count. Without a remote, the local
|
|
47
|
+
default branch is used.
|
|
48
|
+
|
|
49
|
+
A merged PR that fails the head or base check is kept in `facts.pr` with
|
|
50
|
+
`evidence` set to `head-moved` or `other-base`. Fork PRs are ignored.
|
|
51
|
+
|
|
52
|
+
Everything else is held back. A branch that is neither merged nor an ancestor
|
|
53
|
+
is `needs-investigation` with the reasons spelled out, not a guess. A worktree
|
|
54
|
+
holding uncommitted or unpushed work is never proposed for removal whatever
|
|
55
|
+
else is true of it — losing an hour of someone's work costs more than any
|
|
56
|
+
amount of clutter.
|
|
57
|
+
|
|
58
|
+
Some worktrees are held back whatever their branch says. Each lands in
|
|
59
|
+
`facts.blockers`, and the row carries no command and has `deletable: false`:
|
|
60
|
+
|
|
61
|
+
| Blocker | Why |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `locked-worktree` | Someone ran `git worktree lock` and the lock's owner is alive or unknown; the reason is in `facts.lock_reason`. A lock whose owner is dead is not a blocker (see below). |
|
|
64
|
+
| `live-process` | A process other than an idle shell has its working directory in the worktree (`facts.live_processes`), lock or not. |
|
|
65
|
+
| `process-probe-failed` | The process table could not be read (no `lsof`, a timeout, an `lsof` that failed outright), so nobody can say the directory is idle. `facts.process_probe_error` has the message. |
|
|
66
|
+
| `hidden-index-flags` | Files marked skip-worktree or assume-unchanged hide edits from `status`. |
|
|
67
|
+
| `nested-repository` | An untracked or ignored directory holds a repository with its own history. |
|
|
68
|
+
| `branch-in-other-worktree` | Another worktree has the same branch checked out (`facts.other_worktrees`). |
|
|
69
|
+
|
|
70
|
+
### Locks and running processes
|
|
71
|
+
|
|
72
|
+
A crashed coding agent leaves its worktree locked. The sweep reads one generic
|
|
73
|
+
token from the lock reason, `pid <N>`, optionally followed by `start <time>`,
|
|
74
|
+
and asks whether that process is still running. It reads nothing else of the
|
|
75
|
+
reason, so a free-form reason such as `keep, build running` always blocks.
|
|
76
|
+
|
|
77
|
+
- The owner is dead: the lock is not a blocker. `facts.stale_lock` is `true`,
|
|
78
|
+
`facts.lock_owner` holds the pid and start time, and the row's commands begin
|
|
79
|
+
with `git worktree unlock`. `apply` unlocks, removes and verifies it, and the
|
|
80
|
+
receipt records the `unlock-worktree` step.
|
|
81
|
+
- The owner is alive, or cannot be ruled out: `locked-worktree`.
|
|
82
|
+
- A lock writes `pid <N>` and, to guard against pid reuse, `start <time>`. The
|
|
83
|
+
time must be precise to the second: a full ISO-8601 timestamp with a time of
|
|
84
|
+
day and a zone (`2026-10-25T18:22:19Z`; one with no zone is not accepted), epoch seconds (9-10 digits), epoch milliseconds
|
|
85
|
+
(12-13 digits), or the `ps -o lstart` form (`Sat Oct 25 18:22:19 2026`). With
|
|
86
|
+
one of those, a live pid whose own start time differs by more than two
|
|
87
|
+
seconds is a reused pid and counts as dead. Anything else, a date alone, a
|
|
88
|
+
short number or a partial date, cannot rule the owner out and the lock stays
|
|
89
|
+
a blocker.
|
|
90
|
+
- Only `ps` exiting 1 with no output and no error text means "no such pid".
|
|
91
|
+
Any other `ps` failure counts as alive.
|
|
92
|
+
- The `pid` token is trusted as a local pid. A pid from another container or
|
|
93
|
+
host that shares the filesystem is looked up here, will not be found, and
|
|
94
|
+
reads as dead; do not sweep a repository whose locks were written elsewhere.
|
|
95
|
+
- Before the unlock and again before the remove, `apply` asks again, and reads
|
|
96
|
+
the process table afresh. If the owner is alive by then, or a process other
|
|
97
|
+
than an idle shell works in the directory, or the table cannot be read, the
|
|
98
|
+
step fails and the run stops. A row dropped at the re-sweep touches nothing;
|
|
99
|
+
a step that fails at the unlock leaves the lock in place. `stale_lock` is part
|
|
100
|
+
of a worktree row's fingerprint, so a lock taken after the plan was written
|
|
101
|
+
drops the row too.
|
|
102
|
+
- If the remove fails after the unlock succeeded, the lock stays cleared:
|
|
103
|
+
`@osdlc/typed-git` has no `worktree lock` verb to put it back. The failed
|
|
104
|
+
step's receipt error says so.
|
|
105
|
+
- Separately, any process whose working directory is inside the worktree blocks
|
|
106
|
+
it as `live-process`, lock or not. A shell (`sh`, `bash`, `zsh`, `fish`) is
|
|
107
|
+
idle only when it has no child processes; it is then listed in
|
|
108
|
+
`facts.idle_shells` and does not block. A shell with a child, or whose
|
|
109
|
+
children are unknown, counts as a live process.
|
|
110
|
+
|
|
111
|
+
Both questions go to a `ProcessProbe` (`alive(pid, startedAt?)` and
|
|
112
|
+
`holders(path, { fresh? })`), passed as `probe` in the options of `sweepRepo`,
|
|
113
|
+
`sweepRepos` and `applyPlan`. The default, `defaultProcessProbe()`, runs
|
|
114
|
+
`ps -o lstart=`, `ps -A -o pid=,ppid=` and `lsof -d cwd` through
|
|
115
|
+
`@osdlc/command-seam`, and needs `ps` and `lsof` on `PATH`. It reads the
|
|
116
|
+
process table once per probe and reuses it for every worktree: `sweepRepo`
|
|
117
|
+
makes one probe per call, and `applyPlan` uses one probe for its re-sweep and
|
|
118
|
+
reads the table afresh for each pre-unlock and pre-remove check. Where the
|
|
119
|
+
table cannot be read the worktree is held back as `process-probe-failed`. A
|
|
120
|
+
host or test supplies its own probe.
|
|
121
|
+
Nothing is ever killed.
|
|
122
|
+
|
|
123
|
+
Without `gh`, or without authentication, classification proceeds on git
|
|
124
|
+
evidence alone and the report says so.
|
|
125
|
+
|
|
126
|
+
Every branch delete names the full sha the sweep judged. `update-ref -d` and
|
|
127
|
+
the pinned `--force-with-lease` push both refuse when the branch has moved
|
|
128
|
+
since, so a commit pushed while you read the plan survives it.
|
|
129
|
+
`localDeleteCommand` and `remoteDeleteCommand` build them for hosts that add
|
|
130
|
+
their own rows.
|
|
131
|
+
|
|
132
|
+
Every row names, in `requires`, the risks its delete carries, so a person
|
|
133
|
+
approves them by name:
|
|
134
|
+
|
|
135
|
+
| Authorization | When a row needs it |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `abandon-unmerged` | Commits not on the default branch and not captured by an exact-head merged PR; a worktree's unpushed commits. `facts.unique_commits` counts them. |
|
|
138
|
+
| `discard-changes` | A worktree has modified, staged or untracked files. |
|
|
139
|
+
| `confirm-open-pr` | The branch has an open PR. |
|
|
140
|
+
|
|
141
|
+
A row kept for structure (default branch, current HEAD, a branch held by a
|
|
142
|
+
worktree, the primary or caller's worktree) has `deletable: false`.
|
|
143
|
+
|
|
144
|
+
## Scope is opt-in
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
repo-cleaner # the repository you are standing in
|
|
148
|
+
repo-cleaner ~/work/api ~/work/web # named repositories
|
|
149
|
+
repo-cleaner ~/Developer # every checkout under it
|
|
150
|
+
repo-cleaner --all # every repository in the config file
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A bare invocation never fans out. What an argument names is read off the disk
|
|
154
|
+
rather than from how it is written: a path that is itself a checkout names
|
|
155
|
+
that repository, and a path that is not is searched for the checkouts under
|
|
156
|
+
it, descending through directories that are not checkouts — an owner
|
|
157
|
+
directory, a folder per client — and stopping at the ones that are.
|
|
158
|
+
|
|
159
|
+
A linked worktree collapses into the repository it belongs to, so a directory
|
|
160
|
+
holding a checkout and three of its worktrees sweeps one repository, not four.
|
|
161
|
+
Those worktrees still appear — as worktree rows in that repository's own
|
|
162
|
+
inventory.
|
|
163
|
+
|
|
164
|
+
The report ends with what the search cost — `searched 1 entry: 214
|
|
165
|
+
directories read, 12 checkouts found.` — because those numbers are how an
|
|
166
|
+
entry pointing at the wrong directory gets caught. A search that passes
|
|
167
|
+
10,000 directories says so on stderr while it is still running, and the walk
|
|
168
|
+
stops descending three levels below the entry.
|
|
169
|
+
|
|
170
|
+
An entry that resolves to nothing is reported rather than dropped. A worklist
|
|
171
|
+
that silently sweeps four of the five repositories it was given is worse than
|
|
172
|
+
one that says which it could not find.
|
|
173
|
+
|
|
174
|
+
## Options
|
|
175
|
+
|
|
176
|
+
| Flag | Effect |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `--all` | Sweep the repositories named in the config file |
|
|
179
|
+
| `--config <path>` | Config file to read |
|
|
180
|
+
| `--scope <list>` | `local-branches`, `worktrees`, `remote-branches` |
|
|
181
|
+
| `--remote <name>` | Remote to measure against (default `origin`) |
|
|
182
|
+
| `--no-gh` | Classify on git evidence alone |
|
|
183
|
+
| `--no-fetch` | Skip the fetch; the header then says `not fetched` |
|
|
184
|
+
| `--keep` | Include `safe-to-keep` rows in the report |
|
|
185
|
+
| `--commands` | Print the proposed commands of rows that need no authorization, and nothing else |
|
|
186
|
+
| `--output json` | The full inventory, facts included |
|
|
187
|
+
| `--branch <name\|HEAD>` | Sweep one branch of this repository: its worktree, local and remote branch. Refuses the default branch |
|
|
188
|
+
| `--write-plan` | Write the deletable rows as a plan for `apply` |
|
|
189
|
+
| `--plan-out <path>` | Write the plan here; implies `--write-plan` |
|
|
190
|
+
|
|
191
|
+
A checkout directory named `apply` is swept as `./apply`.
|
|
192
|
+
|
|
193
|
+
## Plan and apply
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
repo-cleaner --branch HEAD --plan-out plan.json
|
|
197
|
+
repo-cleaner apply --plan plan.json --row worktree:/path/to/wt --row local_branch:feat/x
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A plan holds each deletable row with a fingerprint of the facts behind its
|
|
201
|
+
verdict. `apply` sweeps again and drops a selected row that is gone, no longer
|
|
202
|
+
deletable, whose facts moved, or whose `requires` was not granted with
|
|
203
|
+
`--authorize <name>` (every row) or `--authorize <name>=<category>:<id>` (one
|
|
204
|
+
row). It then removes worktrees, deletes local branches, deletes remote
|
|
205
|
+
branches and prunes, in that order. Branch deletes are compare-and-swap, every
|
|
206
|
+
step is verified, and the first failure stops the run.
|
|
207
|
+
|
|
208
|
+
A receipt records every step and is rewritten after each one. Plans and receipts go under
|
|
209
|
+
`repo-cleaner/plans/` and `repo-cleaner/receipts/` in
|
|
210
|
+
[`@osdlc/tiered-dirs`](https://www.npmjs.com/package/@osdlc/tiered-dirs)' standalone durable home
|
|
211
|
+
(`repo-cleaner` directory, `REPO_CLEANER_*` override variables), unless `--plan-out` or `--receipt-dir` says
|
|
212
|
+
otherwise. `--dry-run` checks again and prints the steps without writing anything.
|
|
213
|
+
|
|
214
|
+
## Exit codes
|
|
215
|
+
|
|
216
|
+
| Code | Meaning |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `0` | Swept; or every selected row was applied or dropped with a reason |
|
|
219
|
+
| `1` | Failed against the state it found: a `{name}` token nothing defines, an unreadable plan, a row not in the plan |
|
|
220
|
+
| `2` | Cannot run as typed: an unknown flag, a malformed `--row`, `--branch` with `--all` or a path |
|
|
221
|
+
| `10` | Nothing to sweep: no entry named a repository |
|
|
222
|
+
| `11` | `apply` stopped at a failed step; the receipt says which |
|
|
223
|
+
|
|
224
|
+
## Configuration
|
|
225
|
+
|
|
226
|
+
Only the CLI reads a config file. The library takes repositories as an
|
|
227
|
+
argument, so a host that already knows where its repositories are passes them
|
|
228
|
+
in.
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
paths:
|
|
232
|
+
dev: ~/Developer
|
|
233
|
+
repos:
|
|
234
|
+
- '{dev}'
|
|
235
|
+
- ~/work/api
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`paths:` names a directory once; any `repos:` entry then refers to it as
|
|
239
|
+
`{name}`. A token nothing defines is an error, not an empty path.
|
|
240
|
+
|
|
241
|
+
The file is [`@osdlc/config-layers`](https://www.npmjs.com/package/@osdlc/config-layers)'s machine config — `config.yaml` in the host
|
|
242
|
+
OS's config home, resolved through
|
|
243
|
+
[`@osdlc/tiered-dirs`](https://www.npmjs.com/package/@osdlc/tiered-dirs)' standalone tier (`repo-cleaner`
|
|
244
|
+
directory, `REPO_CLEANER_*` override variables):
|
|
245
|
+
|
|
246
|
+
| OS | Path |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| macOS | `~/Library/Application Support/repo-cleaner/config.yaml` |
|
|
249
|
+
| Linux and other | `$XDG_CONFIG_HOME/repo-cleaner/config.yaml`, else `~/.config/repo-cleaner/config.yaml` |
|
|
250
|
+
| Windows | `%APPDATA%\repo-cleaner\config.yaml` |
|
|
251
|
+
|
|
252
|
+
`REPO_CLEANER_MACHINE_CONFIG`, or `--config <path>`, relocates it. A missing,
|
|
253
|
+
unreadable or malformed file degrades to no repositories rather than failing
|
|
254
|
+
the run.
|
|
255
|
+
|
|
256
|
+
## As a library
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { sweepRepo, sweepRepos } from '@osdlc/repo-cleaner'
|
|
260
|
+
|
|
261
|
+
const sweep = sweepRepo('/Users/me/work/api', { noGh: true })
|
|
262
|
+
for (const item of sweep.items) {
|
|
263
|
+
console.log(item.classification, item.id, item.reason)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = sweepRepos(['~/Developer'], { cwd: process.cwd() })
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`sweepRepo` never throws for a repository it cannot read: the failure comes
|
|
270
|
+
back as `error`, because a fan-out must not end on its first bad entry.
|
|
271
|
+
|
|
272
|
+
`fetch: true` runs `git fetch --prune --quiet --no-write-fetch-head <remote>`
|
|
273
|
+
before classifying, so ancestry is read against fresh remote-tracking refs.
|
|
274
|
+
That fetch is the only write. A repository whose fetch fails comes back with `error` starting
|
|
275
|
+
`fetch failed:` and no items. `repo.fetched` says which case applied. The
|
|
276
|
+
library default is off; the CLI fetches unless given `--no-fetch`.
|
|
277
|
+
|
|
278
|
+
### Adding knowledge the library does not have
|
|
279
|
+
|
|
280
|
+
Every row carries the facts behind its verdict, and `investigationParts` is a
|
|
281
|
+
list of sentences rather than a finished string. That is deliberate: a host
|
|
282
|
+
that knows something git does not — an issue tracker, a task corpus — re-ranks
|
|
283
|
+
the returned rows and appends its own sentence, instead of reaching into
|
|
284
|
+
classification. `applyPlan` takes the same re-ranking as its `annotate` hook,
|
|
285
|
+
so the rows it checks again are the rows the host planned from.
|
|
286
|
+
|
|
287
|
+
This works because of how the ladder is ordered. Every verdict the sweep
|
|
288
|
+
reaches on its own evidence outranks anything a host could contribute, so a
|
|
289
|
+
host never has to veto one. It only sharpens the rows left at `safe-to-keep`
|
|
290
|
+
or `needs-investigation`.
|
|
291
|
+
|
|
292
|
+
An issue tracker's linkage, applied this way, is a host's own layer over the
|
|
293
|
+
sweep and `applyPlan`.
|
|
294
|
+
|
|
295
|
+
## Runtime dependencies
|
|
296
|
+
|
|
297
|
+
`git` on `PATH`, and `gh` when PR state should count. Authentication is `gh`'s
|
|
298
|
+
own — this package reads no token.
|
|
299
|
+
|
|
300
|
+
## Licence
|
|
301
|
+
|
|
302
|
+
`MIT OR Apache-2.0`, at your option. See [LICENSE-MIT](./LICENSE-MIT) and [LICENSE-APACHE](./LICENSE-APACHE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report vulnerabilities privately. Do not open a public issue.
|
|
6
|
+
|
|
7
|
+
Use GitHub private vulnerability reporting on the repository: open the
|
|
8
|
+
[Security tab](https://github.com/osdlc/osdlc/security) and choose
|
|
9
|
+
"Report a vulnerability".
|
|
10
|
+
|
|
11
|
+
Include the version, a short description, and steps to reproduce.
|
|
12
|
+
|
|
13
|
+
## Supported versions
|
|
14
|
+
|
|
15
|
+
Only the latest release gets security fixes. While the version is 0.x, that
|
|
16
|
+
is the latest minor release.
|
|
17
|
+
|
|
18
|
+
## What to expect
|
|
19
|
+
|
|
20
|
+
This is a small project. We aim to reply within a week, tell you whether we
|
|
21
|
+
accept the report, and credit you in the release notes if you want that.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The published entry point: `npx @osdlc/repo-cleaner …` and a plain
|
|
3
|
+
// `node ./bin/repo-cleaner.js …` both land here. Requires the built dist/cli.js
|
|
4
|
+
// (`pnpm run build`).
|
|
5
|
+
import { runMain } from '@osdlc/cli-tool'
|
|
6
|
+
import { main } from '../dist/cli.js'
|
|
7
|
+
|
|
8
|
+
await runMain(main, { name: 'repo-cleaner' })
|
package/dist/apply.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply an approved cleanup plan.
|
|
3
|
+
*
|
|
4
|
+
* Re-sweeps each repository and drops selected rows that are gone, no longer
|
|
5
|
+
* deletable, changed, or lack a grant. Deletes the rest in a fixed order and
|
|
6
|
+
* stops at the first failure:
|
|
7
|
+
*
|
|
8
|
+
* 1. worktrees, each unlocked first when its lock's owner is dead, and each
|
|
9
|
+
* checked again for live processes just before it is unlocked or removed;
|
|
10
|
+
* 2. local branches;
|
|
11
|
+
* 3. remote branches;
|
|
12
|
+
* 4. one prune per repository.
|
|
13
|
+
*
|
|
14
|
+
* Branch deletes are compare-and-swap on the planned sha. Every step is
|
|
15
|
+
* verified, and the receipt is rewritten after each.
|
|
16
|
+
*/
|
|
17
|
+
import type { CommandRunner } from '@osdlc/command-seam';
|
|
18
|
+
import type { HostHooks } from './hooks.ts';
|
|
19
|
+
import { type ProcessProbe } from './processes.ts';
|
|
20
|
+
import { type Plan } from './plan.ts';
|
|
21
|
+
import type { Receipt, RowRef } from './receipt.ts';
|
|
22
|
+
import { type Authorization } from './types.ts';
|
|
23
|
+
/** An authorization the caller gave, for one row or (`row: null`) for all. */
|
|
24
|
+
export interface Grant {
|
|
25
|
+
name: Authorization;
|
|
26
|
+
row: RowRef | null;
|
|
27
|
+
}
|
|
28
|
+
export interface ApplyOptions extends HostHooks {
|
|
29
|
+
/** Injected runners. Defaults spawn the real `git` and `gh`. */
|
|
30
|
+
git?: CommandRunner | undefined;
|
|
31
|
+
gh?: CommandRunner | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Answers lock-owner and working-directory questions, for the re-sweep and
|
|
34
|
+
* for the checks just before an unlock and a remove. Defaults to `ps` and
|
|
35
|
+
* `lsof`.
|
|
36
|
+
*/
|
|
37
|
+
probe?: ProcessProbe | undefined;
|
|
38
|
+
/** The caller's directory; the sweep never offers the worktree holding it. */
|
|
39
|
+
callerCwd: string;
|
|
40
|
+
dryRun: boolean;
|
|
41
|
+
/** Where the receipt goes. Unused under `dryRun`. */
|
|
42
|
+
receiptPath: string;
|
|
43
|
+
/** Shown in the receipt. */
|
|
44
|
+
planPath: string;
|
|
45
|
+
/** Limit the run to the plan repository rooted here. */
|
|
46
|
+
repoRoot?: string | undefined;
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
}
|
|
49
|
+
/** A selection or grant the plan cannot satisfy. Nothing has been touched. */
|
|
50
|
+
export declare class ApplyArgumentError extends Error {
|
|
51
|
+
}
|
|
52
|
+
/** `<category>:<id>`, split at the first colon. */
|
|
53
|
+
export declare function parseRowRef(text: string): RowRef;
|
|
54
|
+
/** `<name>` for every row, or `<name>=<category>:<id>` for one. */
|
|
55
|
+
export declare function parseGrant(text: string): Grant;
|
|
56
|
+
/**
|
|
57
|
+
* Apply the `requested` rows of `plan` under `grants`. A row named twice runs once.
|
|
58
|
+
*
|
|
59
|
+
* @throws ApplyArgumentError before touching anything, when a row or grant does
|
|
60
|
+
* not fit the plan.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyPlan(plan: Plan, requested: readonly RowRef[], grants: readonly Grant[], opts: ApplyOptions): Receipt;
|