@webpieces/pr-gate 0.4.688 → 0.4.689
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/package.json +2 -2
- package/src/scripts/commands/checkout-clean-main-command.js +6 -1
- package/src/scripts/commands/checkout-clean-main-command.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +73 -52
- package/src/scripts/commands/cleanup-command.js +219 -91
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/cleanup-options.d.ts +88 -0
- package/src/scripts/commands/cleanup-options.js +174 -0
- package/src/scripts/commands/cleanup-options.js.map +1 -0
- package/src/scripts/commands/worktree-cleanup.d.ts +19 -2
- package/src/scripts/commands/worktree-cleanup.js +44 -3
- package/src/scripts/commands/worktree-cleanup.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +6 -2
- package/src/scripts/pr-gate-app.js +6 -3
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/wp-cleanup.js +11 -3
- package/src/scripts/wp-cleanup.js.map +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CliUsage } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* `wp-cleanup`'s flags, parsed — the whole decision about what this run deletes, as a value.
|
|
4
|
+
*
|
|
5
|
+
* WHY A CLASS AND NOT A BAG OF BOOLEANS THREADED THROUGH ARGUMENTS: the decision "delete these
|
|
6
|
+
* worktrees / those branches / nothing at all" is ONE thing, made once at the bin, and every method
|
|
7
|
+
* downstream has to agree about it. Passed as separate parameters it becomes four independent
|
|
8
|
+
* arguments that a new call site can get half-right — and half-right, here, means deleting a ref
|
|
9
|
+
* nobody chose. Same argument BuildOptions makes for its single `force` field.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FLAG_DELETE_BRANCHES = "--delete-branches";
|
|
12
|
+
export declare const FLAG_DELETE_WORKTREES = "--delete-worktrees";
|
|
13
|
+
export declare const FLAG_REPORT = "--report";
|
|
14
|
+
export declare const FLAG_INTERACTIVE = "--interactive";
|
|
15
|
+
export declare const SELECTION_UNSET = "unset";
|
|
16
|
+
export declare const SELECTION_ALL = "all";
|
|
17
|
+
export declare const SELECTION_NONE = "none";
|
|
18
|
+
export declare const SELECTION_NUMBERS = "numbers";
|
|
19
|
+
/**
|
|
20
|
+
* One `--delete-branches=` / `--delete-worktrees=` answer: `all`, `none`, or the numbers printed in
|
|
21
|
+
* the classified block on this run.
|
|
22
|
+
*
|
|
23
|
+
* THE NUMBERING CONTRACT: `numbers` index the block wp-cleanup just printed, 1-based, and an index
|
|
24
|
+
* outside that block is a hard failure rather than a silent skip. A number that lands on the wrong
|
|
25
|
+
* ref is the single way this command can delete something nobody asked for, so "the list moved under
|
|
26
|
+
* me" must stop the run, not quietly delete four of the five refs the caller meant.
|
|
27
|
+
*/
|
|
28
|
+
export declare class DeleteSelection {
|
|
29
|
+
readonly mode: string;
|
|
30
|
+
readonly numbers: readonly number[];
|
|
31
|
+
private readonly flag;
|
|
32
|
+
/**
|
|
33
|
+
* ONE constructor, taking argv's two facts about the flag: was it there, and what did it carry.
|
|
34
|
+
*
|
|
35
|
+
* Parsing in the constructor rather than in a static factory is what keeps this to one spelling —
|
|
36
|
+
* there is no way to hold a DeleteSelection that was never checked. `present === false` is the
|
|
37
|
+
* absent flag (the run falls back to its tty sniff); present with an EMPTY value is an error, not
|
|
38
|
+
* an implicit `all` and not an implicit `none`: both readings are defensible, which is exactly
|
|
39
|
+
* why neither may be guessed at when the outcome is a delete.
|
|
40
|
+
*/
|
|
41
|
+
constructor(flag: string, present: boolean, raw: string);
|
|
42
|
+
private parseNumbers;
|
|
43
|
+
/** Did the caller say anything? An explicit flag ALWAYS beats the terminal sniff. */
|
|
44
|
+
given(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The chosen entries out of the block that was just printed.
|
|
47
|
+
*
|
|
48
|
+
* Range is checked against THAT block: a number past its end means the caller is holding numbers
|
|
49
|
+
* from an older run, and the refs have moved under them. That stops the run.
|
|
50
|
+
*/
|
|
51
|
+
pick<T>(block: readonly T[]): T[];
|
|
52
|
+
private usage;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Everything argv said about this cleanup run.
|
|
56
|
+
*
|
|
57
|
+
* `report` prints the full classified report and deletes NOTHING — the print-and-exit case, and the
|
|
58
|
+
* one honest way to get numbers that are still valid on the next command, because a run that deletes
|
|
59
|
+
* nothing cannot renumber anything.
|
|
60
|
+
*
|
|
61
|
+
* `interactive` forces the prompt when stdin is not a tty. It exists because `process.stdin.isTTY`
|
|
62
|
+
* was never a fact about who is standing there, only a proxy: a human running
|
|
63
|
+
* `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the
|
|
64
|
+
* DEFAULT, and any explicit flag beats it.
|
|
65
|
+
*/
|
|
66
|
+
export declare class CleanupOptions {
|
|
67
|
+
readonly branches: DeleteSelection;
|
|
68
|
+
readonly worktrees: DeleteSelection;
|
|
69
|
+
readonly report: boolean;
|
|
70
|
+
readonly interactive: boolean;
|
|
71
|
+
constructor(branches: DeleteSelection, worktrees: DeleteSelection, report: boolean, interactive: boolean);
|
|
72
|
+
/**
|
|
73
|
+
* Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.
|
|
74
|
+
* A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.
|
|
75
|
+
*/
|
|
76
|
+
prompts(): boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What `wp-cleanup --help` prints, and the ONE list CliArgs validates argv against.
|
|
80
|
+
*
|
|
81
|
+
* It lives here rather than inline in the bin so a spec can assert that every flag this command
|
|
82
|
+
* honours is a flag it also tells you about. A flag that works but is undocumented, or documented but
|
|
83
|
+
* rejected, is the same defect from either side — and the bin itself is a `runMain` call that cannot
|
|
84
|
+
* be imported into a test without executing.
|
|
85
|
+
*/
|
|
86
|
+
export declare class CleanupUsage {
|
|
87
|
+
declare(): CliUsage;
|
|
88
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CleanupUsage = exports.CleanupOptions = exports.DeleteSelection = exports.SELECTION_NUMBERS = exports.SELECTION_NONE = exports.SELECTION_ALL = exports.SELECTION_UNSET = exports.FLAG_INTERACTIVE = exports.FLAG_REPORT = exports.FLAG_DELETE_WORKTREES = exports.FLAG_DELETE_BRANCHES = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
/**
|
|
8
|
+
* `wp-cleanup`'s flags, parsed — the whole decision about what this run deletes, as a value.
|
|
9
|
+
*
|
|
10
|
+
* WHY A CLASS AND NOT A BAG OF BOOLEANS THREADED THROUGH ARGUMENTS: the decision "delete these
|
|
11
|
+
* worktrees / those branches / nothing at all" is ONE thing, made once at the bin, and every method
|
|
12
|
+
* downstream has to agree about it. Passed as separate parameters it becomes four independent
|
|
13
|
+
* arguments that a new call site can get half-right — and half-right, here, means deleting a ref
|
|
14
|
+
* nobody chose. Same argument BuildOptions makes for its single `force` field.
|
|
15
|
+
*/
|
|
16
|
+
// The flag names, shared by the bin (which declares them to CliArgs) and by the parser below, so the
|
|
17
|
+
// spelling a caller is told about and the spelling that is honoured can never drift apart.
|
|
18
|
+
exports.FLAG_DELETE_BRANCHES = '--delete-branches';
|
|
19
|
+
exports.FLAG_DELETE_WORKTREES = '--delete-worktrees';
|
|
20
|
+
exports.FLAG_REPORT = '--report';
|
|
21
|
+
exports.FLAG_INTERACTIVE = '--interactive';
|
|
22
|
+
// What a `--delete-*` flag said. UNSET is "the flag was not passed at all" and is a genuinely
|
|
23
|
+
// different answer from NONE: unset defers to the tty sniff, NONE is a caller saying no out loud.
|
|
24
|
+
exports.SELECTION_UNSET = 'unset';
|
|
25
|
+
exports.SELECTION_ALL = 'all';
|
|
26
|
+
exports.SELECTION_NONE = 'none';
|
|
27
|
+
exports.SELECTION_NUMBERS = 'numbers';
|
|
28
|
+
/**
|
|
29
|
+
* One `--delete-branches=` / `--delete-worktrees=` answer: `all`, `none`, or the numbers printed in
|
|
30
|
+
* the classified block on this run.
|
|
31
|
+
*
|
|
32
|
+
* THE NUMBERING CONTRACT: `numbers` index the block wp-cleanup just printed, 1-based, and an index
|
|
33
|
+
* outside that block is a hard failure rather than a silent skip. A number that lands on the wrong
|
|
34
|
+
* ref is the single way this command can delete something nobody asked for, so "the list moved under
|
|
35
|
+
* me" must stop the run, not quietly delete four of the five refs the caller meant.
|
|
36
|
+
*/
|
|
37
|
+
class DeleteSelection {
|
|
38
|
+
mode;
|
|
39
|
+
numbers;
|
|
40
|
+
flag;
|
|
41
|
+
/**
|
|
42
|
+
* ONE constructor, taking argv's two facts about the flag: was it there, and what did it carry.
|
|
43
|
+
*
|
|
44
|
+
* Parsing in the constructor rather than in a static factory is what keeps this to one spelling —
|
|
45
|
+
* there is no way to hold a DeleteSelection that was never checked. `present === false` is the
|
|
46
|
+
* absent flag (the run falls back to its tty sniff); present with an EMPTY value is an error, not
|
|
47
|
+
* an implicit `all` and not an implicit `none`: both readings are defensible, which is exactly
|
|
48
|
+
* why neither may be guessed at when the outcome is a delete.
|
|
49
|
+
*/
|
|
50
|
+
constructor(flag, present, raw) {
|
|
51
|
+
this.flag = flag;
|
|
52
|
+
this.numbers = [];
|
|
53
|
+
if (!present) {
|
|
54
|
+
this.mode = exports.SELECTION_UNSET;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const value = raw.trim().toLowerCase();
|
|
58
|
+
if (value === '')
|
|
59
|
+
throw this.usage('a value is required.');
|
|
60
|
+
if (value === exports.SELECTION_ALL || value === exports.SELECTION_NONE) {
|
|
61
|
+
this.mode = value;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.mode = exports.SELECTION_NUMBERS;
|
|
65
|
+
this.numbers = this.parseNumbers(value, raw);
|
|
66
|
+
}
|
|
67
|
+
parseNumbers(value, raw) {
|
|
68
|
+
const numbers = [];
|
|
69
|
+
for (const token of value.split(/[\s,]+/)) {
|
|
70
|
+
if (token === '')
|
|
71
|
+
continue;
|
|
72
|
+
const index = Number(token);
|
|
73
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
74
|
+
throw this.usage(`'${raw}' — '${token}' is not one of the numbers printed.`);
|
|
75
|
+
}
|
|
76
|
+
numbers.push(index);
|
|
77
|
+
}
|
|
78
|
+
if (numbers.length === 0)
|
|
79
|
+
throw this.usage(`'${raw}' named no numbers.`);
|
|
80
|
+
return numbers;
|
|
81
|
+
}
|
|
82
|
+
/** Did the caller say anything? An explicit flag ALWAYS beats the terminal sniff. */
|
|
83
|
+
given() {
|
|
84
|
+
return this.mode !== exports.SELECTION_UNSET;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The chosen entries out of the block that was just printed.
|
|
88
|
+
*
|
|
89
|
+
* Range is checked against THAT block: a number past its end means the caller is holding numbers
|
|
90
|
+
* from an older run, and the refs have moved under them. That stops the run.
|
|
91
|
+
*/
|
|
92
|
+
pick(block) {
|
|
93
|
+
if (this.mode === exports.SELECTION_ALL)
|
|
94
|
+
return [...block];
|
|
95
|
+
if (this.mode === exports.SELECTION_NONE || this.mode === exports.SELECTION_UNSET)
|
|
96
|
+
return [];
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const index of this.numbers) {
|
|
99
|
+
if (index > block.length) {
|
|
100
|
+
throw this.usage(`it names [${String(index)}], but the list above has only ${String(block.length)} entr(ies).\n`
|
|
101
|
+
+ 'Those numbers came from a different run. Re-run `pnpm wp-cleanup --report`, read the\n'
|
|
102
|
+
+ 'numbers it prints, and pass those — nothing was deleted from that list.');
|
|
103
|
+
}
|
|
104
|
+
out.push(block[index - 1]);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
usage(detail) {
|
|
109
|
+
return new rules_config_1.CliExitError(2, `❌ ${this.flag}: ${detail}\n\n`
|
|
110
|
+
+ `Usage: ${this.flag}=all | ${this.flag}=none | ${this.flag}=1,3\n`
|
|
111
|
+
+ 'The numbers are the ones wp-cleanup printed in the classified block on the SAME run.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.DeleteSelection = DeleteSelection;
|
|
115
|
+
/**
|
|
116
|
+
* Everything argv said about this cleanup run.
|
|
117
|
+
*
|
|
118
|
+
* `report` prints the full classified report and deletes NOTHING — the print-and-exit case, and the
|
|
119
|
+
* one honest way to get numbers that are still valid on the next command, because a run that deletes
|
|
120
|
+
* nothing cannot renumber anything.
|
|
121
|
+
*
|
|
122
|
+
* `interactive` forces the prompt when stdin is not a tty. It exists because `process.stdin.isTTY`
|
|
123
|
+
* was never a fact about who is standing there, only a proxy: a human running
|
|
124
|
+
* `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the
|
|
125
|
+
* DEFAULT, and any explicit flag beats it.
|
|
126
|
+
*/
|
|
127
|
+
class CleanupOptions {
|
|
128
|
+
branches;
|
|
129
|
+
worktrees;
|
|
130
|
+
report;
|
|
131
|
+
interactive;
|
|
132
|
+
// Every parameter REQUIRED, no defaults — same reasoning as BuildOptions: a defaulted parameter
|
|
133
|
+
// means a call site written before this class grew a field silently keeps the old behaviour, and
|
|
134
|
+
// the old behaviour here is "delete without being told to".
|
|
135
|
+
constructor(branches, worktrees, report, interactive) {
|
|
136
|
+
this.branches = branches;
|
|
137
|
+
this.worktrees = worktrees;
|
|
138
|
+
this.report = report;
|
|
139
|
+
this.interactive = interactive;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.
|
|
143
|
+
* A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.
|
|
144
|
+
*/
|
|
145
|
+
prompts() {
|
|
146
|
+
return this.interactive || process.stdin.isTTY === true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.CleanupOptions = CleanupOptions;
|
|
150
|
+
/**
|
|
151
|
+
* What `wp-cleanup --help` prints, and the ONE list CliArgs validates argv against.
|
|
152
|
+
*
|
|
153
|
+
* It lives here rather than inline in the bin so a spec can assert that every flag this command
|
|
154
|
+
* honours is a flag it also tells you about. A flag that works but is undocumented, or documented but
|
|
155
|
+
* rejected, is the same defect from either side — and the bin itself is a `runMain` call that cannot
|
|
156
|
+
* be imported into a test without executing.
|
|
157
|
+
*/
|
|
158
|
+
let CleanupUsage = class CleanupUsage {
|
|
159
|
+
declare() {
|
|
160
|
+
return new rules_config_1.CliUsage('wp-cleanup', 'Remove worktrees and branches that are provably dead, reap the zero-commit husks, and report the rest.', [
|
|
161
|
+
new rules_config_1.CliFlag(exports.FLAG_DELETE_BRANCHES, 'all | none | 1,3 — which of the classified BRANCHES to delete. The\n'
|
|
162
|
+
+ ' numbers are the ones printed in the same run\'s block.', true),
|
|
163
|
+
new rules_config_1.CliFlag(exports.FLAG_DELETE_WORKTREES, 'all | none | 1,3 — the same, for the classified WORKTREES.', true),
|
|
164
|
+
new rules_config_1.CliFlag(exports.FLAG_REPORT, 'Print the full classified report and exit. Deletes NOTHING — the only\n'
|
|
165
|
+
+ ' run whose numbers are still valid for the next command.'),
|
|
166
|
+
new rules_config_1.CliFlag(exports.FLAG_INTERACTIVE, 'Prompt even when stdin is not a terminal.'),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
exports.CleanupUsage = CleanupUsage;
|
|
171
|
+
exports.CleanupUsage = CleanupUsage = tslib_1.__decorate([
|
|
172
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
173
|
+
], CleanupUsage);
|
|
174
|
+
//# sourceMappingURL=cleanup-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup-options.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-options.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, CliFlag, CliUsage } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * `wp-cleanup`'s flags, parsed — the whole decision about what this run deletes, as a value.\n *\n * WHY A CLASS AND NOT A BAG OF BOOLEANS THREADED THROUGH ARGUMENTS: the decision \"delete these\n * worktrees / those branches / nothing at all\" is ONE thing, made once at the bin, and every method\n * downstream has to agree about it. Passed as separate parameters it becomes four independent\n * arguments that a new call site can get half-right — and half-right, here, means deleting a ref\n * nobody chose. Same argument BuildOptions makes for its single `force` field.\n */\n\n// The flag names, shared by the bin (which declares them to CliArgs) and by the parser below, so the\n// spelling a caller is told about and the spelling that is honoured can never drift apart.\nexport const FLAG_DELETE_BRANCHES = '--delete-branches';\nexport const FLAG_DELETE_WORKTREES = '--delete-worktrees';\nexport const FLAG_REPORT = '--report';\nexport const FLAG_INTERACTIVE = '--interactive';\n\n// What a `--delete-*` flag said. UNSET is \"the flag was not passed at all\" and is a genuinely\n// different answer from NONE: unset defers to the tty sniff, NONE is a caller saying no out loud.\nexport const SELECTION_UNSET = 'unset';\nexport const SELECTION_ALL = 'all';\nexport const SELECTION_NONE = 'none';\nexport const SELECTION_NUMBERS = 'numbers';\n\n/**\n * One `--delete-branches=` / `--delete-worktrees=` answer: `all`, `none`, or the numbers printed in\n * the classified block on this run.\n *\n * THE NUMBERING CONTRACT: `numbers` index the block wp-cleanup just printed, 1-based, and an index\n * outside that block is a hard failure rather than a silent skip. A number that lands on the wrong\n * ref is the single way this command can delete something nobody asked for, so \"the list moved under\n * me\" must stop the run, not quietly delete four of the five refs the caller meant.\n */\nexport class DeleteSelection {\n readonly mode: string;\n readonly numbers: readonly number[];\n private readonly flag: string;\n\n /**\n * ONE constructor, taking argv's two facts about the flag: was it there, and what did it carry.\n *\n * Parsing in the constructor rather than in a static factory is what keeps this to one spelling —\n * there is no way to hold a DeleteSelection that was never checked. `present === false` is the\n * absent flag (the run falls back to its tty sniff); present with an EMPTY value is an error, not\n * an implicit `all` and not an implicit `none`: both readings are defensible, which is exactly\n * why neither may be guessed at when the outcome is a delete.\n */\n constructor(flag: string, present: boolean, raw: string) {\n this.flag = flag;\n this.numbers = [];\n if (!present) {\n this.mode = SELECTION_UNSET;\n return;\n }\n const value = raw.trim().toLowerCase();\n if (value === '') throw this.usage('a value is required.');\n if (value === SELECTION_ALL || value === SELECTION_NONE) {\n this.mode = value;\n return;\n }\n this.mode = SELECTION_NUMBERS;\n this.numbers = this.parseNumbers(value, raw);\n }\n\n private parseNumbers(value: string, raw: string): number[] {\n const numbers: number[] = [];\n for (const token of value.split(/[\\s,]+/)) {\n if (token === '') continue;\n const index = Number(token);\n if (!Number.isInteger(index) || index < 1) {\n throw this.usage(`'${raw}' — '${token}' is not one of the numbers printed.`);\n }\n numbers.push(index);\n }\n if (numbers.length === 0) throw this.usage(`'${raw}' named no numbers.`);\n return numbers;\n }\n\n /** Did the caller say anything? An explicit flag ALWAYS beats the terminal sniff. */\n given(): boolean {\n return this.mode !== SELECTION_UNSET;\n }\n\n /**\n * The chosen entries out of the block that was just printed.\n *\n * Range is checked against THAT block: a number past its end means the caller is holding numbers\n * from an older run, and the refs have moved under them. That stops the run.\n */\n pick<T>(block: readonly T[]): T[] {\n if (this.mode === SELECTION_ALL) return [...block];\n if (this.mode === SELECTION_NONE || this.mode === SELECTION_UNSET) return [];\n const out: T[] = [];\n for (const index of this.numbers) {\n if (index > block.length) {\n throw this.usage(\n `it names [${String(index)}], but the list above has only ${String(block.length)} entr(ies).\\n`\n + 'Those numbers came from a different run. Re-run `pnpm wp-cleanup --report`, read the\\n'\n + 'numbers it prints, and pass those — nothing was deleted from that list.');\n }\n out.push(block[index - 1]);\n }\n return out;\n }\n\n private usage(detail: string): CliExitError {\n return new CliExitError(2,\n `❌ ${this.flag}: ${detail}\\n\\n`\n + `Usage: ${this.flag}=all | ${this.flag}=none | ${this.flag}=1,3\\n`\n + 'The numbers are the ones wp-cleanup printed in the classified block on the SAME run.');\n }\n}\n\n/**\n * Everything argv said about this cleanup run.\n *\n * `report` prints the full classified report and deletes NOTHING — the print-and-exit case, and the\n * one honest way to get numbers that are still valid on the next command, because a run that deletes\n * nothing cannot renumber anything.\n *\n * `interactive` forces the prompt when stdin is not a tty. It exists because `process.stdin.isTTY`\n * was never a fact about who is standing there, only a proxy: a human running\n * `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the\n * DEFAULT, and any explicit flag beats it.\n */\nexport class CleanupOptions {\n readonly branches: DeleteSelection;\n readonly worktrees: DeleteSelection;\n readonly report: boolean;\n readonly interactive: boolean;\n\n // Every parameter REQUIRED, no defaults — same reasoning as BuildOptions: a defaulted parameter\n // means a call site written before this class grew a field silently keeps the old behaviour, and\n // the old behaviour here is \"delete without being told to\".\n constructor(\n branches: DeleteSelection,\n worktrees: DeleteSelection,\n report: boolean,\n interactive: boolean,\n ) {\n this.branches = branches;\n this.worktrees = worktrees;\n this.report = report;\n this.interactive = interactive;\n }\n\n /**\n * Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.\n * A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.\n */\n prompts(): boolean {\n return this.interactive || process.stdin.isTTY === true;\n }\n}\n\n/**\n * What `wp-cleanup --help` prints, and the ONE list CliArgs validates argv against.\n *\n * It lives here rather than inline in the bin so a spec can assert that every flag this command\n * honours is a flag it also tells you about. A flag that works but is undocumented, or documented but\n * rejected, is the same defect from either side — and the bin itself is a `runMain` call that cannot\n * be imported into a test without executing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanupUsage {\n declare(): CliUsage {\n return new CliUsage(\n 'wp-cleanup',\n 'Remove worktrees and branches that are provably dead, reap the zero-commit husks, and report the rest.',\n [\n new CliFlag(FLAG_DELETE_BRANCHES,\n 'all | none | 1,3 — which of the classified BRANCHES to delete. The\\n'\n + ' numbers are the ones printed in the same run\\'s block.', true),\n new CliFlag(FLAG_DELETE_WORKTREES,\n 'all | none | 1,3 — the same, for the classified WORKTREES.', true),\n new CliFlag(FLAG_REPORT,\n 'Print the full classified report and exit. Deletes NOTHING — the only\\n'\n + ' run whose numbers are still valid for the next command.'),\n new CliFlag(FLAG_INTERACTIVE,\n 'Prompt even when stdin is not a terminal.'),\n ]);\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapResult, WorktreeReaper } from '@webpieces/rules-config';
|
|
1
|
+
import { MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapResult, WorktreeReaper, WorktreeService, WorktreeWorkInFlight } from '@webpieces/rules-config';
|
|
2
2
|
/**
|
|
3
3
|
* The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.
|
|
4
4
|
*
|
|
@@ -17,7 +17,8 @@ import { MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapRes
|
|
|
17
17
|
export declare class WorktreeCleanupSection {
|
|
18
18
|
private readonly mergedBranches;
|
|
19
19
|
private readonly reaper;
|
|
20
|
-
|
|
20
|
+
private readonly worktreeService;
|
|
21
|
+
constructor(mergedBranches: MergedBranchesService, reaper: WorktreeReaper, worktreeService: WorktreeService);
|
|
21
22
|
/**
|
|
22
23
|
* FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
|
|
23
24
|
* deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
|
|
@@ -35,6 +36,22 @@ export declare class WorktreeCleanupSection {
|
|
|
35
36
|
* PRUNABLE (already provably dead — it is in the auto-reap list, not this one).
|
|
36
37
|
*/
|
|
37
38
|
promptable(verdicts: DeletableWorktree[]): DeletableWorktree[];
|
|
39
|
+
/**
|
|
40
|
+
* The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or
|
|
41
|
+
* untracked work — with a printed line for each one that is spared.
|
|
42
|
+
*
|
|
43
|
+
* THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits
|
|
44
|
+
* of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed
|
|
45
|
+
* in the last twenty minutes, and the two are indistinguishable by ref alone. `git status
|
|
46
|
+
* --porcelain` is the difference, it is one local spawn, and it fails safe to "dirty".
|
|
47
|
+
*
|
|
48
|
+
* It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and
|
|
49
|
+
* git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the
|
|
50
|
+
* backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody
|
|
51
|
+
* was asked about, which is why the husk path states the spare instead of tripping over it.
|
|
52
|
+
*/
|
|
53
|
+
withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[];
|
|
54
|
+
protected workInFlight(worktreePath: string): WorktreeWorkInFlight;
|
|
38
55
|
reap(repoRoot: string, verb: MutationVerb, targets: DeletableWorktree[], retention: string): WorktreeReapResult;
|
|
39
56
|
report(result: WorktreeReapResult): string;
|
|
40
57
|
private reapedLine;
|
|
@@ -23,9 +23,11 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
|
|
|
23
23
|
let WorktreeCleanupSection = class WorktreeCleanupSection {
|
|
24
24
|
mergedBranches;
|
|
25
25
|
reaper;
|
|
26
|
-
|
|
26
|
+
worktreeService;
|
|
27
|
+
constructor(mergedBranches, reaper, worktreeService) {
|
|
27
28
|
this.mergedBranches = mergedBranches;
|
|
28
29
|
this.reaper = reaper;
|
|
30
|
+
this.worktreeService = worktreeService;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
|
|
@@ -49,7 +51,7 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
|
|
|
49
51
|
*/
|
|
50
52
|
promptable(verdicts) {
|
|
51
53
|
const out = [];
|
|
52
|
-
for (const classification of rules_config_1.
|
|
54
|
+
for (const classification of rules_config_1.ADJUDICATED_CLASSIFICATIONS) {
|
|
53
55
|
for (const tree of verdicts) {
|
|
54
56
|
if (!tree.deletable && tree.classification === classification)
|
|
55
57
|
out.push(tree);
|
|
@@ -57,6 +59,44 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
|
|
|
57
59
|
}
|
|
58
60
|
return out;
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or
|
|
64
|
+
* untracked work — with a printed line for each one that is spared.
|
|
65
|
+
*
|
|
66
|
+
* THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits
|
|
67
|
+
* of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed
|
|
68
|
+
* in the last twenty minutes, and the two are indistinguishable by ref alone. `git status
|
|
69
|
+
* --porcelain` is the difference, it is one local spawn, and it fails safe to "dirty".
|
|
70
|
+
*
|
|
71
|
+
* It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and
|
|
72
|
+
* git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the
|
|
73
|
+
* backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody
|
|
74
|
+
* was asked about, which is why the husk path states the spare instead of tripping over it.
|
|
75
|
+
*/
|
|
76
|
+
withoutUncommitted(husks) {
|
|
77
|
+
const clean = [];
|
|
78
|
+
let spared = '';
|
|
79
|
+
for (const tree of husks) {
|
|
80
|
+
const held = this.workInFlight(tree.path);
|
|
81
|
+
if (held.held) {
|
|
82
|
+
// The REASON is printed verbatim, because "it has uncommitted files" and "git would
|
|
83
|
+
// not tell me" send an operator to two different places.
|
|
84
|
+
spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\n`
|
|
85
|
+
+ ' nothing archives that, so it is left exactly where it is\n';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
clean.push(tree);
|
|
89
|
+
}
|
|
90
|
+
if (spared !== '') {
|
|
91
|
+
process.stdout.write('\nZero-commit worktrees SPARED because work may be in flight in them:\n' + spared);
|
|
92
|
+
}
|
|
93
|
+
return clean;
|
|
94
|
+
}
|
|
95
|
+
// Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no
|
|
96
|
+
// real worktrees on disk.
|
|
97
|
+
workInFlight(worktreePath) {
|
|
98
|
+
return this.worktreeService.workInFlight(worktreePath);
|
|
99
|
+
}
|
|
60
100
|
reap(repoRoot, verb, targets, retention) {
|
|
61
101
|
return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);
|
|
62
102
|
}
|
|
@@ -125,6 +165,7 @@ exports.WorktreeCleanupSection = WorktreeCleanupSection;
|
|
|
125
165
|
exports.WorktreeCleanupSection = WorktreeCleanupSection = tslib_1.__decorate([
|
|
126
166
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
127
167
|
tslib_1.__metadata("design:paramtypes", [rules_config_1.MergedBranchesService,
|
|
128
|
-
rules_config_1.WorktreeReaper
|
|
168
|
+
rules_config_1.WorktreeReaper,
|
|
169
|
+
rules_config_1.WorktreeService])
|
|
129
170
|
], WorktreeCleanupSection);
|
|
130
171
|
//# sourceMappingURL=worktree-cleanup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n PROMPTABLE_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of PROMPTABLE_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += '\\nEvery removal is logged in .webpieces/logs/branch-mutations.log (phase REAP_WORKTREE)\\n'\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n WorktreeService,\n WorktreeWorkInFlight,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n private readonly worktreeService: WorktreeService,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of ADJUDICATED_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or\n * untracked work — with a printed line for each one that is spared.\n *\n * THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits\n * of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed\n * in the last twenty minutes, and the two are indistinguishable by ref alone. `git status\n * --porcelain` is the difference, it is one local spawn, and it fails safe to \"dirty\".\n *\n * It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and\n * git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the\n * backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody\n * was asked about, which is why the husk path states the spare instead of tripping over it.\n */\n withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[] {\n const clean: DeletableWorktree[] = [];\n let spared = '';\n for (const tree of husks) {\n const held = this.workInFlight(tree.path);\n if (held.held) {\n // The REASON is printed verbatim, because \"it has uncommitted files\" and \"git would\n // not tell me\" send an operator to two different places.\n spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\\n`\n + ' nothing archives that, so it is left exactly where it is\\n';\n continue;\n }\n clean.push(tree);\n }\n if (spared !== '') {\n process.stdout.write('\\nZero-commit worktrees SPARED because work may be in flight in them:\\n' + spared);\n }\n return clean;\n }\n\n // Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no\n // real worktrees on disk.\n protected workInFlight(worktreePath: string): WorktreeWorkInFlight {\n return this.worktreeService.workInFlight(worktreePath);\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += '\\nEvery removal is logged in .webpieces/logs/branch-mutations.log (phase REAP_WORKTREE)\\n'\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
|
|
@@ -3,6 +3,7 @@ import { FinishUpdateCommand } from './commands/finish-update-command';
|
|
|
3
3
|
import { StartUpsertPrCommand } from './commands/start-upsert-pr-command';
|
|
4
4
|
import { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';
|
|
5
5
|
import { CleanupCommand } from './commands/cleanup-command';
|
|
6
|
+
import { CleanupOptions } from './commands/cleanup-options';
|
|
6
7
|
import { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';
|
|
7
8
|
import { LandPrCommand } from './commands/land-pr-command';
|
|
8
9
|
import { CheckPrCommand } from './commands/check-pr-command';
|
|
@@ -69,8 +70,11 @@ export declare class PrGateApp {
|
|
|
69
70
|
startUpsertPr(): Promise<void>;
|
|
70
71
|
/** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */
|
|
71
72
|
finishUpsertPr(): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with
|
|
75
|
+
* the exact flags that take it. `options` carries what argv said — see CleanupOptions.
|
|
76
|
+
*/
|
|
77
|
+
cleanup(options: CleanupOptions): Promise<void>;
|
|
74
78
|
/**
|
|
75
79
|
* `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
|
|
76
80
|
* the orphan directories a project move leaves behind. Replaces `git checkout main && git pull
|
|
@@ -115,10 +115,13 @@ let PrGateApp = class PrGateApp {
|
|
|
115
115
|
this.assertNoResolveInProgress('wp-finish-upsert-pr');
|
|
116
116
|
return this.finishUpsertPrCommand.run();
|
|
117
117
|
}
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with
|
|
120
|
+
* the exact flags that take it. `options` carries what argv said — see CleanupOptions.
|
|
121
|
+
*/
|
|
122
|
+
cleanup(options) {
|
|
120
123
|
this.assertNoResolveInProgress('wp-cleanup');
|
|
121
|
-
return this.cleanupCommand.run();
|
|
124
|
+
return this.cleanupCommand.run(options);
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
127
|
* `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
@@ -5,12 +5,20 @@ require("reflect-metadata");
|
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
6
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
7
|
const pr_gate_app_1 = require("./pr-gate-app");
|
|
8
|
+
const cleanup_options_1 = require("./commands/cleanup-options");
|
|
8
9
|
// Composition root: build the container and resolve the app so inversify constructs the whole DAG.
|
|
10
|
+
//
|
|
11
|
+
// WHY THIS BIN TAKES FLAGS AT ALL: the thing that used to decide whether wp-cleanup deletes anything
|
|
12
|
+
// was `process.stdin.isTTY`, which is a guess about who is standing there rather than a fact — a human
|
|
13
|
+
// piping to `tee` has no tty, an agent on a pty has one. The flags in CleanupUsage let the caller who
|
|
14
|
+
// KNOWS say so, and an explicit flag always beats the sniff. See CleanupCommand for the numbering
|
|
15
|
+
// contract the `--delete-*` flags carry.
|
|
9
16
|
(0, rules_config_1.runMain)(async () => {
|
|
10
17
|
// autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)
|
|
11
18
|
const container = new inversify_1.Container({ autobind: true });
|
|
12
|
-
// Reject
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
// Reject bogus flags BEFORE the app touches git — a mistyped `--delete-branchs` must never be
|
|
20
|
+
// silently dropped and then run a cleanup that deletes something else instead.
|
|
21
|
+
const args = container.get(rules_config_1.CliArgs).parse(container.get(cleanup_options_1.CleanupUsage).declare());
|
|
22
|
+
await container.get(pr_gate_app_1.PrGateApp).cleanup(new cleanup_options_1.CleanupOptions(new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_BRANCHES, args.has(cleanup_options_1.FLAG_DELETE_BRANCHES), args.value(cleanup_options_1.FLAG_DELETE_BRANCHES)), new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_WORKTREES, args.has(cleanup_options_1.FLAG_DELETE_WORKTREES), args.value(cleanup_options_1.FLAG_DELETE_WORKTREES)), args.has(cleanup_options_1.FLAG_REPORT), args.has(cleanup_options_1.FLAG_INTERACTIVE)));
|
|
15
23
|
});
|
|
16
24
|
//# sourceMappingURL=wp-cleanup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wp-cleanup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-cleanup.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,
|
|
1
|
+
{"version":3,"file":"wp-cleanup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-cleanup.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAA2D;AAC3D,+CAA0C;AAC1C,gEAQoC;AAEpC,mGAAmG;AACnG,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,sGAAsG;AACtG,kGAAkG;AAClG,yCAAyC;AACzC,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,8FAA8F;IAC9F,+EAA+E;IAC/E,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACjF,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,OAAO,CAAC,IAAI,gCAAc,CACrD,IAAI,iCAAe,CAAC,sCAAoB,EAAE,IAAI,CAAC,GAAG,CAAC,sCAAoB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,sCAAoB,CAAC,CAAC,EAC3G,IAAI,iCAAe,CAAC,uCAAqB,EAAE,IAAI,CAAC,GAAG,CAAC,uCAAqB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,uCAAqB,CAAC,CAAC,EAC9G,IAAI,CAAC,GAAG,CAAC,6BAAW,CAAC,EACrB,IAAI,CAAC,GAAG,CAAC,kCAAgB,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\nimport {\n CleanupOptions,\n CleanupUsage,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n FLAG_INTERACTIVE,\n FLAG_REPORT,\n} from './commands/cleanup-options';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\n//\n// WHY THIS BIN TAKES FLAGS AT ALL: the thing that used to decide whether wp-cleanup deletes anything\n// was `process.stdin.isTTY`, which is a guess about who is standing there rather than a fact — a human\n// piping to `tee` has no tty, an agent on a pty has one. The flags in CleanupUsage let the caller who\n// KNOWS say so, and an explicit flag always beats the sniff. See CleanupCommand for the numbering\n// contract the `--delete-*` flags carry.\nrunMain(async (): Promise<void> => {\n // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)\n const container = new Container({ autobind: true });\n // Reject bogus flags BEFORE the app touches git — a mistyped `--delete-branchs` must never be\n // silently dropped and then run a cleanup that deletes something else instead.\n const args = container.get(CliArgs).parse(container.get(CleanupUsage).declare());\n await container.get(PrGateApp).cleanup(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, args.has(FLAG_DELETE_BRANCHES), args.value(FLAG_DELETE_BRANCHES)),\n new DeleteSelection(FLAG_DELETE_WORKTREES, args.has(FLAG_DELETE_WORKTREES), args.value(FLAG_DELETE_WORKTREES)),\n args.has(FLAG_REPORT),\n args.has(FLAG_INTERACTIVE)));\n});\n"]}
|