@osdlc/repo-cleaner 0.17.2 → 0.19.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 (54) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +691 -305
  3. package/dist/branches.d.ts +37 -36
  4. package/dist/branches.js +199 -196
  5. package/dist/clean.d.ts +179 -0
  6. package/dist/clean.js +824 -0
  7. package/dist/cli.d.ts +12 -16
  8. package/dist/cli.js +367 -478
  9. package/dist/hooks.d.ts +8 -12
  10. package/dist/hooks.js +2 -2
  11. package/dist/index.d.ts +18 -25
  12. package/dist/index.js +16 -23
  13. package/dist/landing.d.ts +3 -3
  14. package/dist/landing.js +7 -7
  15. package/dist/plan.d.ts +76 -70
  16. package/dist/plan.js +82 -77
  17. package/dist/providers/github.d.ts +2 -1
  18. package/dist/providers/github.js +2 -1
  19. package/dist/providers/gitlab.d.ts +3 -2
  20. package/dist/providers/gitlab.js +14 -8
  21. package/dist/providers/index.d.ts +5 -4
  22. package/dist/providers/index.js +8 -5
  23. package/dist/providers/types.d.ts +8 -2
  24. package/dist/prs.d.ts +35 -7
  25. package/dist/prs.js +45 -12
  26. package/dist/receipt.d.ts +46 -57
  27. package/dist/receipt.js +52 -28
  28. package/dist/reference.d.ts +12 -0
  29. package/dist/reference.js +152 -0
  30. package/dist/remote-mirror.d.ts +9 -1
  31. package/dist/remote-mirror.js +19 -1
  32. package/dist/render.d.ts +66 -61
  33. package/dist/render.js +206 -255
  34. package/dist/resolve.d.ts +3 -3
  35. package/dist/resolve.js +4 -12
  36. package/dist/status-line.d.ts +1 -1
  37. package/dist/status-line.js +5 -5
  38. package/dist/step-board.d.ts +1 -1
  39. package/dist/step-board.js +2 -5
  40. package/dist/submodules.d.ts +120 -0
  41. package/dist/submodules.js +292 -0
  42. package/dist/sweep.d.ts +107 -64
  43. package/dist/sweep.js +333 -119
  44. package/dist/types.d.ts +294 -105
  45. package/dist/types.js +106 -54
  46. package/dist/util.d.ts +13 -0
  47. package/dist/util.js +36 -0
  48. package/dist/version.generated.d.ts +1 -1
  49. package/dist/version.generated.js +1 -1
  50. package/dist/worktrees.d.ts +21 -37
  51. package/dist/worktrees.js +198 -165
  52. package/package.json +5 -5
  53. package/dist/apply.d.ts +0 -131
  54. package/dist/apply.js +0 -541
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The full reference `--help --extended` prints: what every option means,
3
+ * the words the report uses, and which combinations are refused. The README
4
+ * says the same in its own layout; keep the two in step.
5
+ */
6
+ const TARGETS = `Targets
7
+ A target is what to sweep:
8
+ a checkout that repository; a linked worktree sweeps the repository it belongs to
9
+ a directory every checkout found under it, three levels deep at most
10
+ a remote URL its remote branches only, through a clone kept in the cache
11
+ directory (https://, ssh://, file://, or user@host:path)
12
+ No target sweeps the directory you are in, read the same way.`;
13
+ const SWEEP = `Sweep options (the report, plan and clean)
14
+ --remote Also judge remote branches. Turns on the network: fetches
15
+ each remote first and asks GitHub or GitLab about pull
16
+ requests. Slower.
17
+ --offline With --remote: do not fetch; judge against the
18
+ remote-tracking refs already on disk, remote branches
19
+ included, so judging does not contact the remote. Tips can
20
+ be stale; clean asks the remote before deleting on it.
21
+ Pull requests are still looked up; add --provider none
22
+ to judge with no network at all.
23
+ --only <kinds> Only these kinds of row: worktree, branch, remote,
24
+ comma-separated. Default: worktree,branch, plus remote with
25
+ --remote. Naming remote implies --remote.
26
+ --branch <name|HEAD> Only the rows of one branch: its worktree, its local branch,
27
+ and its remote branch with --remote. HEAD is the branch
28
+ checked out at the target. Never the default branch.
29
+ --against <remote> The remote whose default branch counts as "merged into".
30
+ Default: origin, then the first remote.
31
+ --provider <name> With --remote: where pull requests come from. auto (by the
32
+ remote's host), github, gitlab, or none to judge on git
33
+ evidence alone.
34
+ --no-progress No progress line on stderr.`;
35
+ const REPORT = `The report: repo-cleaner [target...]
36
+ Deletes nothing. Lists each row with its verdict, why, the risks it carries,
37
+ and the command that would delete it.
38
+ --all Also show keep rows. By default only delete, review and
39
+ blocked rows are listed. Only with --format text.
40
+ --format text The report. The default.
41
+ --format summary Counts per repository: total, merged, delete, prune,
42
+ review, blocked, keep.
43
+ --format commands Only the commands of delete rows that carry no risk.
44
+ --format json Everything, evidence included, for scripts.`;
45
+ const PLAN = `repo-cleaner plan [target...]
46
+ Sweeps and saves every row, each with a fingerprint of the evidence behind its
47
+ verdict, so it can be reviewed now and run later with clean --plan.
48
+ -o, --out <file> Where the plan goes; - (the default) is stdout.`;
49
+ const CLEAN = `repo-cleaner clean [target...]
50
+ Sweeps (or reads --plan), checks every row again, shows the steps, asks, then
51
+ deletes, compare-and-swap, verifying each step and stopping at the first failure.
52
+ Without --pick it runs every row judged delete.
53
+ --pick <row> Work on exactly these rows. Narrows the run to them, and opts
54
+ in review rows and kept rows with an action (an open pull
55
+ request), which never run otherwise. A picked row still
56
+ needs --allow for its risks. A blocked row is skipped.
57
+ Repeatable.
58
+ --spare <row> Leave this row alone, and make sure it is what you saw. A
59
+ spared row is not deleted, even if it is judged delete. A
60
+ spared remote branch is still checked: if its tip moved or
61
+ it is gone since the plan, the run stops before deleting
62
+ anything. Sparing a worktree also spares its branch.
63
+ Repeatable.
64
+ --allow <risk> Accept this risk for every row that carries it.
65
+ --allow <risk>=<row> Accept it for that one row only.
66
+ --plan <file|-> Run a saved plan instead of sweeping now (- reads stdin).
67
+ Rows whose evidence changed since the plan are dropped.
68
+ -y, --yes Do not ask. Without it, and with no terminal to ask on,
69
+ clean refuses.
70
+ --dry-run Check and show the steps, then stop.
71
+ --receipt <file> Write a record of every step, rewritten after each one.
72
+ Default: next to a --plan file; none otherwise.
73
+ --format text|json How the steps and the outcome are shown.`;
74
+ const CACHE = `repo-cleaner cache [list | remove <url>... | remove --all]
75
+ Lists or removes the clones kept for remote URL targets. A removed clone is
76
+ cloned again by the next sweep of its URL; a plan that names it cannot run.`;
77
+ const WORDS = `Rows
78
+ One thing that could be deleted, named <kind>:<name>:
79
+ worktree:<path> a linked worktree: removed with git worktree remove, or,
80
+ when its directory is already gone, its record pruned
81
+ branch:<name> a local branch, deleted with git update-ref -d <sha>
82
+ remote:<remote>/<name> a branch on a remote, deleted by a push with a lease
83
+ A bare name or path works when only one row has it.
84
+
85
+ Verdicts
86
+ delete Its work is on the default branch, or it is otherwise safe to lose.
87
+ clean deletes it, once its risks are allowed.
88
+ review The signals are mixed, or it holds work. Deleted only when picked and
89
+ its risks are allowed.
90
+ blocked Something rules removal out: a lock, a running process, a submodule
91
+ that cannot be read, hidden index flags, a nested repository, a branch
92
+ checked out twice. Never deleted; fix the cause and sweep again.
93
+ keep Structural (the default branch, the current checkout, the primary
94
+ worktree): never deleted. Or in use (an open pull request): deleted
95
+ only when picked and confirm-open-pr is allowed.
96
+ prune Shown for a delete row whose worktree directory is already gone: only
97
+ the stale record goes.
98
+
99
+ Risks (approve each by name with --allow)
100
+ abandon-unmerged Commits that are not on the default branch, or a worktree's
101
+ (or its submodules') commits and stashes that no remote has.
102
+ discard-changes Uncommitted or untracked files in the worktree or any of its
103
+ submodules.
104
+ confirm-open-pr The branch has an open pull request, or open pull requests could
105
+ not be checked (the CLI is missing or signed out, or the listing
106
+ failed); deleting the remote branch closes it.
107
+ A worktree's submodules are part of it: one --allow on the worktree row covers
108
+ the same risk in every submodule.`;
109
+ const CONFLICTS = `Refused before anything is deleted (exit 2)
110
+ A combination is refused when it contradicts itself, or when one of its options
111
+ would have no effect. Most are refused before anything is swept; a row named
112
+ two ways (a bare name and its id), and an --allow for a row, are checked
113
+ against the plan, after the sweep.
114
+ the same row in --pick and --spare
115
+ --allow <risk>=<row> for a spared row, or for a row outside --pick
116
+ a picked or spared row of a kind --only leaves out
117
+ a picked or spared row that is not one of --branch's rows
118
+ --branch with more than one target, or with a remote URL, or the default branch
119
+ --plan with targets or sweep options: the plan already fixed them
120
+ --plan - without --yes or --dry-run: stdin carries the plan, so nothing can ask
121
+ --yes with --dry-run
122
+ --receipt with --dry-run
123
+ --offline or --provider without --remote
124
+ --remote with an --only that leaves remote out
125
+ a remote URL target with --only worktree or --only branch, with --offline, or
126
+ mixed with checkout targets
127
+ --all with any --format but text
128
+
129
+ Refused after the sweep, before deleting (exit 1)
130
+ a picked, spared or allowed row the plan does not have
131
+ a row name that matches rows in more than one repository
132
+ a spared remote branch that moved since the plan, or that the re-check no longer finds
133
+
134
+ Allowed, because nothing contradicts
135
+ the same row picked twice: it runs once
136
+ --allow <risk> with --allow <risk>=<row>: the broader one covers it
137
+ --allow for a risk no row that runs carries: the preview lists it as unused
138
+ picking a blocked or keep row: it is skipped and shown with the reason
139
+ --only remote without --remote: asking for remote branches implies the network`;
140
+ const EXIT = `Exit codes
141
+ 0 swept; or clean ran, was declined, or had nothing to do
142
+ 1 failed against the state it found: an unreadable plan, a row not in it
143
+ 2 cannot run as typed: an unknown flag, a malformed row, a refused combination
144
+ 10 no target named a repository
145
+ 11 clean stopped at a failed step; the receipt says which`;
146
+ /** The extended help for each command: its own options, then the shared vocabulary. */
147
+ export const REFERENCE = {
148
+ report: [TARGETS, SWEEP, REPORT, WORDS, CONFLICTS, EXIT].join('\n\n'),
149
+ plan: [TARGETS, SWEEP, PLAN, WORDS, EXIT].join('\n\n'),
150
+ clean: [TARGETS, SWEEP, CLEAN, WORDS, CONFLICTS, EXIT].join('\n\n'),
151
+ cache: CACHE,
152
+ };
@@ -8,7 +8,7 @@
8
8
  * fetches them over the network one comparison at a time, which made a sweep
