@webpieces/pr-gate 0.4.523 → 0.4.524
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/land-pr-command.d.ts +14 -10
- package/src/scripts/commands/land-pr-command.js +20 -21
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/reap-worktree-command.d.ts +36 -0
- package/src/scripts/commands/reap-worktree-command.js +116 -0
- package/src/scripts/commands/reap-worktree-command.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +9 -1
- package/src/scripts/pr-gate-app.js +14 -2
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/landed-worktree-reaper.d.ts +62 -0
- package/src/scripts/workflow/landed-worktree-reaper.js +158 -0
- package/src/scripts/workflow/landed-worktree-reaper.js.map +1 -0
- package/src/scripts/wp-reap-worktree.d.ts +2 -0
- package/src/scripts/wp-reap-worktree.js +23 -0
- package/src/scripts/wp-reap-worktree.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.524",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.524",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { RepoRootFinder, BranchArchiver
|
|
1
|
+
import { RepoRootFinder, BranchArchiver } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BranchNaming } from '../workflow/branch-naming';
|
|
4
|
+
import { LandedWorktreeReaper } from '../workflow/landed-worktree-reaper';
|
|
4
5
|
import { MergeInfoIndex } from '../workflow/merge-info-index';
|
|
5
6
|
import { PrMerger } from '../workflow/pr-merger';
|
|
6
7
|
/**
|
|
@@ -25,21 +26,24 @@ export declare class LandPrCommand {
|
|
|
25
26
|
private readonly prMerger;
|
|
26
27
|
private readonly archiver;
|
|
27
28
|
private readonly mergeInfoIndex;
|
|
28
|
-
private readonly
|
|
29
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, prMerger: PrMerger, archiver: BranchArchiver, mergeInfoIndex: MergeInfoIndex,
|
|
29
|
+
private readonly landedWorktree;
|
|
30
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, prMerger: PrMerger, archiver: BranchArchiver, mergeInfoIndex: MergeInfoIndex, landedWorktree: LandedWorktreeReaper);
|
|
30
31
|
run(): Promise<void>;
|
|
31
32
|
/**
|
|
32
|
-
* What
|
|
33
|
+
* What happens next — which is NOT the same act when you landed from a worktree.
|
|
33
34
|
*
|
|
34
35
|
* Landing from a linked worktree always used to leave a corpse: the merged branch is checked out
|
|
35
36
|
* here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing
|
|
36
|
-
* removed the worktree — so the pair sat there until branch-creation-guard hit its cap.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made
|
|
38
|
+
* worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct
|
|
39
|
+
* and it was the most-skipped step in the flow, because the PR is already landed and the work
|
|
40
|
+
* feels done.
|
|
39
41
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* So the reap is no longer an instruction. This command still refuses to remove the directory it
|
|
43
|
+
* is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted
|
|
44
|
+
* in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that
|
|
45
|
+
* hand-off is not safely achievable the #512 notice is printed unchanged, because an honest
|
|
46
|
+
* limitation beats a command that deletes its own working directory mid-run.
|
|
43
47
|
*/
|
|
44
48
|
private nextStep;
|
|
45
49
|
/**
|
|
@@ -9,6 +9,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
9
9
|
const inversify_1 = require("inversify");
|
|
10
10
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
11
11
|
const branch_naming_1 = require("../workflow/branch-naming");
|
|
12
|
+
const landed_worktree_reaper_1 = require("../workflow/landed-worktree-reaper");
|
|
12
13
|
const merge_info_index_1 = require("../workflow/merge-info-index");
|
|
13
14
|
const pr_merger_1 = require("../workflow/pr-merger");
|
|
14
15
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
@@ -34,15 +35,15 @@ let LandPrCommand = class LandPrCommand {
|
|
|
34
35
|
prMerger;
|
|
35
36
|
archiver;
|
|
36
37
|
mergeInfoIndex;
|
|
37
|
-
|
|
38
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, prMerger, archiver, mergeInfoIndex,
|
|
38
|
+
landedWorktree;
|
|
39
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, prMerger, archiver, mergeInfoIndex, landedWorktree) {
|
|
39
40
|
this.repoRootFinder = repoRootFinder;
|
|
40
41
|
this.aiBranchName = aiBranchName;
|
|
41
42
|
this.branchNaming = branchNaming;
|
|
42
43
|
this.prMerger = prMerger;
|
|
43
44
|
this.archiver = archiver;
|
|
44
45
|
this.mergeInfoIndex = mergeInfoIndex;
|
|
45
|
-
this.
|
|
46
|
+
this.landedWorktree = landedWorktree;
|
|
46
47
|
}
|
|
47
48
|
async run() {
|
|
48
49
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
@@ -87,30 +88,28 @@ let LandPrCommand = class LandPrCommand {
|
|
|
87
88
|
'\n');
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
90
|
-
* What
|
|
91
|
+
* What happens next — which is NOT the same act when you landed from a worktree.
|
|
91
92
|
*
|
|
92
93
|
* Landing from a linked worktree always used to leave a corpse: the merged branch is checked out
|
|
93
94
|
* here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing
|
|
94
|
-
* removed the worktree — so the pair sat there until branch-creation-guard hit its cap.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made
|
|
96
|
+
* worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct
|
|
97
|
+
* and it was the most-skipped step in the flow, because the PR is already landed and the work
|
|
98
|
+
* feels done.
|
|
97
99
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
100
|
+
* So the reap is no longer an instruction. This command still refuses to remove the directory it
|
|
101
|
+
* is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted
|
|
102
|
+
* in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that
|
|
103
|
+
* hand-off is not safely achievable the #512 notice is printed unchanged, because an honest
|
|
104
|
+
* limitation beats a command that deletes its own working directory mid-run.
|
|
101
105
|
*/
|
|
102
106
|
nextStep(repoRoot, base) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
107
|
+
const handoff = this.landedWorktree.plan(repoRoot, base);
|
|
108
|
+
if (handoff === null)
|
|
105
109
|
return ' Next: `pnpm wp-cleanup` to delete the merged branch.\n';
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const from = main ? main.path : '<the primary clone>';
|
|
110
|
-
return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\n'
|
|
111
|
-
+ ` be removed from in here. Run cleanup from the primary clone instead:\n`
|
|
112
|
-
+ ` cd ${from} && pnpm wp-cleanup\n`
|
|
113
|
-
+ ` It archives ${base} as a tag, removes ${here.path}, then deletes the branch.\n`;
|
|
110
|
+
if (!handoff.canReap)
|
|
111
|
+
return this.landedWorktree.manualNotice(handoff);
|
|
112
|
+
return this.landedWorktree.handOff(handoff);
|
|
114
113
|
}
|
|
115
114
|
/**
|
|
116
115
|
* The landed branch's post-merge bookkeeping, in one place:
|
|
@@ -170,7 +169,7 @@ exports.LandPrCommand = LandPrCommand = tslib_1.__decorate([
|
|
|
170
169
|
pr_merger_1.PrMerger,
|
|
171
170
|
rules_config_1.BranchArchiver,
|
|
172
171
|
merge_info_index_1.MergeInfoIndex,
|
|
173
|
-
|
|
172
|
+
landed_worktree_reaper_1.LandedWorktreeReaper])
|
|
174
173
|
], LandPrCommand);
|
|
175
174
|
// The open PR's number + title, as read back from GitHub.
|
|
176
175
|
class PrIdentity {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n Worktree, WorktreeService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and\n * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is\n * missing, the honest answer is that finish has not run — so this fails and says so, rather than\n * inventing a body that would not match the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly worktrees: WorktreeService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n\n if (!fs.existsSync(mergeBodyFile)) {\n throw new InformAiError(\n '\\n' + SEP + '❌ Nothing to land — no rendered merge body\\n' + SEP + '\\n' +\n `Expected the compact squash-commit body at:\\n ${mergeBodyFile}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\\n' +\n 'Its absence means finish has not run on this branch, so there is no reviewed PR to\\n' +\n 'land. Run the gated flow first:\\n\\n' +\n ' pnpm wp-start-upsert-pr # update from main, push, build gate\\n' +\n ' # write .webpieces/review.json\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n\\n' +\n 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\\n' + SEP,\n );\n }\n\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n // MERGE_MODE_AUTO is passed explicitly: running this command IS the intent to merge, so it is\n // not gated on pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand).\n const outcome = this.prMerger.merge(base, `${ref.title} (#${ref.number})`, mergeBodyFile, MERGE_MODE_AUTO);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n const archived = outcome.merged\n ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n archived +\n (outcome.merged ? this.nextStep(repoRoot, base) : '') +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * What to run next — which is NOT the same sentence when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. wp-cleanup\n * now reaps dead worktrees, but it cannot reap the one it is RUNNING IN (removing your own cwd\n * mid-command is a self-destruct), so the instruction has to say where to run it from.\n *\n * We deliberately do NOT remove the worktree here. This command is executing inside it, and a\n * process that deletes the directory underneath itself is exactly the thing every safety rail in\n * WorktreeReaper exists to refuse.\n */\n private nextStep(repoRoot: string, base: string): string {\n const here = this.worktrees.currentWorktree(repoRoot);\n if (here === null || here.isMain || here.branch !== base) {\n return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n }\n const main = this.worktrees.listWorktrees(repoRoot)\n .find((tree: Worktree): boolean => tree.isMain);\n const from = main ? main.path : '<the primary clone>';\n return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\\n'\n + ` be removed from in here. Run cleanup from the primary clone instead:\\n`\n + ` cd ${from} && pnpm wp-cleanup\\n`\n + ` It archives ${base} as a tag, removes ${here.path}, then deletes the branch.\\n`;\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number + title for this head branch, or null when there is none. The TITLE comes\n // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even\n // if review.json was edited afterwards.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '');\n }\n}\n\n// The open PR's number + title, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n\n constructor(number: string, title: string) {\n this.number = number;\n this.title = title;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and\n * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is\n * missing, the honest answer is that finish has not run — so this fails and says so, rather than\n * inventing a body that would not match the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n\n if (!fs.existsSync(mergeBodyFile)) {\n throw new InformAiError(\n '\\n' + SEP + '❌ Nothing to land — no rendered merge body\\n' + SEP + '\\n' +\n `Expected the compact squash-commit body at:\\n ${mergeBodyFile}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\\n' +\n 'Its absence means finish has not run on this branch, so there is no reviewed PR to\\n' +\n 'land. Run the gated flow first:\\n\\n' +\n ' pnpm wp-start-upsert-pr # update from main, push, build gate\\n' +\n ' # write .webpieces/review.json\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n\\n' +\n 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\\n' + SEP,\n );\n }\n\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n // MERGE_MODE_AUTO is passed explicitly: running this command IS the intent to merge, so it is\n // not gated on pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand).\n const outcome = this.prMerger.merge(base, `${ref.title} (#${ref.number})`, mergeBodyFile, MERGE_MODE_AUTO);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n const archived = outcome.merged\n ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n archived +\n (outcome.merged ? this.nextStep(repoRoot, base) : '') +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number + title for this head branch, or null when there is none. The TITLE comes\n // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even\n // if review.json was edited afterwards.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '');\n }\n}\n\n// The open PR's number + title, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n\n constructor(number: string, title: string) {\n this.number = number;\n this.title = title;\n }\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
2
|
+
import { WorktreeCleanupSection } from './worktree-cleanup';
|
|
3
|
+
/**
|
|
4
|
+
* The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the
|
|
5
|
+
* primary clone.
|
|
6
|
+
*
|
|
7
|
+
* It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`
|
|
8
|
+
* is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already
|
|
9
|
+
* identified corpse without also reaping every other agent's worktree as a side effect of landing one
|
|
10
|
+
* PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a
|
|
11
|
+
* `process.chdir`.
|
|
12
|
+
*
|
|
13
|
+
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.
|
|
14
|
+
* The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs
|
|
15
|
+
* `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never
|
|
16
|
+
* because a caller said so. That is what keeps "never remove a tree still holding unmerged work" true
|
|
17
|
+
* even if the parent is wrong, out of date, or someone runs this by hand.
|
|
18
|
+
*
|
|
19
|
+
* The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the
|
|
20
|
+
* verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper
|
|
21
|
+
* refuses it a second time by name. Neither rail is load-bearing alone.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ReapWorktreeCommand {
|
|
24
|
+
private readonly repoRootFinder;
|
|
25
|
+
private readonly worktreeSection;
|
|
26
|
+
constructor(repoRootFinder: RepoRootFinder, worktreeSection: WorktreeCleanupSection);
|
|
27
|
+
run(args: string[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are
|
|
30
|
+
* printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a
|
|
31
|
+
* successful landing would report a landed PR as a failed command.
|
|
32
|
+
*/
|
|
33
|
+
private resolveTarget;
|
|
34
|
+
private render;
|
|
35
|
+
private parse;
|
|
36
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReapWorktreeCommand = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
const worktree_cleanup_1 = require("./worktree-cleanup");
|
|
9
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
10
|
+
/**
|
|
11
|
+
* The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the
|
|
12
|
+
* primary clone.
|
|
13
|
+
*
|
|
14
|
+
* It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`
|
|
15
|
+
* is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already
|
|
16
|
+
* identified corpse without also reaping every other agent's worktree as a side effect of landing one
|
|
17
|
+
* PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a
|
|
18
|
+
* `process.chdir`.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.
|
|
21
|
+
* The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs
|
|
22
|
+
* `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never
|
|
23
|
+
* because a caller said so. That is what keeps "never remove a tree still holding unmerged work" true
|
|
24
|
+
* even if the parent is wrong, out of date, or someone runs this by hand.
|
|
25
|
+
*
|
|
26
|
+
* The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the
|
|
27
|
+
* verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper
|
|
28
|
+
* refuses it a second time by name. Neither rail is load-bearing alone.
|
|
29
|
+
*/
|
|
30
|
+
let ReapWorktreeCommand = class ReapWorktreeCommand {
|
|
31
|
+
repoRootFinder;
|
|
32
|
+
worktreeSection;
|
|
33
|
+
constructor(repoRootFinder, worktreeSection) {
|
|
34
|
+
this.repoRootFinder = repoRootFinder;
|
|
35
|
+
this.worktreeSection = worktreeSection;
|
|
36
|
+
}
|
|
37
|
+
async run(args) {
|
|
38
|
+
await Promise.resolve();
|
|
39
|
+
const request = this.parse(args);
|
|
40
|
+
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
41
|
+
const target = this.resolveTarget(repoRoot, request);
|
|
42
|
+
if (target === null)
|
|
43
|
+
return;
|
|
44
|
+
const retention = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.landPr.branchRetention;
|
|
45
|
+
const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);
|
|
46
|
+
process.stdout.write(this.render(result));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are
|
|
50
|
+
* printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a
|
|
51
|
+
* successful landing would report a landed PR as a failed command.
|
|
52
|
+
*/
|
|
53
|
+
resolveTarget(repoRoot, request) {
|
|
54
|
+
const wanted = path.resolve(request.worktreePath);
|
|
55
|
+
const target = this.worktreeSection.verdicts(repoRoot)
|
|
56
|
+
.find((tree) => path.resolve(tree.path) === wanted);
|
|
57
|
+
if (target === undefined) {
|
|
58
|
+
process.stdout.write(`\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\n`
|
|
59
|
+
+ ' primary clone, or already gone). Nothing removed.\n');
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// The branch moved under us between landing and reaping — somebody checked something else out
|
|
63
|
+
// there. Whatever that is, it is not the corpse we were asked to bury.
|
|
64
|
+
if (target.branch !== request.branch) {
|
|
65
|
+
process.stdout.write(`\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\n`
|
|
66
|
+
+ ' Refusing to remove a worktree whose branch changed since the PR landed.\n');
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!target.deletable) {
|
|
70
|
+
process.stdout.write(`\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\n`
|
|
71
|
+
+ ' Refusing to remove a worktree that may still hold unmerged work.\n');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return target;
|
|
75
|
+
}
|
|
76
|
+
// The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's
|
|
77
|
+
// point of view "nothing happened" is a whole answer. Here it never is: we asked for exactly one.
|
|
78
|
+
render(result) {
|
|
79
|
+
if (result.reaped.length === 0 && result.failed.length === 0) {
|
|
80
|
+
let out = '\n' + SEP + '🛑 Nothing removed\n' + SEP + '\n';
|
|
81
|
+
for (const tree of result.spared)
|
|
82
|
+
out += ` · ${tree.path} — ${tree.reason}\n`;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
return this.worktreeSection.report(result);
|
|
86
|
+
}
|
|
87
|
+
// `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller
|
|
88
|
+
// is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.
|
|
89
|
+
parse(args) {
|
|
90
|
+
const worktreePath = args[0] ?? '';
|
|
91
|
+
const branch = args[1] ?? '';
|
|
92
|
+
if (worktreePath === '' || branch === '') {
|
|
93
|
+
throw new rules_config_1.InformAiError('\n' + SEP + '❌ wp-reap-worktree: missing arguments\n' + SEP + '\n'
|
|
94
|
+
+ 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\n\n'
|
|
95
|
+
+ 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\n'
|
|
96
|
+
+ 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\n' + SEP);
|
|
97
|
+
}
|
|
98
|
+
return new ReapRequest(worktreePath, branch);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
exports.ReapWorktreeCommand = ReapWorktreeCommand;
|
|
102
|
+
exports.ReapWorktreeCommand = ReapWorktreeCommand = tslib_1.__decorate([
|
|
103
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
104
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
105
|
+
worktree_cleanup_1.WorktreeCleanupSection])
|
|
106
|
+
], ReapWorktreeCommand);
|
|
107
|
+
// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.
|
|
108
|
+
class ReapRequest {
|
|
109
|
+
worktreePath;
|
|
110
|
+
branch;
|
|
111
|
+
constructor(worktreePath, branch) {
|
|
112
|
+
this.worktreePath = worktreePath;
|
|
113
|
+
this.branch = branch;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=reap-worktree-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n ) {}\n\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) return;\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(result));\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n const target = this.worktreeSection.verdicts(repoRoot)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
|
|
@@ -6,6 +6,7 @@ import { CleanupCommand } from './commands/cleanup-command';
|
|
|
6
6
|
import { LandPrCommand } from './commands/land-pr-command';
|
|
7
7
|
import { CheckPrCommand } from './commands/check-pr-command';
|
|
8
8
|
import { ReviewUpsertPrCommand } from './commands/review-upsert-pr-command';
|
|
9
|
+
import { ReapWorktreeCommand } from './commands/reap-worktree-command';
|
|
9
10
|
/**
|
|
10
11
|
* The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
|
|
11
12
|
* classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
|
|
@@ -21,7 +22,14 @@ export declare class PrGateApp {
|
|
|
21
22
|
private readonly landPrCommand;
|
|
22
23
|
private readonly checkPrCommand;
|
|
23
24
|
private readonly reviewUpsertPrCommand;
|
|
24
|
-
|
|
25
|
+
private readonly reapWorktreeCommand;
|
|
26
|
+
constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand);
|
|
27
|
+
/**
|
|
28
|
+
* INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.
|
|
29
|
+
* `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be
|
|
30
|
+
* reaped by a process that is not standing in it.
|
|
31
|
+
*/
|
|
32
|
+
reapWorktree(args: string[]): Promise<void>;
|
|
25
33
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
26
34
|
startUpdate(): Promise<void>;
|
|
27
35
|
/** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */
|
|
@@ -12,6 +12,7 @@ const cleanup_command_1 = require("./commands/cleanup-command");
|
|
|
12
12
|
const land_pr_command_1 = require("./commands/land-pr-command");
|
|
13
13
|
const check_pr_command_1 = require("./commands/check-pr-command");
|
|
14
14
|
const review_upsert_pr_command_1 = require("./commands/review-upsert-pr-command");
|
|
15
|
+
const reap_worktree_command_1 = require("./commands/reap-worktree-command");
|
|
15
16
|
/**
|
|
16
17
|
* The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
|
|
17
18
|
* classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
|
|
@@ -27,7 +28,8 @@ let PrGateApp = class PrGateApp {
|
|
|
27
28
|
landPrCommand;
|
|
28
29
|
checkPrCommand;
|
|
29
30
|
reviewUpsertPrCommand;
|
|
30
|
-
|
|
31
|
+
reapWorktreeCommand;
|
|
32
|
+
constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand) {
|
|
31
33
|
this.startUpdateCommand = startUpdateCommand;
|
|
32
34
|
this.finishUpdateCommand = finishUpdateCommand;
|
|
33
35
|
this.startUpsertPrCommand = startUpsertPrCommand;
|
|
@@ -36,6 +38,15 @@ let PrGateApp = class PrGateApp {
|
|
|
36
38
|
this.landPrCommand = landPrCommand;
|
|
37
39
|
this.checkPrCommand = checkPrCommand;
|
|
38
40
|
this.reviewUpsertPrCommand = reviewUpsertPrCommand;
|
|
41
|
+
this.reapWorktreeCommand = reapWorktreeCommand;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.
|
|
45
|
+
* `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be
|
|
46
|
+
* reaped by a process that is not standing in it.
|
|
47
|
+
*/
|
|
48
|
+
reapWorktree(args) {
|
|
49
|
+
return this.reapWorktreeCommand.run(args);
|
|
39
50
|
}
|
|
40
51
|
/** `wp-start-update`: 3-point squash-update from main (no PR). */
|
|
41
52
|
startUpdate() {
|
|
@@ -85,6 +96,7 @@ exports.PrGateApp = PrGateApp = tslib_1.__decorate([
|
|
|
85
96
|
cleanup_command_1.CleanupCommand,
|
|
86
97
|
land_pr_command_1.LandPrCommand,
|
|
87
98
|
check_pr_command_1.CheckPrCommand,
|
|
88
|
-
review_upsert_pr_command_1.ReviewUpsertPrCommand
|
|
99
|
+
review_upsert_pr_command_1.ReviewUpsertPrCommand,
|
|
100
|
+
reap_worktree_command_1.ReapWorktreeCommand])
|
|
89
101
|
], PrGateApp);
|
|
90
102
|
//# sourceMappingURL=pr-gate-app.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA2D;AAC3D,kEAA6D;AAC7D,kFAA4E;
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA2D;AAC3D,kEAA6D;AAC7D,kFAA4E;AAC5E,4EAAuE;AAEvE;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IATrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC;QARxC,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;IAC1D,CAAC;IAEJ;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,mGAAmG;IACnG,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,MAAM;QACF,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AAjEY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACf,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;GAVpD,SAAS,CAiErB","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 { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\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 landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\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 /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\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 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 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 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 return this.cleanupCommand.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 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(): Promise<void> {\n return this.reviewUpsertPrCommand.run();\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"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { WorktreeService } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which
|
|
4
|
+
* directory, which branch, where the reap must run FROM, and — when it cannot run — why not.
|
|
5
|
+
*/
|
|
6
|
+
export declare class WorktreeReapHandoff {
|
|
7
|
+
/** The linked worktree holding the branch that was just landed. */
|
|
8
|
+
readonly worktreePath: string;
|
|
9
|
+
readonly branch: string;
|
|
10
|
+
/** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */
|
|
11
|
+
readonly primaryPath: string;
|
|
12
|
+
/** The compiled entry the child runs. '' when it could not be located on disk. */
|
|
13
|
+
readonly entryScript: string;
|
|
14
|
+
/** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */
|
|
15
|
+
readonly blockedBecause: string;
|
|
16
|
+
/** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */
|
|
17
|
+
readonly canReap: boolean;
|
|
18
|
+
constructor(worktreePath: string, branch: string, primaryPath: string, entryScript: string, blockedBecause: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class LandedWorktreeReaper {
|
|
21
|
+
private readonly worktrees;
|
|
22
|
+
constructor(worktrees: WorktreeService);
|
|
23
|
+
/**
|
|
24
|
+
* Is there a worktree to reap at all, and can the hand-off run?
|
|
25
|
+
*
|
|
26
|
+
* `null` means "nothing to do here" — we are in the primary clone, or in a worktree that does not
|
|
27
|
+
* hold the branch that just landed. That is the ordinary `pnpm wp-cleanup` case and needs no
|
|
28
|
+
* special sentence. A non-null handoff with `canReap === false` means there IS a corpse but the
|
|
29
|
+
* re-exec is not safely achievable, which is the case that must keep the manual notice.
|
|
30
|
+
*/
|
|
31
|
+
plan(repoRoot: string, landedBranch: string): WorktreeReapHandoff | null;
|
|
32
|
+
/**
|
|
33
|
+
* Run the reap in a child process rooted in the primary clone, and render what happened.
|
|
34
|
+
*
|
|
35
|
+
* stdin is `ignore` deliberately: the child must never be able to ask a question. A prompt printed
|
|
36
|
+
* into a landing recap nobody is watching is not consent, and this reap is authorised by the merge
|
|
37
|
+
* that just succeeded, not by an answer.
|
|
38
|
+
*/
|
|
39
|
+
handOff(handoff: WorktreeReapHandoff): string;
|
|
40
|
+
/**
|
|
41
|
+
* The #512 notice, kept verbatim in spirit for every case the re-exec cannot cover. An honest
|
|
42
|
+
* limitation beats a command that deletes its own working directory mid-run.
|
|
43
|
+
*/
|
|
44
|
+
manualNotice(handoff: WorktreeReapHandoff): string;
|
|
45
|
+
/**
|
|
46
|
+
* The one thing a human or agent MUST be told after a successful reap: the shell they typed this
|
|
47
|
+
* into is now sitting in a directory that no longer exists. Every relative path from here on is a
|
|
48
|
+
* mystery ENOENT unless they move.
|
|
49
|
+
*/
|
|
50
|
+
private afterReap;
|
|
51
|
+
/**
|
|
52
|
+
* WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this
|
|
53
|
+
* package runs from `.ts` sources and the compiled sibling does not exist.
|
|
54
|
+
*
|
|
55
|
+
* Resolved from `__dirname`, i.e. from THIS package's own installed files, NOT by remapping a path
|
|
56
|
+
* into the primary clone. The primary clone may have a different @webpieces/pr-gate version
|
|
57
|
+
* installed, and re-exec'ing a build that predates this feature would silently do something else.
|
|
58
|
+
* The files are read into the child before it removes anything, so the directory going away
|
|
59
|
+
* underneath them afterwards is harmless.
|
|
60
|
+
*/
|
|
61
|
+
protected reapEntryScript(): string;
|
|
62
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LandedWorktreeReaper = exports.WorktreeReapHandoff = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
8
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
9
|
+
const inversify_1 = require("inversify");
|
|
10
|
+
/**
|
|
11
|
+
* The RE-EXEC half of `wp-land-pr`: reap the worktree the command is standing in, from a process that
|
|
12
|
+
* is NOT standing in it.
|
|
13
|
+
*
|
|
14
|
+
* THE PROBLEM. `wp-land-pr` squash-merges the branch checked out in the worktree it runs in. The
|
|
15
|
+
* moment that lands, both the branch and the directory are dead — but removing them from in here means
|
|
16
|
+
* deleting the files underneath the running process, which is exactly what every rail in WorktreeReaper
|
|
17
|
+
* refuses. #512 took this to an honest halfway point: print `cd <primary> && pnpm wp-cleanup` and stop.
|
|
18
|
+
* Correct, and the single most-skipped step in the whole flow, because the PR is already landed and the
|
|
19
|
+
* work FEELS done. Every skipped one leaves a corpse, and corpses are what trip branch-creation-guard.
|
|
20
|
+
*
|
|
21
|
+
* THE FIX. Do not reap in this process at all — hand the reap to a CHILD process whose `cwd` is the
|
|
22
|
+
* PRIMARY CLONE. The child's cwd is a directory nobody is deleting, so `git worktree remove` is an
|
|
23
|
+
* ordinary removal of somebody else's directory, and every existing rail still holds inside the child
|
|
24
|
+
* (it refuses its own cwd, which is now the primary clone, and it refuses the primary clone by name).
|
|
25
|
+
*
|
|
26
|
+
* WHY `WorktreeService` AND NOT `EffectiveTreeResolver`. EffectiveTreeResolver (ai-hook-rules) answers
|
|
27
|
+
* "which tree does this BASH COMMAND STRING act on" — it takes a command, resolves its leading `cd`
|
|
28
|
+
* run, and classifies foreign/outside repos for the PreToolUse guards. There is no command string
|
|
29
|
+
* here, pr-gate does not (and must not) depend on ai-hook-rules, and the only question being asked is
|
|
30
|
+
* "which of git's worktrees is the primary clone, and which one holds this branch". That is
|
|
31
|
+
* WorktreeService's whole job, and it is already the authority WorktreeReaper itself uses to decide
|
|
32
|
+
* what is protected — using a second resolver would risk the two disagreeing about which tree we are
|
|
33
|
+
* in, which for a directory deletion is the one disagreement nobody survives.
|
|
34
|
+
*
|
|
35
|
+
* WHY A CHILD AND NOT AN IN-PROCESS `chdir`. `process.chdir(primary)` would leave this node process's
|
|
36
|
+
* own module graph rooted in a directory that is about to be deleted, and its lazily-resolved requires
|
|
37
|
+
* with it. The child is spawned from THIS package's own files (same version — no skew with whatever
|
|
38
|
+
* the primary clone has installed), and it finishes loading them before it removes anything.
|
|
39
|
+
*/
|
|
40
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
41
|
+
/**
|
|
42
|
+
* Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which
|
|
43
|
+
* directory, which branch, where the reap must run FROM, and — when it cannot run — why not.
|
|
44
|
+
*/
|
|
45
|
+
class WorktreeReapHandoff {
|
|
46
|
+
/** The linked worktree holding the branch that was just landed. */
|
|
47
|
+
worktreePath;
|
|
48
|
+
branch;
|
|
49
|
+
/** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */
|
|
50
|
+
primaryPath;
|
|
51
|
+
/** The compiled entry the child runs. '' when it could not be located on disk. */
|
|
52
|
+
entryScript;
|
|
53
|
+
/** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */
|
|
54
|
+
blockedBecause;
|
|
55
|
+
/** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */
|
|
56
|
+
canReap;
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
58
|
+
constructor(worktreePath, branch, primaryPath, entryScript, blockedBecause) {
|
|
59
|
+
this.worktreePath = worktreePath;
|
|
60
|
+
this.branch = branch;
|
|
61
|
+
this.primaryPath = primaryPath;
|
|
62
|
+
this.entryScript = entryScript;
|
|
63
|
+
this.blockedBecause = blockedBecause;
|
|
64
|
+
this.canReap = blockedBecause === '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.WorktreeReapHandoff = WorktreeReapHandoff;
|
|
68
|
+
let LandedWorktreeReaper = class LandedWorktreeReaper {
|
|
69
|
+
worktrees;
|
|
70
|
+
constructor(worktrees) {
|
|
71
|
+
this.worktrees = worktrees;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Is there a worktree to reap at all, and can the hand-off run?
|
|
75
|
+
*
|
|
76
|
+
* `null` means "nothing to do here" — we are in the primary clone, or in a worktree that does not
|
|
77
|
+
* hold the branch that just landed. That is the ordinary `pnpm wp-cleanup` case and needs no
|
|
78
|
+
* special sentence. A non-null handoff with `canReap === false` means there IS a corpse but the
|
|
79
|
+
* re-exec is not safely achievable, which is the case that must keep the manual notice.
|
|
80
|
+
*/
|
|
81
|
+
plan(repoRoot, landedBranch) {
|
|
82
|
+
const here = this.worktrees.currentWorktree(repoRoot);
|
|
83
|
+
if (here === null || here.isMain || here.branch !== landedBranch)
|
|
84
|
+
return null;
|
|
85
|
+
const primary = this.worktrees.listWorktrees(repoRoot)
|
|
86
|
+
.find((tree) => tree.isMain);
|
|
87
|
+
if (primary === undefined) {
|
|
88
|
+
return new WorktreeReapHandoff(here.path, landedBranch, '<the primary clone>', '', 'git did not report a primary clone, so there is no safe directory to reap from');
|
|
89
|
+
}
|
|
90
|
+
const entry = this.reapEntryScript();
|
|
91
|
+
if (entry === '') {
|
|
92
|
+
return new WorktreeReapHandoff(here.path, landedBranch, primary.path, '', 'the reap entry point is not on disk (this package is running unbuilt)');
|
|
93
|
+
}
|
|
94
|
+
return new WorktreeReapHandoff(here.path, landedBranch, primary.path, entry, '');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run the reap in a child process rooted in the primary clone, and render what happened.
|
|
98
|
+
*
|
|
99
|
+
* stdin is `ignore` deliberately: the child must never be able to ask a question. A prompt printed
|
|
100
|
+
* into a landing recap nobody is watching is not consent, and this reap is authorised by the merge
|
|
101
|
+
* that just succeeded, not by an answer.
|
|
102
|
+
*/
|
|
103
|
+
handOff(handoff) {
|
|
104
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [handoff.entryScript, handoff.worktreePath, handoff.branch], { cwd: handoff.primaryPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
105
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd();
|
|
106
|
+
const head = '\n' + SEP + `🌲 Reaping this worktree from ${handoff.primaryPath}\n` + SEP + '\n'
|
|
107
|
+
+ ` The branch just landed, so this directory is dead. The reap runs in a child process\n`
|
|
108
|
+
+ ` whose cwd is the primary clone — nothing deletes the directory it is standing in.\n\n`;
|
|
109
|
+
if (result.status !== 0) {
|
|
110
|
+
return head + (output !== '' ? output + '\n' : '')
|
|
111
|
+
+ `\n ⚠️ The reap did not complete (exit ${String(result.status ?? -1)}). Nothing was forced.\n`
|
|
112
|
+
+ this.manualNotice(handoff);
|
|
113
|
+
}
|
|
114
|
+
return head + (output !== '' ? output + '\n' : '') + this.afterReap(handoff);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The #512 notice, kept verbatim in spirit for every case the re-exec cannot cover. An honest
|
|
118
|
+
* limitation beats a command that deletes its own working directory mid-run.
|
|
119
|
+
*/
|
|
120
|
+
manualNotice(handoff) {
|
|
121
|
+
const why = handoff.blockedBecause !== '' ? ` (${handoff.blockedBecause})\n` : '';
|
|
122
|
+
return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\n'
|
|
123
|
+
+ ' be removed from in here. Run cleanup from the primary clone instead:\n'
|
|
124
|
+
+ ` cd ${handoff.primaryPath} && pnpm wp-cleanup\n`
|
|
125
|
+
+ why
|
|
126
|
+
+ ` It archives ${handoff.branch} as a tag, removes ${handoff.worktreePath}, then deletes the branch.\n`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The one thing a human or agent MUST be told after a successful reap: the shell they typed this
|
|
130
|
+
* into is now sitting in a directory that no longer exists. Every relative path from here on is a
|
|
131
|
+
* mystery ENOENT unless they move.
|
|
132
|
+
*/
|
|
133
|
+
afterReap(handoff) {
|
|
134
|
+
return `\n ⚠️ ${handoff.worktreePath} NO LONGER EXISTS — your shell is standing in a deleted\n`
|
|
135
|
+
+ ' directory, and every following command will fail until you move:\n'
|
|
136
|
+
+ ` cd ${handoff.primaryPath}\n`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this
|
|
140
|
+
* package runs from `.ts` sources and the compiled sibling does not exist.
|
|
141
|
+
*
|
|
142
|
+
* Resolved from `__dirname`, i.e. from THIS package's own installed files, NOT by remapping a path
|
|
143
|
+
* into the primary clone. The primary clone may have a different @webpieces/pr-gate version
|
|
144
|
+
* installed, and re-exec'ing a build that predates this feature would silently do something else.
|
|
145
|
+
* The files are read into the child before it removes anything, so the directory going away
|
|
146
|
+
* underneath them afterwards is harmless.
|
|
147
|
+
*/
|
|
148
|
+
reapEntryScript() {
|
|
149
|
+
const entry = path.join(__dirname, '..', 'wp-reap-worktree.js');
|
|
150
|
+
return fs.existsSync(entry) ? entry : '';
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
exports.LandedWorktreeReaper = LandedWorktreeReaper;
|
|
154
|
+
exports.LandedWorktreeReaper = LandedWorktreeReaper = tslib_1.__decorate([
|
|
155
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
156
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.WorktreeService])
|
|
157
|
+
], LandedWorktreeReaper);
|
|
158
|
+
//# sourceMappingURL=landed-worktree-reaper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landed-worktree-reaper.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/landed-worktree-reaper.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,+CAAyB;AACzB,mDAA6B;AAC7B,0DAAoE;AACpE,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;GAGG;AACH,MAAa,mBAAmB;IAC5B,mEAAmE;IAC1D,YAAY,CAAS;IACrB,MAAM,CAAS;IACxB,gGAAgG;IACvF,WAAW,CAAS;IAC7B,kFAAkF;IACzE,WAAW,CAAS;IAC7B,kGAAkG;IACzF,cAAc,CAAS;IAChC,8FAA8F;IACrF,OAAO,CAAU;IAE1B,yDAAyD;IACzD,YACI,YAAoB,EACpB,MAAc,EACd,WAAmB,EACnB,WAAmB,EACnB,cAAsB;QAEtB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,OAAO,GAAG,cAAc,KAAK,EAAE,CAAC;IACzC,CAAC;CACJ;AA5BD,kDA4BC;AAGM,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACA;IAA7B,YAA6B,SAA0B;QAA1B,cAAS,GAAT,SAAS,CAAiB;IAAG,CAAC;IAE3D;;;;;;;OAOG;IACH,IAAI,CAAC,QAAgB,EAAE,YAAoB;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY;YAAE,OAAO,IAAI,CAAC;QAE9E,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC;aACjD,IAAI,CAAC,CAAC,IAAc,EAAW,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACxB,OAAO,IAAI,mBAAmB,CAC1B,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,qBAAqB,EAAE,EAAE,EAClD,gFAAgF,CAAC,CAAC;QAC1F,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACrC,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACf,OAAO,IAAI,mBAAmB,CAC1B,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EACzC,uEAAuE,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,OAA4B;QAChC,MAAM,MAAM,GAAG,IAAA,yBAAS,EACpB,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,EAC7E,EAAE,GAAG,EAAE,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAEvF,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC;QACxE,MAAM,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,iCAAiC,OAAO,CAAC,WAAW,IAAI,GAAG,GAAG,GAAG,IAAI;cACzF,0FAA0F;cAC1F,0FAA0F,CAAC;QAEjG,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;kBAC5C,4CAA4C,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,0BAA0B;kBACjG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACjF,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,OAA4B;QACrC,MAAM,GAAG,GAAG,OAAO,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC,CAAC,aAAa,OAAO,CAAC,cAAc,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,OAAO,4FAA4F;cAC7F,iFAAiF;cACjF,iBAAiB,OAAO,CAAC,WAAW,uBAAuB;cAC3D,GAAG;cACH,wBAAwB,OAAO,CAAC,MAAM,sBAAsB,OAAO,CAAC,YAAY,8BAA8B,CAAC;IACzH,CAAC;IAED;;;;OAIG;IACK,SAAS,CAAC,OAA4B;QAC1C,OAAO,YAAY,OAAO,CAAC,YAAY,2DAA2D;cAC5F,2EAA2E;cAC3E,iBAAiB,OAAO,CAAC,WAAW,IAAI,CAAC;IACnD,CAAC;IAED;;;;;;;;;OASG;IACO,eAAe;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,qBAAqB,CAAC,CAAC;QAChE,OAAO,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,CAAC;CACJ,CAAA;AA/FY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEG,8BAAe;GAD9C,oBAAoB,CA+FhC","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { Worktree, WorktreeService } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The RE-EXEC half of `wp-land-pr`: reap the worktree the command is standing in, from a process that\n * is NOT standing in it.\n *\n * THE PROBLEM. `wp-land-pr` squash-merges the branch checked out in the worktree it runs in. The\n * moment that lands, both the branch and the directory are dead — but removing them from in here means\n * deleting the files underneath the running process, which is exactly what every rail in WorktreeReaper\n * refuses. #512 took this to an honest halfway point: print `cd <primary> && pnpm wp-cleanup` and stop.\n * Correct, and the single most-skipped step in the whole flow, because the PR is already landed and the\n * work FEELS done. Every skipped one leaves a corpse, and corpses are what trip branch-creation-guard.\n *\n * THE FIX. Do not reap in this process at all — hand the reap to a CHILD process whose `cwd` is the\n * PRIMARY CLONE. The child's cwd is a directory nobody is deleting, so `git worktree remove` is an\n * ordinary removal of somebody else's directory, and every existing rail still holds inside the child\n * (it refuses its own cwd, which is now the primary clone, and it refuses the primary clone by name).\n *\n * WHY `WorktreeService` AND NOT `EffectiveTreeResolver`. EffectiveTreeResolver (ai-hook-rules) answers\n * \"which tree does this BASH COMMAND STRING act on\" — it takes a command, resolves its leading `cd`\n * run, and classifies foreign/outside repos for the PreToolUse guards. There is no command string\n * here, pr-gate does not (and must not) depend on ai-hook-rules, and the only question being asked is\n * \"which of git's worktrees is the primary clone, and which one holds this branch\". That is\n * WorktreeService's whole job, and it is already the authority WorktreeReaper itself uses to decide\n * what is protected — using a second resolver would risk the two disagreeing about which tree we are\n * in, which for a directory deletion is the one disagreement nobody survives.\n *\n * WHY A CHILD AND NOT AN IN-PROCESS `chdir`. `process.chdir(primary)` would leave this node process's\n * own module graph rooted in a directory that is about to be deleted, and its lazily-resolved requires\n * with it. The child is spawned from THIS package's own files (same version — no skew with whatever\n * the primary clone has installed), and it finishes loading them before it removes anything.\n */\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which\n * directory, which branch, where the reap must run FROM, and — when it cannot run — why not.\n */\nexport class WorktreeReapHandoff {\n /** The linked worktree holding the branch that was just landed. */\n readonly worktreePath: string;\n readonly branch: string;\n /** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */\n readonly primaryPath: string;\n /** The compiled entry the child runs. '' when it could not be located on disk. */\n readonly entryScript: string;\n /** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */\n readonly blockedBecause: string;\n /** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */\n readonly canReap: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n worktreePath: string,\n branch: string,\n primaryPath: string,\n entryScript: string,\n blockedBecause: string,\n ) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n this.primaryPath = primaryPath;\n this.entryScript = entryScript;\n this.blockedBecause = blockedBecause;\n this.canReap = blockedBecause === '';\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class LandedWorktreeReaper {\n constructor(private readonly worktrees: WorktreeService) {}\n\n /**\n * Is there a worktree to reap at all, and can the hand-off run?\n *\n * `null` means \"nothing to do here\" — we are in the primary clone, or in a worktree that does not\n * hold the branch that just landed. That is the ordinary `pnpm wp-cleanup` case and needs no\n * special sentence. A non-null handoff with `canReap === false` means there IS a corpse but the\n * re-exec is not safely achievable, which is the case that must keep the manual notice.\n */\n plan(repoRoot: string, landedBranch: string): WorktreeReapHandoff | null {\n const here = this.worktrees.currentWorktree(repoRoot);\n if (here === null || here.isMain || here.branch !== landedBranch) return null;\n\n const primary = this.worktrees.listWorktrees(repoRoot)\n .find((tree: Worktree): boolean => tree.isMain);\n if (primary === undefined) {\n return new WorktreeReapHandoff(\n here.path, landedBranch, '<the primary clone>', '',\n 'git did not report a primary clone, so there is no safe directory to reap from');\n }\n\n const entry = this.reapEntryScript();\n if (entry === '') {\n return new WorktreeReapHandoff(\n here.path, landedBranch, primary.path, '',\n 'the reap entry point is not on disk (this package is running unbuilt)');\n }\n return new WorktreeReapHandoff(here.path, landedBranch, primary.path, entry, '');\n }\n\n /**\n * Run the reap in a child process rooted in the primary clone, and render what happened.\n *\n * stdin is `ignore` deliberately: the child must never be able to ask a question. A prompt printed\n * into a landing recap nobody is watching is not consent, and this reap is authorised by the merge\n * that just succeeded, not by an answer.\n */\n handOff(handoff: WorktreeReapHandoff): string {\n const result = spawnSync(\n process.execPath, [handoff.entryScript, handoff.worktreePath, handoff.branch],\n { cwd: handoff.primaryPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });\n\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd();\n const head = '\\n' + SEP + `🌲 Reaping this worktree from ${handoff.primaryPath}\\n` + SEP + '\\n'\n + ` The branch just landed, so this directory is dead. The reap runs in a child process\\n`\n + ` whose cwd is the primary clone — nothing deletes the directory it is standing in.\\n\\n`;\n\n if (result.status !== 0) {\n return head + (output !== '' ? output + '\\n' : '')\n + `\\n ⚠️ The reap did not complete (exit ${String(result.status ?? -1)}). Nothing was forced.\\n`\n + this.manualNotice(handoff);\n }\n return head + (output !== '' ? output + '\\n' : '') + this.afterReap(handoff);\n }\n\n /**\n * The #512 notice, kept verbatim in spirit for every case the re-exec cannot cover. An honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n manualNotice(handoff: WorktreeReapHandoff): string {\n const why = handoff.blockedBecause !== '' ? ` (${handoff.blockedBecause})\\n` : '';\n return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\\n'\n + ' be removed from in here. Run cleanup from the primary clone instead:\\n'\n + ` cd ${handoff.primaryPath} && pnpm wp-cleanup\\n`\n + why\n + ` It archives ${handoff.branch} as a tag, removes ${handoff.worktreePath}, then deletes the branch.\\n`;\n }\n\n /**\n * The one thing a human or agent MUST be told after a successful reap: the shell they typed this\n * into is now sitting in a directory that no longer exists. Every relative path from here on is a\n * mystery ENOENT unless they move.\n */\n private afterReap(handoff: WorktreeReapHandoff): string {\n return `\\n ⚠️ ${handoff.worktreePath} NO LONGER EXISTS — your shell is standing in a deleted\\n`\n + ' directory, and every following command will fail until you move:\\n'\n + ` cd ${handoff.primaryPath}\\n`;\n }\n\n /**\n * WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this\n * package runs from `.ts` sources and the compiled sibling does not exist.\n *\n * Resolved from `__dirname`, i.e. from THIS package's own installed files, NOT by remapping a path\n * into the primary clone. The primary clone may have a different @webpieces/pr-gate version\n * installed, and re-exec'ing a build that predates this feature would silently do something else.\n * The files are read into the child before it removes anything, so the directory going away\n * underneath them afterwards is harmless.\n */\n protected reapEntryScript(): string {\n const entry = path.join(__dirname, '..', 'wp-reap-worktree.js');\n return fs.existsSync(entry) ? entry : '';\n }\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
require("reflect-metadata");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const pr_gate_app_1 = require("./pr-gate-app");
|
|
8
|
+
/**
|
|
9
|
+
* INTERNAL entry point — deliberately NOT registered in package.json `bin`.
|
|
10
|
+
*
|
|
11
|
+
* `pnpm wp-land-pr` spawns this by absolute path with `cwd` set to the PRIMARY CLONE, so that the
|
|
12
|
+
* worktree it just landed from can be removed by a process that is not standing in it. There is no
|
|
13
|
+
* `wp-reap-worktree` verb for a human: `pnpm wp-cleanup` is that verb and does strictly more.
|
|
14
|
+
*
|
|
15
|
+
* Unlike its siblings it TAKES arguments (`<worktree-path> <branch>`), so `CliArgs.assertNoArgs` —
|
|
16
|
+
* which exists to stop a stray flag from silently starting a mutation flow — would be wrong here.
|
|
17
|
+
* ReapWorktreeCommand validates argv itself and refuses to do anything without both values.
|
|
18
|
+
*/
|
|
19
|
+
(0, rules_config_1.runMain)(async () => {
|
|
20
|
+
const container = new inversify_1.Container({ autobind: true });
|
|
21
|
+
await container.get(pr_gate_app_1.PrGateApp).reapWorktree(process.argv.slice(2));
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=wp-reap-worktree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wp-reap-worktree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-reap-worktree.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAAkD;AAClD,+CAA0C;AAE1C;;;;;;;;;;GAUG;AACH,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\n\n/**\n * INTERNAL entry point — deliberately NOT registered in package.json `bin`.\n *\n * `pnpm wp-land-pr` spawns this by absolute path with `cwd` set to the PRIMARY CLONE, so that the\n * worktree it just landed from can be removed by a process that is not standing in it. There is no\n * `wp-reap-worktree` verb for a human: `pnpm wp-cleanup` is that verb and does strictly more.\n *\n * Unlike its siblings it TAKES arguments (`<worktree-path> <branch>`), so `CliArgs.assertNoArgs` —\n * which exists to stop a stray flag from silently starting a mutation flow — would be wrong here.\n * ReapWorktreeCommand validates argv itself and refuses to do anything without both values.\n */\nrunMain(async (): Promise<void> => {\n const container = new Container({ autobind: true });\n await container.get(PrGateApp).reapWorktree(process.argv.slice(2));\n});\n"]}
|