9
9
  * of a few hundred branches take minutes instead of seconds. It is kept
10
10
  * rather than thrown away so a plan written from it can be applied later: the
11
- * plan names the clone's directory, and `apply` re-sweeps and pushes deletes
11
+ * plan names the clone's directory, and `clean` re-sweeps and pushes deletes
12
12
  * from there. A later run reuses it, and the sweep's fetch brings it up to
13
13
  * date.
14
14
  */
@@ -77,3 +77,11 @@ export declare function listMirrors(cacheDir: string, opts?: {
77
77
  * given, credentials included.
78
78
  */
79
79
  export declare function cachedMirrorOf(url: string, cacheDir: string): string | null;
80
+ /**
81
+ * True when `target` names a remote rather than a directory: a URL with a
82
+ * scheme (`https://`, `ssh://`, `file://`), or the scp form `user@host:path`
83
+ * that names no directory on disk. A Windows drive path is never one.
84
+ */
85
+ export declare function isRemoteUrl(target: string, exists: (path: string) => boolean): boolean;
86
+ /** Where remote URL targets' clones are kept: `repo-cleaner/remotes` under the user's cache directory. */
87
+ export declare function defaultCacheDir(env?: Record<string, string | undefined>): string;
@@ -8,14 +8,16 @@
8
8
  * fetches them over the network one comparison at a time, which made a sweep
9
9
  * of a few hundred branches take minutes instead of seconds. It is kept
10
10
  * rather than thrown away so a plan written from it can be applied later: the
11
- * plan names the clone's directory, and `apply` re-sweeps and pushes deletes
11
+ * plan names the clone's directory, and `clean` re-sweeps and pushes deletes
12
12
  * from there. A later run reuses it, and the sweep's fetch brings it up to
13
13
  * date.
14
14
  */
15
15
  import { createHash } from 'node:crypto';
16
16
  import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs';
17
17
  import { dirname, join } from 'node:path';
18
+ import { systemDirs } from '@osdlc/cli-tool';
18
19
  import { Git, parseRemoteRepo } from '@osdlc/typed-git';
20
+ import { VENDOR_DIRS } from "./devlink.js";
19
21
  /** A remote URL that cannot be mirrored, or a mirror that cannot be made or reused. */
20
22
  export class RemoteMirrorError extends Error {
21
23
  constructor(message) {
@@ -163,3 +165,19 @@ export function cachedMirrorOf(url, cacheDir) {
163
165
  const dir = join(cacheDir, mirrorName(url));
164
166
  return existsSync(dir) ? dir : null;
165
167
  }
168
+ /**
169
+ * True when `target` names a remote rather than a directory: a URL with a
170
+ * scheme (`https://`, `ssh://`, `file://`), or the scp form `user@host:path`
171
+ * that names no directory on disk. A Windows drive path is never one.
172
+ */
173
+ export function isRemoteUrl(target, exists) {
174
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target))
175
+ return true;
176
+ if (/^[a-z]:[\\/]/i.test(target))
177
+ return false;
178
+ return /^(?:[^@/:\\]+@)?[^/:\\]+:(?!\/\/).+$/.test(target) && !exists(target);
179
+ }
180
+ /** Where remote URL targets' clones are kept: `repo-cleaner/remotes` under the user's cache directory. */
181
+ export function defaultCacheDir(env = process.env) {
182
+ return systemDirs(VENDOR_DIRS, { env }).standalone('repo-cleaner').cache().app('remotes').path;
183
+ }
package/dist/render.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Rendering a sweep for a terminal, and for a program.
2
+ * Rendering a sweep and a run, for a terminal and for a program.
3
3
  *
4
4
  * The text report is columnar because it is read by eye across repositories:
5
- * the eye finds a column, and a line-per-item paragraph gives it nothing to
6
- * find. JSON carries everything the text report elides, including the facts
7
- * behind every verdict.
5
+ * the eye finds a column, and a line-per-row paragraph gives it nothing to
6
+ * find. JSON carries everything the text report leaves out, the evidence
7
+ * behind every verdict included.
8
8
  */
9
- import type { Receipt, Step } from './receipt.ts';
10
- import { type RepoSweep, type SweepItem, type SweepResult } from './types.ts';
9
+ import type { Preview } from './clean.ts';
10
+ import type { Dropped, Receipt, Step } from './receipt.ts';
11
+ import { type Kind, type RepoSweep, type Sweep } from './types.ts';
11
12
  /**
12
13
  * How a report colors its words. {@link PLAIN} leaves them as they are; the
13
14
  * CLI passes {@link TERMINAL_STYLE} when the stream is a terminal that takes
@@ -31,57 +32,43 @@ export interface Style {
31
32
  export declare const PLAIN: Style;
32
33
  /** The basic terminal colors: red to delete, blue to prune, yellow to review, green to keep. */
33
34
  export declare const TERMINAL_STYLE: Style;
34
- /**
35
- * A row's authorizations for a report, e.g. `requires abandon-unmerged (drops 3
36
- * commits), confirm-open-pr`. Empty when the row needs none.
37
- */
38
- export declare function requiresNote(item: Pick<SweepItem, 'requires' | 'facts'>): string;
39
- /** One repository's rows, indented under its header. */
35
+ /** One repository's rows, indented under its header. `all` shows `keep` rows too. */
40
36
  export declare function renderRepo(sweep: RepoSweep, opts?: {
41
- keep?: boolean;
37
+ all?: boolean;
42
38
  style?: Style;
43
39
  }): string;
44
40
  /**
45
- * What the search cost, or null when nothing was searched — every entry
41
+ * What the search cost, or null when nothing was searched — every target
46
42
  * named a checkout outright.
47
43
  *
48
- * Worth a line because the numbers are how a person catches an entry
44
+ * Worth a line because the numbers are how a person catches a target
49
45
  * pointing at the wrong directory: thousands of directories read for three
50
- * repositories says the entry is a home directory, not a workspace.
46
+ * repositories says the target is a home directory, not a workspace.
51
47
  */
52
- export declare function renderScan(result: SweepResult): string | null;
48
+ export declare function renderScan(sweep: Pick<Sweep, 'scan'>): string | null;
53
49
  /** The one-line verdict a person reads first, and often only. */
54
- export declare function renderSummary(result: SweepResult, style?: Style): string;
55
- /**
56
- * One repository as counts: a row per category with its total, how many are
57
- * merged, and how many the sweep would delete, prune, review or keep.
58
- */
59
- export declare function renderCounts(sweep: RepoSweep): string;
60
- /** The whole run as counts: a table per repository, then the one-line summary. */
61
- export declare function renderCountsText(result: SweepResult, style?: Style): string;
62
- /** The whole run's counts as JSON: no rows, no facts. */
63
- export declare function renderCountsJson(result: SweepResult): string;
64
- /** The whole run as text: duplicate checkouts, a section per repository, then the summary. */
65
- export declare function renderText(result: SweepResult, opts?: {
66
- keep?: boolean;
50
+ export declare function renderTotals(sweep: Sweep, style?: Style): string;
51
+ /** The sweep as counts (`--format summary`): a table per repository, then the totals line. */
52
+ export declare function renderSummary(sweep: Sweep, style?: Style): string;
53
+ /** The sweep as text: duplicate checkouts, a section per repository, then the totals. */
54
+ export declare function renderText(sweep: Sweep, opts?: {
55
+ all?: boolean;
67
56
  style?: Style;
68
57
  }): string;
69
- /**
70
- * The whole run as JSON, facts included. `extra` adds top-level keys, such as
71
- * the path of a plan the run wrote.
72
- */
73
- export declare function renderJson(result: SweepResult, extra?: Record<string, unknown>): string;
74
- /** Every command a `safe-to-delete` row that needs no authorization proposes, in report order. */
75
- export declare function proposedCommands(result: SweepResult): string[];
76
- /** What an apply run did, or would do, counted by kind. */
77
- export interface ApplySummary {
78
- /** Rows deleted (or, in a dry run, that would be), by category. Pruned worktrees are not counted here. */
79
- deleted: {
80
- worktree: number;
81
- local_branch: number;
82
- remote_branch: number;
83
- };
84
- /** Worktrees whose stale registration was pruned; their directories were gone or left alone. */
58
+ /** The sweep as JSON: every row with its evidence, the counts, and what was swept. */
59
+ export declare function renderJson(sweep: Sweep, extra?: Record<string, unknown>): string;
60
+ /** Every command of a `delete` row that carries no risk, in report order (`--format commands`). */
61
+ export declare function renderCommands(sweep: Sweep): string[];
62
+ /** What a run did, or would do, counted by kind. */
63
+ export interface RunSummary {
64
+ /**
65
+ * Rows deleted (or, in a preview, that would be), by kind. Pruned worktrees,
66
+ * and remote branches a delete step found already gone, are counted apart.
67
+ */
68
+ deleted: Record<Kind, number>;
69
+ /** Remote branches a delete step found the remote had already deleted. */
70
+ gone: number;
71
+ /** Worktrees whose stale record was pruned; their directories were gone or left alone. */
85
72
  pruned: number;
86
73
  /** Dropped rows by reason. */
87
74
  dropped: Record<string, number>;
@@ -90,29 +77,47 @@ export interface ApplySummary {
90
77
  /** Row steps never reached because a step failed. */
91
78
  notRun: number;
92
79
  }
93
- /** Count a receipt's outcome. A dry run's `would-run` steps count as done. */
94
- export declare function summarizeReceipt(receipt: Receipt): ApplySummary;
95
- /** Rows a receipt deletes or prunes, which is what a person is asked to approve. */
96
- export declare function rowsActedOn(summary: ApplySummary): number;
97
- /** The closing lines of an apply run: what it deleted, skipped and could not finish. */
98
- export declare function renderApplySummary(receipt: Receipt): string[];
80
+ /** Count what `steps` did and `dropped` left out. A preview's `would-run` steps count as done. */
81
+ export declare function summarizeRun(r: {
82
+ steps: readonly Step[];
83
+ dropped: readonly Dropped[];
84
+ }): RunSummary;
85
+ /** Rows a run deletes or prunes, which is what a person is asked to approve. */
86
+ export declare function rowsActedOn(summary: RunSummary): number;
87
+ /** The closing lines of a run or preview: what it deleted, skipped and could not finish. */
88
+ export declare function renderRunSummary(r: {
89
+ steps: readonly Step[];
90
+ dropped: readonly Dropped[];
91
+ }, opts?: {
92
+ preview?: boolean;
93
+ }): string[];
99
94
  /** The word a preview shows for a step: what it does to the row. */
100
95
  export declare const STEP_VERB: Record<Step['action'], string>;
101
96
  /**
102
- * What `apply` is about to do, for the person asked to approve it: per
103
- * repository, each row and the command that deletes it, the rows it will
104
- * skip and why, then the counts. Terminal controls are stripped from what it
105
- * shows, so a path or name cannot hide part of what is approved.
97
+ * What a run is about to do, for the person asked to approve it: per
98
+ * repository, each row and the command that deletes it, the rows it will skip
99
+ * and why, the allows no row needed, then the counts. Terminal controls are
100
+ * stripped from what it shows, so a path or name cannot hide part of what is
101
+ * approved.
106
102
  */
107
- export declare function renderPreview(receipt: Receipt, style?: Style): string;
108
- /** Rows dropped by a live run's own re-check, after the preview: each with why. */
109
- export declare function renderPreviewDrops(dropped: Receipt['dropped'], style?: Style): string;
103
+ export declare function renderPreview(p: Pick<Preview, 'steps' | 'dropped'> & Partial<Pick<Preview, 'unusedAllows'>>, style?: Style): string;
104
+ /** Rows dropped by a run's own re-check after the preview, each with why. */
105
+ export declare function renderLateDrops(dropped: readonly Dropped[], style?: Style): string;
110
106
  /**
111
107
  * A failed step for a run shown live: `✗`, the row, and the error in full,
112
- * which the step's row on the board is too narrow to hold.
108
+ * which the step's line on the board is too narrow to hold.
113
109
  */
114
110
  export declare function renderStepFailure(step: Step, style?: Style): string;
115
- /** An apply run as text: dropped rows, each step, a summary, then where the receipt is. */
111
+ /** A run as text: dropped rows, each step, a summary, then where the receipt is. */
116
112
  export declare function renderReceipt(receipt: Receipt, receiptPath: string | null, style?: Style, opts?: {
117
113
  steps?: boolean;
118
114
  }): string;
115
+ /** The renderers a library user needs: the CLI's own formats. */
116
+ export declare const render: {
117
+ text: typeof renderText;
118
+ json: typeof renderJson;
119
+ summary: typeof renderSummary;
120
+ commands: typeof renderCommands;
121
+ preview: typeof renderPreview;
122
+ receipt: typeof renderReceipt;
123
+ };