@webpieces/pr-gate 0.4.724 → 0.4.725

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.724",
3
+ "version": "0.4.725",
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",
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@inversifyjs/binding-decorators": "1.1.5",
19
- "@webpieces/ai-hook-rules": "0.4.724",
20
- "@webpieces/rules-config": "0.4.724",
19
+ "@webpieces/ai-hook-rules": "0.4.725",
20
+ "@webpieces/rules-config": "0.4.725",
21
21
  "inversify": "7.10.4",
22
22
  "reflect-metadata": "0.2.2"
23
23
  },
@@ -32,7 +32,7 @@
32
32
  "wp-start-update": "./src/scripts/wp-start-update.js",
33
33
  "wp-finish-update": "./src/scripts/wp-finish-update.js",
34
34
  "wp-cleanup": "./src/scripts/wp-cleanup.js",
35
- "wp-checkout-clean-main": "./src/scripts/wp-checkout-clean-main.js",
35
+ "wp-sync-main": "./src/scripts/wp-sync-main.js",
36
36
  "wp-land-pr": "./src/scripts/wp-land-pr.js",
37
37
  "wp-check-pr": "./src/scripts/wp-check-pr.js",
38
38
  "wp-review-upsert-pr": "./src/scripts/wp-review-upsert-pr.js",
@@ -125,7 +125,7 @@ let MainCheckout = class MainCheckout {
125
125
  if (!this.isCollision(first, untracked))
126
126
  throw this.checkoutFailed(first);
127
127
  const stash = this.captureAndEcho(repoRoot, ['stash', 'push', '-u',
128
- '-m', 'wp-checkout-clean-main: untracked files that collided with main']);
128
+ '-m', 'wp-sync-main: untracked files that collided with main']);
129
129
  if (!stash.succeeded()) {
130
130
  throw new rules_config_1.CliExitError(1, `${SEP}❌ Could not check out main, and could not stash\n${SEP}\n`
131
131
  + 'git refused the checkout because untracked files in this tree sit at paths main\n'
@@ -1 +1 @@
1
- {"version":3,"file":"main-checkout.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/main-checkout.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from '@webpieces/rules-config';\n\nimport { UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * git's own words for the one checkout failure this class is allowed to react to. Matched on git's\n * TEXT rather than re-derived from the untracked list on purpose: git's rules here cover\n * directory-vs-file collisions, sparse checkouts and case-insensitive filesystems, and any\n * re-derivation of them drifts from the git actually installed. git's refusal is the authority.\n */\nconst WOULD_BE_OVERWRITTEN = 'untracked working tree files would be overwritten';\n\n/** One git invocation's exit status and the output it produced, for a caller that must decide on it. */\nexport class GitRun {\n constructor(readonly status: number, readonly output: string) {}\n\n succeeded(): boolean {\n return this.status === 0;\n }\n}\n\n/**\n * The files `git stash -u` took so the checkout could proceed — carried out so the caller can SHOUT\n * about them. Empty is the overwhelmingly common case: nothing collided, nothing was stashed.\n *\n * The message is ALL CAPS and banner-wrapped deliberately. This is the one path in the command where\n * files that were sitting in the working tree are no longer sitting in the working tree, and a reader\n * who skims past that line loses track of their own work. Everything else this command prints is\n * routine; this is not.\n */\nexport class StashedFiles {\n constructor(readonly paths: readonly string[]) {}\n\n isEmpty(): boolean {\n return this.paths.length === 0;\n }\n\n /** The human-facing banner, or '' when nothing was stashed. */\n render(): string {\n if (this.isEmpty()) return '';\n const list = this.paths.map((path: string): string => ` ${path}\\n`).join('');\n const quoted = this.paths.map((path: string): string => `'${path}'`).join(' ');\n return `${SEP}⚠️ UNTRACKED FILES WERE STASHED BECAUSE THEY CONFLICTED\\n${SEP}\\n`\n + 'GIT REFUSED TO CHECK OUT MAIN: AN UNTRACKED FILE HERE SITS AT A PATH MAIN TRACKS, AND\\n'\n + 'THE CHECKOUT WOULD HAVE OVERWRITTEN IT. `git stash -u` TAKES EVERY UNTRACKED FILE, NOT\\n'\n + 'ONLY THE ONE THAT COLLIDED, SO ALL OF THESE LEFT THE WORKING TREE. THE CHECKOUT WAS THEN\\n'\n + 'RETRIED AND YOU ARE NOW ON A CLEAN MAIN. NOTHING WAS DELETED — YOUR VERSION OF EVERY\\n'\n + 'FILE BELOW IS IN THE STASH:\\n\\n'\n + list\n + '\\nGET THEM BACK WITH:\\n\\n'\n + ' git stash pop\\n\\n'\n + 'EXPECT THAT POP TO REFUSE THE FIRST TIME. IT LANDS ON THE NEW MAIN, WHICH IS EXACTLY\\n'\n + 'WHERE A COMMITTED COPY OF THE COLLIDING PATH NOW SITS — THAT COLLISION IS WHY ANY OF\\n'\n + 'THIS HAPPENED — SO GIT SAYS \"already exists, no checkout\" AND KEEPS THE STASH. TO TAKE\\n'\n + 'YOUR VERSIONS BACK, DELETE MAIN\\'S COPIES AND POP ONTO THE HOLE:\\n\\n'\n + ` rm -f ${quoted} && git stash pop\\n\\n`\n + 'NOTHING IS DISCARDED WHILE THE POP HAS NOT SUCCEEDED — `git stash list` STILL SHOWS THE\\n'\n + 'ENTRY, AS MANY TRIES AS YOU NEED. IF THE FILE IS GENERATED, REGENERATING IT ON THE NEW\\n'\n + 'MAIN IS USUALLY BETTER THAN RESTORING A COPY BUILT FROM THE OLD ONE.\\n\\n'\n + 'WHY STASHED AND NOT REFUSED: REACHING A CLEAN MAIN HAS TO STAY POSSIBLE. A REFUSAL HERE\\n'\n + 'DEADLOCKS ANYONE — HUMAN OR AGENT — WHOSE NEXT STEP IS GATED ON BEING ON MAIN, AND AN\\n'\n + `AGENT MID-UPGRADE CANNOT ASK FOR HELP.\\n${SEP}`;\n }\n}\n\n/**\n * Get onto a current main, whatever the working tree is holding.\n *\n * ─── WHY A COLLISION STASHES RATHER THAN REFUSES ──────────────────────────────────────────────────\n * `WorkingTreeGate` deliberately lets untracked files through: they are on no branch, so the switch\n * does not move them. That is true about the FILES and not true about the CHECKOUT — git aborts when\n * an untracked file sits at a path the destination branch TRACKS (\"The following untracked working\n * tree files would be overwritten by checkout\"). Letting untracked files reach the checkout is\n * exactly what makes that abort reachable, and the field case that produced it is the common one: a\n * generated `design.html`, untracked in a working clone because it is regenerated, and committed on\n * main.\n *\n * A refusal there is a DEAD END for the one caller that most needs the command to work — an agent\n * mid-upgrade of webpieces, unable to use its tools until it reaches a clean main, with nobody\n * watching to move the file for it. So the collision is resolved rather than reported: stash with\n * `-u`, retry, and then say so loudly enough that no human loses track of the files. Losing a minute\n * to a `git stash pop` beats a state nothing can get out of.\n *\n * ─── WHY THE CHECKOUT'S OUTPUT IS CAPTURED AND RE-PRINTED ─────────────────────────────────────────\n * The decision above is made on git's own refusal text, which means the checkout cannot run on\n * `stdio: 'inherit'` — inherited output is never seen by this process. It is piped and echoed back\n * instead, so the reader still gets every byte git wrote. `fetch` and `pull` stay inherited: nothing\n * is decided from their text, and they are the two that render live progress, which a pipe flattens.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class MainCheckout {\n /**\n * Fetch, check out main, fast-forward it. Returns the files that had to be stashed to get there —\n * empty in the ordinary case.\n *\n * `--ff-only` rather than a `reset --hard`, deliberately: a developer or an agent that\n * accidentally committed to local main would have that work silently destroyed by a reset, and\n * the whole point of this command is that it is safe to run without thinking. A refusal to\n * fast-forward is a real condition a human should see and decide about.\n */\n goToMain(repoRoot: string, untracked: UntrackedFiles): StashedFiles {\n this.passThrough(repoRoot, ['fetch', 'origin', 'main']);\n const stashed = this.checkoutMain(repoRoot, untracked);\n if (this.passThrough(repoRoot, ['pull', '--ff-only', 'origin', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Local main could not fast-forward\\n${SEP}\\n`\n + 'Local main has commits that origin/main does not, so it is not a clean copy of the\\n'\n + 'remote. Nothing was reset — those commits may be the only copy. Inspect them with\\n'\n + '`git log origin/main..main` and decide what they are before going further.\\n'\n + this.alsoStashed(stashed));\n }\n return stashed;\n }\n\n private checkoutMain(repoRoot: string, untracked: UntrackedFiles): StashedFiles {\n const first = this.captureAndEcho(repoRoot, ['checkout', 'main']);\n if (first.succeeded()) return new StashedFiles([]);\n if (!this.isCollision(first, untracked)) throw this.checkoutFailed(first);\n\n const stash = this.captureAndEcho(repoRoot, ['stash', 'push', '-u',\n '-m', 'wp-checkout-clean-main: untracked files that collided with main']);\n if (!stash.succeeded()) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main, and could not stash\\n${SEP}\\n`\n + 'git refused the checkout because untracked files in this tree sit at paths main\\n'\n + 'tracks, and `git stash -u` then failed too, so NOTHING WAS STASHED — the tree is\\n'\n + 'exactly as you left it and you are still on your branch. Both git messages are\\n'\n + 'above. Move or delete these files yourself, then re-run:\\n\\n'\n + this.list(untracked.paths));\n }\n\n const second = this.captureAndEcho(repoRoot, ['checkout', 'main']);\n const stashed = new StashedFiles(untracked.paths);\n if (!second.succeeded()) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main even after stashing\\n${SEP}\\n`\n + 'The untracked files below WERE stashed — they are no longer in the working tree —\\n'\n + 'and git still refused the checkout, so something else is blocking it. Its message is\\n'\n + 'above. You are still on your branch. Recover the files with `git stash pop`, and\\n'\n + 'read git\\'s reason before re-running:\\n\\n'\n + this.list(stashed.paths));\n }\n return stashed;\n }\n\n /**\n * Only the collision git NAMES, and only when there is something a stash could actually take.\n * Untracked files with any other refusal, or a refusal with an empty untracked list, is a\n * different problem and stashing would be a guess.\n */\n private isCollision(run: GitRun, untracked: UntrackedFiles): boolean {\n return run.output.includes(WOULD_BE_OVERWRITTEN) && !untracked.isEmpty();\n }\n\n private checkoutFailed(run: GitRun): CliExitError {\n return new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout for a reason this command does not resolve on your behalf — its\\n'\n + 'message is above and is the thing to read. The states that reach here:\\n\\n'\n + ' • a merge or rebase is in progress — finish it or `git merge --abort` / `git rebase\\n'\n + ' --abort`, then re-run\\n'\n + ' • unmerged paths from a conflict — resolve them and `git add` them, then re-run\\n'\n + ' • no local `main` and no `origin/main` to create it from — check `git remote -v`\\n\\n'\n + `git's output:\\n${run.output}`);\n }\n\n /** Appended to a failure that happens AFTER the stash, so the stash is never silently forgotten. */\n private alsoStashed(stashed: StashedFiles): string {\n if (stashed.isEmpty()) return '';\n return '\\nALSO: THESE UNTRACKED FILES WERE STASHED TO GET HERE — `git stash pop` RETURNS THEM:\\n\\n'\n + this.list(stashed.paths);\n }\n\n private list(paths: readonly string[]): string {\n return paths.map((path: string): string => ` ${path}\\n`).join('');\n }\n\n /** git piped, then echoed back verbatim — for the one command whose text is decided on. */\n private captureAndEcho(repoRoot: string, args: string[]): GitRun {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],\n });\n const stdout = result.stdout ?? '';\n const stderr = result.stderr ?? '';\n process.stdout.write(stdout);\n process.stderr.write(stderr);\n return new GitRun(result.status === null ? 1 : result.status, `${stdout}${stderr}`);\n }\n\n /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private passThrough(repoRoot: string, args: string[]): number {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: 'inherit' });\n return result.status === null ? 1 : result.status;\n }\n}\n"]}
1
+ {"version":3,"file":"main-checkout.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/main-checkout.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from '@webpieces/rules-config';\n\nimport { UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * git's own words for the one checkout failure this class is allowed to react to. Matched on git's\n * TEXT rather than re-derived from the untracked list on purpose: git's rules here cover\n * directory-vs-file collisions, sparse checkouts and case-insensitive filesystems, and any\n * re-derivation of them drifts from the git actually installed. git's refusal is the authority.\n */\nconst WOULD_BE_OVERWRITTEN = 'untracked working tree files would be overwritten';\n\n/** One git invocation's exit status and the output it produced, for a caller that must decide on it. */\nexport class GitRun {\n constructor(readonly status: number, readonly output: string) {}\n\n succeeded(): boolean {\n return this.status === 0;\n }\n}\n\n/**\n * The files `git stash -u` took so the checkout could proceed — carried out so the caller can SHOUT\n * about them. Empty is the overwhelmingly common case: nothing collided, nothing was stashed.\n *\n * The message is ALL CAPS and banner-wrapped deliberately. This is the one path in the command where\n * files that were sitting in the working tree are no longer sitting in the working tree, and a reader\n * who skims past that line loses track of their own work. Everything else this command prints is\n * routine; this is not.\n */\nexport class StashedFiles {\n constructor(readonly paths: readonly string[]) {}\n\n isEmpty(): boolean {\n return this.paths.length === 0;\n }\n\n /** The human-facing banner, or '' when nothing was stashed. */\n render(): string {\n if (this.isEmpty()) return '';\n const list = this.paths.map((path: string): string => ` ${path}\\n`).join('');\n const quoted = this.paths.map((path: string): string => `'${path}'`).join(' ');\n return `${SEP}⚠️ UNTRACKED FILES WERE STASHED BECAUSE THEY CONFLICTED\\n${SEP}\\n`\n + 'GIT REFUSED TO CHECK OUT MAIN: AN UNTRACKED FILE HERE SITS AT A PATH MAIN TRACKS, AND\\n'\n + 'THE CHECKOUT WOULD HAVE OVERWRITTEN IT. `git stash -u` TAKES EVERY UNTRACKED FILE, NOT\\n'\n + 'ONLY THE ONE THAT COLLIDED, SO ALL OF THESE LEFT THE WORKING TREE. THE CHECKOUT WAS THEN\\n'\n + 'RETRIED AND YOU ARE NOW ON A CLEAN MAIN. NOTHING WAS DELETED — YOUR VERSION OF EVERY\\n'\n + 'FILE BELOW IS IN THE STASH:\\n\\n'\n + list\n + '\\nGET THEM BACK WITH:\\n\\n'\n + ' git stash pop\\n\\n'\n + 'EXPECT THAT POP TO REFUSE THE FIRST TIME. IT LANDS ON THE NEW MAIN, WHICH IS EXACTLY\\n'\n + 'WHERE A COMMITTED COPY OF THE COLLIDING PATH NOW SITS — THAT COLLISION IS WHY ANY OF\\n'\n + 'THIS HAPPENED — SO GIT SAYS \"already exists, no checkout\" AND KEEPS THE STASH. TO TAKE\\n'\n + 'YOUR VERSIONS BACK, DELETE MAIN\\'S COPIES AND POP ONTO THE HOLE:\\n\\n'\n + ` rm -f ${quoted} && git stash pop\\n\\n`\n + 'NOTHING IS DISCARDED WHILE THE POP HAS NOT SUCCEEDED — `git stash list` STILL SHOWS THE\\n'\n + 'ENTRY, AS MANY TRIES AS YOU NEED. IF THE FILE IS GENERATED, REGENERATING IT ON THE NEW\\n'\n + 'MAIN IS USUALLY BETTER THAN RESTORING A COPY BUILT FROM THE OLD ONE.\\n\\n'\n + 'WHY STASHED AND NOT REFUSED: REACHING A CLEAN MAIN HAS TO STAY POSSIBLE. A REFUSAL HERE\\n'\n + 'DEADLOCKS ANYONE — HUMAN OR AGENT — WHOSE NEXT STEP IS GATED ON BEING ON MAIN, AND AN\\n'\n + `AGENT MID-UPGRADE CANNOT ASK FOR HELP.\\n${SEP}`;\n }\n}\n\n/**\n * Get onto a current main, whatever the working tree is holding.\n *\n * ─── WHY A COLLISION STASHES RATHER THAN REFUSES ──────────────────────────────────────────────────\n * `WorkingTreeGate` deliberately lets untracked files through: they are on no branch, so the switch\n * does not move them. That is true about the FILES and not true about the CHECKOUT — git aborts when\n * an untracked file sits at a path the destination branch TRACKS (\"The following untracked working\n * tree files would be overwritten by checkout\"). Letting untracked files reach the checkout is\n * exactly what makes that abort reachable, and the field case that produced it is the common one: a\n * generated `design.html`, untracked in a working clone because it is regenerated, and committed on\n * main.\n *\n * A refusal there is a DEAD END for the one caller that most needs the command to work — an agent\n * mid-upgrade of webpieces, unable to use its tools until it reaches a clean main, with nobody\n * watching to move the file for it. So the collision is resolved rather than reported: stash with\n * `-u`, retry, and then say so loudly enough that no human loses track of the files. Losing a minute\n * to a `git stash pop` beats a state nothing can get out of.\n *\n * ─── WHY THE CHECKOUT'S OUTPUT IS CAPTURED AND RE-PRINTED ─────────────────────────────────────────\n * The decision above is made on git's own refusal text, which means the checkout cannot run on\n * `stdio: 'inherit'` — inherited output is never seen by this process. It is piped and echoed back\n * instead, so the reader still gets every byte git wrote. `fetch` and `pull` stay inherited: nothing\n * is decided from their text, and they are the two that render live progress, which a pipe flattens.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class MainCheckout {\n /**\n * Fetch, check out main, fast-forward it. Returns the files that had to be stashed to get there —\n * empty in the ordinary case.\n *\n * `--ff-only` rather than a `reset --hard`, deliberately: a developer or an agent that\n * accidentally committed to local main would have that work silently destroyed by a reset, and\n * the whole point of this command is that it is safe to run without thinking. A refusal to\n * fast-forward is a real condition a human should see and decide about.\n */\n goToMain(repoRoot: string, untracked: UntrackedFiles): StashedFiles {\n this.passThrough(repoRoot, ['fetch', 'origin', 'main']);\n const stashed = this.checkoutMain(repoRoot, untracked);\n if (this.passThrough(repoRoot, ['pull', '--ff-only', 'origin', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Local main could not fast-forward\\n${SEP}\\n`\n + 'Local main has commits that origin/main does not, so it is not a clean copy of the\\n'\n + 'remote. Nothing was reset — those commits may be the only copy. Inspect them with\\n'\n + '`git log origin/main..main` and decide what they are before going further.\\n'\n + this.alsoStashed(stashed));\n }\n return stashed;\n }\n\n private checkoutMain(repoRoot: string, untracked: UntrackedFiles): StashedFiles {\n const first = this.captureAndEcho(repoRoot, ['checkout', 'main']);\n if (first.succeeded()) return new StashedFiles([]);\n if (!this.isCollision(first, untracked)) throw this.checkoutFailed(first);\n\n const stash = this.captureAndEcho(repoRoot, ['stash', 'push', '-u',\n '-m', 'wp-sync-main: untracked files that collided with main']);\n if (!stash.succeeded()) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main, and could not stash\\n${SEP}\\n`\n + 'git refused the checkout because untracked files in this tree sit at paths main\\n'\n + 'tracks, and `git stash -u` then failed too, so NOTHING WAS STASHED — the tree is\\n'\n + 'exactly as you left it and you are still on your branch. Both git messages are\\n'\n + 'above. Move or delete these files yourself, then re-run:\\n\\n'\n + this.list(untracked.paths));\n }\n\n const second = this.captureAndEcho(repoRoot, ['checkout', 'main']);\n const stashed = new StashedFiles(untracked.paths);\n if (!second.succeeded()) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main even after stashing\\n${SEP}\\n`\n + 'The untracked files below WERE stashed — they are no longer in the working tree —\\n'\n + 'and git still refused the checkout, so something else is blocking it. Its message is\\n'\n + 'above. You are still on your branch. Recover the files with `git stash pop`, and\\n'\n + 'read git\\'s reason before re-running:\\n\\n'\n + this.list(stashed.paths));\n }\n return stashed;\n }\n\n /**\n * Only the collision git NAMES, and only when there is something a stash could actually take.\n * Untracked files with any other refusal, or a refusal with an empty untracked list, is a\n * different problem and stashing would be a guess.\n */\n private isCollision(run: GitRun, untracked: UntrackedFiles): boolean {\n return run.output.includes(WOULD_BE_OVERWRITTEN) && !untracked.isEmpty();\n }\n\n private checkoutFailed(run: GitRun): CliExitError {\n return new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout for a reason this command does not resolve on your behalf — its\\n'\n + 'message is above and is the thing to read. The states that reach here:\\n\\n'\n + ' • a merge or rebase is in progress — finish it or `git merge --abort` / `git rebase\\n'\n + ' --abort`, then re-run\\n'\n + ' • unmerged paths from a conflict — resolve them and `git add` them, then re-run\\n'\n + ' • no local `main` and no `origin/main` to create it from — check `git remote -v`\\n\\n'\n + `git's output:\\n${run.output}`);\n }\n\n /** Appended to a failure that happens AFTER the stash, so the stash is never silently forgotten. */\n private alsoStashed(stashed: StashedFiles): string {\n if (stashed.isEmpty()) return '';\n return '\\nALSO: THESE UNTRACKED FILES WERE STASHED TO GET HERE — `git stash pop` RETURNS THEM:\\n\\n'\n + this.list(stashed.paths);\n }\n\n private list(paths: readonly string[]): string {\n return paths.map((path: string): string => ` ${path}\\n`).join('');\n }\n\n /** git piped, then echoed back verbatim — for the one command whose text is decided on. */\n private captureAndEcho(repoRoot: string, args: string[]): GitRun {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],\n });\n const stdout = result.stdout ?? '';\n const stderr = result.stderr ?? '';\n process.stdout.write(stdout);\n process.stderr.write(stderr);\n return new GitRun(result.status === null ? 1 : result.status, `${stdout}${stderr}`);\n }\n\n /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private passThrough(repoRoot: string, args: string[]): number {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8', stdio: 'inherit' });\n return result.status === null ? 1 : result.status;\n }\n}\n"]}
@@ -3,7 +3,7 @@ import { CleanupCommand } from './cleanup-command';
3
3
  import { MainCheckout } from './main-checkout';
4
4
  import { WorkingTreeGate } from './working-tree-gate';
5
5
  /**
6
- * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three
6
+ * `wp-sync-main` — go to main, get main, and take out the trash. ONE command, because the three
7
7
  * are one intention and splitting them is what let the third never happen.
8
8
  *
9
9
  * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE "DELETING" THAT) ────
@@ -25,7 +25,7 @@ import { WorkingTreeGate } from './working-tree-gate';
25
25
  * there; it is the only thing that works when the package manager's output is what is in doubt.
26
26
  * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,
27
27
  * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's "Finishing a
28
- * Feature") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.
28
+ * Feature") → prescribes `pnpm wp-sync-main`, and no longer prints the pair.
29
29
  *
30
30
  * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not
31
31
  * what they permit. Blocking it would have deleted the L0 escape along with the shim.
@@ -42,7 +42,7 @@ import { WorkingTreeGate } from './working-tree-gate';
42
42
  * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one
43
43
  * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.
44
44
  */
45
- export declare class CheckoutCleanMainCommand {
45
+ export declare class SyncMainCommand {
46
46
  private readonly repoRootFinder;
47
47
  private readonly cleanupCommand;
48
48
  private readonly orphanDirSweeper;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CheckoutCleanMainCommand = void 0;
3
+ exports.SyncMainCommand = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const inversify_1 = require("inversify");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
@@ -10,7 +10,7 @@ const main_checkout_1 = require("./main-checkout");
10
10
  const working_tree_gate_1 = require("./working-tree-gate");
11
11
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
12
12
  /**
13
- * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three
13
+ * `wp-sync-main` — go to main, get main, and take out the trash. ONE command, because the three
14
14
  * are one intention and splitting them is what let the third never happen.
15
15
  *
16
16
  * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE "DELETING" THAT) ────
@@ -32,7 +32,7 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
32
32
  * there; it is the only thing that works when the package manager's output is what is in doubt.
33
33
  * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,
34
34
  * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's "Finishing a
35
- * Feature") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.
35
+ * Feature") → prescribes `pnpm wp-sync-main`, and no longer prints the pair.
36
36
  *
37
37
  * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not
38
38
  * what they permit. Blocking it would have deleted the L0 escape along with the shim.
@@ -49,7 +49,7 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
49
49
  * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one
50
50
  * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.
51
51
  */
52
- let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
52
+ let SyncMainCommand = class SyncMainCommand {
53
53
  repoRootFinder;
54
54
  cleanupCommand;
55
55
  orphanDirSweeper;
@@ -138,13 +138,13 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
138
138
  process.stdout.write(`\n${rendered}`);
139
139
  }
140
140
  };
141
- exports.CheckoutCleanMainCommand = CheckoutCleanMainCommand;
142
- exports.CheckoutCleanMainCommand = CheckoutCleanMainCommand = tslib_1.__decorate([
141
+ exports.SyncMainCommand = SyncMainCommand;
142
+ exports.SyncMainCommand = SyncMainCommand = tslib_1.__decorate([
143
143
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
144
144
  tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
145
145
  cleanup_command_1.CleanupCommand,
146
146
  rules_config_1.OrphanDirSweeper,
147
147
  working_tree_gate_1.WorkingTreeGate,
148
148
  main_checkout_1.MainCheckout])
149
- ], CheckoutCleanMainCommand);
150
- //# sourceMappingURL=checkout-clean-main-command.js.map
149
+ ], SyncMainCommand);
150
+ //# sourceMappingURL=sync-main-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/sync-main-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\nimport { MainCheckout, StashedFiles } from './main-checkout';\nimport { WorkingTreeGate, UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-sync-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-sync-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SyncMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n private readonly workingTreeGate: WorkingTreeGate,\n private readonly mainCheckout: MainCheckout,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);\n const stashed = this.mainCheckout.goToMain(repoRoot, untracked);\n this.reportUntracked(untracked, stashed);\n // No flags: this is the \"go to main and take out the trash\" path, so cleanup behaves exactly as a\n // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask\n // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever\n // grows a field, this call site is a compile error instead of a silent old behaviour.\n await this.cleanupCommand.run(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, false, ''),\n new DeleteSelection(FLAG_DELETE_WORKTREES, false, ''),\n false, false, false));\n this.sweep(repoRoot);\n this.reportStashed(stashed);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Say which untracked files were let through, AFTER the move — so nobody has to wonder whether the\n * files still sitting in their tree came along for the ride. Printed rather than refused on: see\n * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.\n *\n * Silent when the checkout had to stash: those files are NOT sitting in the tree any more, and\n * \"nothing about them changed\" would be a lie. `reportStashed` speaks for that case instead.\n */\n private reportUntracked(untracked: UntrackedFiles, stashed: StashedFiles): void {\n if (!stashed.isEmpty()) return;\n const rendered = untracked.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * The stash banner goes LAST, on purpose. It is the one thing in this command's output a reader\n * must not miss — their files left the working tree — and the cleanup and sweep that follow the\n * checkout print enough to scroll it away if it were emitted where it happened.\n */\n private reportStashed(stashed: StashedFiles): void {\n const rendered = stashed.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n}\n"]}
@@ -10,7 +10,7 @@ export declare class UntrackedFiles {
10
10
  render(): string;
11
11
  }
12
12
  /**
13
- * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and
13
+ * The clean-tree gate for `wp-sync-main`: refuse when TRACKED work would ride onto main, and
14
14
  * only then.
15
15
  *
16
16
  * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────
@@ -30,7 +30,7 @@ class UntrackedFiles {
30
30
  }
31
31
  exports.UntrackedFiles = UntrackedFiles;
32
32
  /**
33
- * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and
33
+ * The clean-tree gate for `wp-sync-main`: refuse when TRACKED work would ride onto main, and
34
34
  * only then.
35
35
  *
36
36
  * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"working-tree-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/working-tree-gate.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,yCAA2D;AAC3D,0DAAuD;AAEvD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;GAGG;AACH,MAAa,cAAc;IACF;IAArB,YAAqB,KAAwB;QAAxB,UAAK,GAAL,KAAK,CAAmB;IAAG,CAAC;IAEjD,OAAO;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,kEAAkE;IAClE,MAAM;QACF,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9E,OAAO,0FAA0F;cAC3F,0CAA0C;cAC1C,IAAI,CAAC;IACf,CAAC;CACJ;AAfD,wCAeC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;OAGG;IACH,sBAAsB,CAAC,QAAgB;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC,CAAC;QACtF,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,GAAG,GAAG,2CAA2C,GAAG,IAAI;kBACtD,yFAAyF;kBACzF,yFAAyF;kBACzF,oEAAoE;kBACpE,GAAG,OAAO,EAAE,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAEO,cAAc,CAAC,QAAgB;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC;QACxF,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC;QACnD,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC;IAED,iFAAiF;IACzE,GAAG,CAAC,QAAgB,EAAE,IAAc;QACxC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,EAAE;YACvD,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACnE,OAAO,MAAM,CAAC,MAAM,CAAC;IACzB,CAAC;CACJ,CAAA;AAhCY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAgC3B","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from '@webpieces/rules-config';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The untracked files a branch switch is about to leave exactly where they are — carried out of the gate\n * so the caller can SAY so, rather than surprise anyone with files that appear to have followed them.\n */\nexport class UntrackedFiles {\n constructor(readonly paths: readonly string[]) {}\n\n isEmpty(): boolean {\n return this.paths.length === 0;\n }\n\n /** The human-facing block, or '' when there is nothing to say. */\n render(): string {\n if (this.isEmpty()) return '';\n const list = this.paths.map((path: string): string => ` ${path}\\n`).join('');\n return 'These untracked files are not on any branch, so the checkout leaves them exactly where\\n'\n + 'they are — nothing about them changed:\\n'\n + list;\n }\n}\n\n/**\n * The clean-tree gate for `wp-checkout-clean-main`: refuse when TRACKED work would ride onto main, and\n * only then.\n *\n * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────\n * The hazard the gate exists for is real but narrow: `git checkout` carries uncommitted changes to\n * TRACKED files across the switch, and main is the one place nobody wants to discover their work. An\n * untracked file has none of that. It is not on any branch, the switch does not move it, and no branch\n * can end up holding it — so refusing on one blocks a routine, safe operation.\n *\n * Worse, it was a refusal the user could not clear without deleting their file. The message prescribed\n * \"commit them on this branch, or stash them\", and a plain `git stash` does NOT take untracked files\n * (that needs `-u`) — so a user who did exactly what the error said got the identical refusal back. That\n * is the shape this repo treats as a defect in its own right: a cure that does not resolve the state it\n * is prescribed for. Reported from the field, where a generated `design.html` sitting untracked made the\n * command permanently unusable in that clone.\n *\n * Now the refusal only ever names tracked changes, so `git stash` — the cure it prints — always resolves\n * it, by construction.\n *\n * That is true about the FILES and not about the CHECKOUT: git itself aborts a switch when an untracked\n * file sits at a path the destination branch TRACKS. Letting untracked files through is what makes that\n * abort reachable, and `MainCheckout` is where it is handled — it reacts to git's own refusal by stashing\n * with `-u`, retrying, and shouting about it. This gate does not predict that case, deliberately: git's\n * rules for it are the authority, and re-deriving them here would drift from the git actually installed.\n *\n * ─── WHY `--untracked-files=no` AND `ls-files -z` RATHER THAN PARSING `??` ─────────────────────────────\n * The decision half asks git the tracked-only question directly, so no line of porcelain output is ever\n * parsed to make it. The reporting half asks `ls-files --others --exclude-standard -z`, which emits bare\n * NUL-separated paths — no `?? ` prefix to strip and, crucially, no quoting: porcelain v1 wraps paths\n * containing special characters in quotes with C-style escapes, so a hand-parser would hand back a\n * mangled name for exactly the paths a human most needs to read correctly.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorkingTreeGate {\n /**\n * Throw when tracked files are modified or staged; otherwise hand back the untracked files that were\n * deliberately allowed through, for the caller to mention.\n */\n assertNoTrackedChanges(repoRoot: string): UntrackedFiles {\n const tracked = this.git(repoRoot, ['status', '--porcelain', '--untracked-files=no']);\n if (tracked !== null && tracked.trim() !== '') {\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted changes to tracked files\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or `git stash` them\\n'\n + '(everything listed below is tracked, which is exactly what a plain stash takes), then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${tracked}`);\n }\n return this.untrackedFiles(repoRoot);\n }\n\n private untrackedFiles(repoRoot: string): UntrackedFiles {\n const output = this.git(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);\n if (output === null) return new UntrackedFiles([]);\n return new UntrackedFiles(output.split('\\0').filter((path: string): boolean => path !== ''));\n }\n\n /** Captured git output, or null when git could not be run or exited non-zero. */\n private git(repoRoot: string, args: string[]): string | null {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n}\n"]}
1
+ {"version":3,"file":"working-tree-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/working-tree-gate.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,yCAA2D;AAC3D,0DAAuD;AAEvD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;GAGG;AACH,MAAa,cAAc;IACF;IAArB,YAAqB,KAAwB;QAAxB,UAAK,GAAL,KAAK,CAAmB;IAAG,CAAC;IAEjD,OAAO;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,kEAAkE;IAClE,MAAM;QACF,IAAI,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9E,OAAO,0FAA0F;cAC3F,0CAA0C;cAC1C,IAAI,CAAC;IACf,CAAC;CACJ;AAfD,wCAeC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;;OAGG;IACH,sBAAsB,CAAC,QAAgB;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC,CAAC;QACtF,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,GAAG,GAAG,2CAA2C,GAAG,IAAI;kBACtD,yFAAyF;kBACzF,yFAAyF;kBACzF,oEAAoE;kBACpE,GAAG,OAAO,EAAE,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAEO,cAAc,CAAC,QAAgB;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAAC;QACxF,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC;QACnD,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC;IAED,iFAAiF;IACzE,GAAG,CAAC,QAAgB,EAAE,IAAc;QACxC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,EAAE;YACvD,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACnE,OAAO,MAAM,CAAC,MAAM,CAAC;IACzB,CAAC;CACJ,CAAA;AAhCY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CAgC3B","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { CliExitError } from '@webpieces/rules-config';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The untracked files a branch switch is about to leave exactly where they are — carried out of the gate\n * so the caller can SAY so, rather than surprise anyone with files that appear to have followed them.\n */\nexport class UntrackedFiles {\n constructor(readonly paths: readonly string[]) {}\n\n isEmpty(): boolean {\n return this.paths.length === 0;\n }\n\n /** The human-facing block, or '' when there is nothing to say. */\n render(): string {\n if (this.isEmpty()) return '';\n const list = this.paths.map((path: string): string => ` ${path}\\n`).join('');\n return 'These untracked files are not on any branch, so the checkout leaves them exactly where\\n'\n + 'they are — nothing about them changed:\\n'\n + list;\n }\n}\n\n/**\n * The clean-tree gate for `wp-sync-main`: refuse when TRACKED work would ride onto main, and\n * only then.\n *\n * ─── WHY UNTRACKED FILES ARE NOT A REASON TO REFUSE ───────────────────────────────────────────────────\n * The hazard the gate exists for is real but narrow: `git checkout` carries uncommitted changes to\n * TRACKED files across the switch, and main is the one place nobody wants to discover their work. An\n * untracked file has none of that. It is not on any branch, the switch does not move it, and no branch\n * can end up holding it — so refusing on one blocks a routine, safe operation.\n *\n * Worse, it was a refusal the user could not clear without deleting their file. The message prescribed\n * \"commit them on this branch, or stash them\", and a plain `git stash` does NOT take untracked files\n * (that needs `-u`) — so a user who did exactly what the error said got the identical refusal back. That\n * is the shape this repo treats as a defect in its own right: a cure that does not resolve the state it\n * is prescribed for. Reported from the field, where a generated `design.html` sitting untracked made the\n * command permanently unusable in that clone.\n *\n * Now the refusal only ever names tracked changes, so `git stash` — the cure it prints — always resolves\n * it, by construction.\n *\n * That is true about the FILES and not about the CHECKOUT: git itself aborts a switch when an untracked\n * file sits at a path the destination branch TRACKS. Letting untracked files through is what makes that\n * abort reachable, and `MainCheckout` is where it is handled — it reacts to git's own refusal by stashing\n * with `-u`, retrying, and shouting about it. This gate does not predict that case, deliberately: git's\n * rules for it are the authority, and re-deriving them here would drift from the git actually installed.\n *\n * ─── WHY `--untracked-files=no` AND `ls-files -z` RATHER THAN PARSING `??` ─────────────────────────────\n * The decision half asks git the tracked-only question directly, so no line of porcelain output is ever\n * parsed to make it. The reporting half asks `ls-files --others --exclude-standard -z`, which emits bare\n * NUL-separated paths — no `?? ` prefix to strip and, crucially, no quoting: porcelain v1 wraps paths\n * containing special characters in quotes with C-style escapes, so a hand-parser would hand back a\n * mangled name for exactly the paths a human most needs to read correctly.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorkingTreeGate {\n /**\n * Throw when tracked files are modified or staged; otherwise hand back the untracked files that were\n * deliberately allowed through, for the caller to mention.\n */\n assertNoTrackedChanges(repoRoot: string): UntrackedFiles {\n const tracked = this.git(repoRoot, ['status', '--porcelain', '--untracked-files=no']);\n if (tracked !== null && tracked.trim() !== '') {\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted changes to tracked files\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or `git stash` them\\n'\n + '(everything listed below is tracked, which is exactly what a plain stash takes), then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${tracked}`);\n }\n return this.untrackedFiles(repoRoot);\n }\n\n private untrackedFiles(repoRoot: string): UntrackedFiles {\n const output = this.git(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']);\n if (output === null) return new UntrackedFiles([]);\n return new UntrackedFiles(output.split('\\0').filter((path: string): boolean => path !== ''));\n }\n\n /** Captured git output, or null when git could not be run or exited non-zero. */\n private git(repoRoot: string, args: string[]): string | null {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n}\n"]}
@@ -4,7 +4,7 @@ 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
6
  import { CleanupOptions } from './commands/cleanup-options';
7
- import { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';
7
+ import { SyncMainCommand } from './commands/sync-main-command';
8
8
  import { LandPrCommand, LandPrRequest } from './commands/land-pr-command';
9
9
  import { CheckPrCommand } from './commands/check-pr-command';
10
10
  import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
@@ -26,7 +26,7 @@ export declare class PrGateApp {
26
26
  private readonly startUpsertPrCommand;
27
27
  private readonly finishUpsertPrCommand;
28
28
  private readonly cleanupCommand;
29
- private readonly checkoutCleanMainCommand;
29
+ private readonly syncMainCommand;
30
30
  private readonly landPrCommand;
31
31
  private readonly checkPrCommand;
32
32
  private readonly reviewUpsertPrCommand;
@@ -36,7 +36,7 @@ export declare class PrGateApp {
36
36
  private readonly finishPushDevCommand;
37
37
  private readonly pushDevStateStore;
38
38
  private readonly repoRootFinder;
39
- constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, checkoutCleanMainCommand: CheckoutCleanMainCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand, buildCommand: BuildCommand, pushDevCommand: PushDevCommand, finishPushDevCommand: FinishPushDevCommand, pushDevStateStore: PushDevStateStore, repoRootFinder: RepoRootFinder);
39
+ constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, syncMainCommand: SyncMainCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand, reapWorktreeCommand: ReapWorktreeCommand, buildCommand: BuildCommand, pushDevCommand: PushDevCommand, finishPushDevCommand: FinishPushDevCommand, pushDevStateStore: PushDevStateStore, repoRootFinder: RepoRootFinder);
40
40
  /**
41
41
  * Refuse `command` while a `wp-push-dev --resolve` is half-finished.
42
42
  *
@@ -76,12 +76,12 @@ export declare class PrGateApp {
76
76
  */
77
77
  cleanup(options: CleanupOptions): Promise<void>;
78
78
  /**
79
- * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
79
+ * `wp-sync-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
80
80
  * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull
81
- * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted
81
+ * origin main` outright — see SyncMainCommand for why the old pair must stop being accepted
82
82
  * rather than surviving beside this.
83
83
  */
84
- checkoutCleanMain(): Promise<void>;
84
+ syncMain(): Promise<void>;
85
85
  /**
86
86
  * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,
87
87
  * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.
@@ -9,7 +9,7 @@ const finish_update_command_1 = require("./commands/finish-update-command");
9
9
  const start_upsert_pr_command_1 = require("./commands/start-upsert-pr-command");
10
10
  const finish_upsert_pr_command_1 = require("./commands/finish-upsert-pr-command");
11
11
  const cleanup_command_1 = require("./commands/cleanup-command");
12
- const checkout_clean_main_command_1 = require("./commands/checkout-clean-main-command");
12
+ const sync_main_command_1 = require("./commands/sync-main-command");
13
13
  const land_pr_command_1 = require("./commands/land-pr-command");
14
14
  const check_pr_command_1 = require("./commands/check-pr-command");
15
15
  const review_upsert_pr_command_1 = require("./commands/review-upsert-pr-command");
@@ -31,7 +31,7 @@ let PrGateApp = class PrGateApp {
31
31
  startUpsertPrCommand;
32
32
  finishUpsertPrCommand;
33
33
  cleanupCommand;
34
- checkoutCleanMainCommand;
34
+ syncMainCommand;
35
35
  landPrCommand;
36
36
  checkPrCommand;
37
37
  reviewUpsertPrCommand;
@@ -41,13 +41,13 @@ let PrGateApp = class PrGateApp {
41
41
  finishPushDevCommand;
42
42
  pushDevStateStore;
43
43
  repoRootFinder;
44
- constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, checkoutCleanMainCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand, buildCommand, pushDevCommand, finishPushDevCommand, pushDevStateStore, repoRootFinder) {
44
+ constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, syncMainCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand, reapWorktreeCommand, buildCommand, pushDevCommand, finishPushDevCommand, pushDevStateStore, repoRootFinder) {
45
45
  this.startUpdateCommand = startUpdateCommand;
46
46
  this.finishUpdateCommand = finishUpdateCommand;
47
47
  this.startUpsertPrCommand = startUpsertPrCommand;
48
48
  this.finishUpsertPrCommand = finishUpsertPrCommand;
49
49
  this.cleanupCommand = cleanupCommand;
50
- this.checkoutCleanMainCommand = checkoutCleanMainCommand;
50
+ this.syncMainCommand = syncMainCommand;
51
51
  this.landPrCommand = landPrCommand;
52
52
  this.checkPrCommand = checkPrCommand;
53
53
  this.reviewUpsertPrCommand = reviewUpsertPrCommand;
@@ -124,14 +124,14 @@ let PrGateApp = class PrGateApp {
124
124
  return this.cleanupCommand.run(options);
125
125
  }
126
126
  /**
127
- * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
127
+ * `wp-sync-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep
128
128
  * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull
129
- * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted
129
+ * origin main` outright — see SyncMainCommand for why the old pair must stop being accepted
130
130
  * rather than surviving beside this.
131
131
  */
132
- checkoutCleanMain() {
133
- this.assertNoResolveInProgress('wp-checkout-clean-main');
134
- return this.checkoutCleanMainCommand.run();
132
+ syncMain() {
133
+ this.assertNoResolveInProgress('wp-sync-main');
134
+ return this.syncMainCommand.run();
135
135
  }
136
136
  /**
137
137
  * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,
@@ -164,7 +164,7 @@ exports.PrGateApp = PrGateApp = tslib_1.__decorate([
164
164
  start_upsert_pr_command_1.StartUpsertPrCommand,
165
165
  finish_upsert_pr_command_1.FinishUpsertPrCommand,
166
166
  cleanup_command_1.CleanupCommand,
167
- checkout_clean_main_command_1.CheckoutCleanMainCommand,
167
+ sync_main_command_1.SyncMainCommand,
168
168
  land_pr_command_1.LandPrCommand,
169
169
  check_pr_command_1.CheckPrCommand,
170
170
  review_upsert_pr_command_1.ReviewUpsertPrCommand,
@@ -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;AAE5D,wFAAkF;AAClF,gEAA0E;AAC1E,kEAA6D;AAC7D,kFAAmG;AACnG,4EAAuE;AACvE,4DAAsE;AACtE,kEAA6E;AAC7E,gFAAgG;AAChG,8DAA8D;AAC9D,0DAAyD;AAEzD;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAfrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,wBAAkD,EAClD,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC,EACxC,YAA0B,EAC1B,cAA8B,EAC9B,oBAA0C,EAC1C,iBAAoC,EACpC,cAA8B;QAd9B,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,6BAAwB,GAAxB,wBAAwB,CAA0B;QAClD,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,mBAAc,GAAd,cAAc,CAAgB;IAChD,CAAC;IAEJ;;;;;;;OAOG;IACK,yBAAyB,CAAC,OAAe;QAC7C,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,OAAO,CAAC;YAAE,OAAO;QACpE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACnG,CAAC;IAED,+GAA+G;IAC/G,OAAO,CAAC,OAAuB,IAAI,iCAAc,EAAE;QAC/C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,mGAAmG;IACnG,aAAa,CAAC,OAA6B,IAAI,8CAAoB,EAAE;QACjE,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAkB;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,IAAI,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,IAAI,CAAC,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,OAAuB;QAC3B,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACb,IAAI,CAAC,yBAAyB,CAAC,wBAAwB,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,wBAAwB,CAAC,GAAG,EAAE,CAAC;IAC/C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,OAAsB;QACzB,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAA8B,IAAI,gDAAqB,EAAE;QACpE,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AA/HY,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;QACJ,sDAAwB;QACnC,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;QAC1B,4BAAY;QACV,iCAAc;QACR,8CAAoB;QACvB,kCAAiB;QACpB,6BAAc;GAhB1C,SAAS,CA+HrB","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, LandPrRequest } 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 /**\n * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,\n * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.\n */\n landPr(request: LandPrRequest): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(request);\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 { SyncMainCommand } from './commands/sync-main-command';\nimport { LandPrCommand, LandPrRequest } 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 syncMainCommand: SyncMainCommand,\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-sync-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 SyncMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n syncMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-sync-main');\n return this.syncMainCommand.run();\n }\n\n /**\n * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,\n * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.\n */\n landPr(request: LandPrRequest): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(request);\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"]}
@@ -10,7 +10,7 @@ const pr_gate_app_1 = require("./pr-gate-app");
10
10
  // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)
11
11
  const container = new inversify_1.Container({ autobind: true });
12
12
  // Reject `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.
13
- container.get(rules_config_1.CliArgs).assertNoArgs(new rules_config_1.CliUsage('wp-checkout-clean-main', 'Go to main, fast-forward it, reap dead worktrees and branches, and sweep orphan directories.'));
14
- await container.get(pr_gate_app_1.PrGateApp).checkoutCleanMain();
13
+ container.get(rules_config_1.CliArgs).assertNoArgs(new rules_config_1.CliUsage('wp-sync-main', 'Go to main, fast-forward it, reap dead worktrees and branches, and sweep orphan directories.'));
14
+ await container.get(pr_gate_app_1.PrGateApp).syncMain();
15
15
  });
16
- //# sourceMappingURL=wp-checkout-clean-main.js.map
16
+ //# sourceMappingURL=wp-sync-main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wp-sync-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-sync-main.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAAqE;AACrE,+CAA0C;AAE1C,mGAAmG;AACnG,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,sGAAsG;IACtG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,YAAY,CAAC,IAAI,uBAAQ,CAC5C,cAAc,EACd,8FAA8F,CAAC,CAAC,CAAC;IACrG,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,QAAQ,EAAE,CAAC;AAC9C,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs, CliUsage } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\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 `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.\n container.get(CliArgs).assertNoArgs(new CliUsage(\n 'wp-sync-main',\n 'Go to main, fast-forward it, reap dead worktrees and branches, and sweep orphan directories.'));\n await container.get(PrGateApp).syncMain();\n});\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"checkout-clean-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checkout-clean-main-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\nimport { MainCheckout, StashedFiles } from './main-checkout';\nimport { WorkingTreeGate, UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckoutCleanMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n private readonly workingTreeGate: WorkingTreeGate,\n private readonly mainCheckout: MainCheckout,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);\n const stashed = this.mainCheckout.goToMain(repoRoot, untracked);\n this.reportUntracked(untracked, stashed);\n // No flags: this is the \"go to main and take out the trash\" path, so cleanup behaves exactly as a\n // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask\n // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever\n // grows a field, this call site is a compile error instead of a silent old behaviour.\n await this.cleanupCommand.run(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, false, ''),\n new DeleteSelection(FLAG_DELETE_WORKTREES, false, ''),\n false, false, false));\n this.sweep(repoRoot);\n this.reportStashed(stashed);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Say which untracked files were let through, AFTER the move — so nobody has to wonder whether the\n * files still sitting in their tree came along for the ride. Printed rather than refused on: see\n * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.\n *\n * Silent when the checkout had to stash: those files are NOT sitting in the tree any more, and\n * \"nothing about them changed\" would be a lie. `reportStashed` speaks for that case instead.\n */\n private reportUntracked(untracked: UntrackedFiles, stashed: StashedFiles): void {\n if (!stashed.isEmpty()) return;\n const rendered = untracked.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * The stash banner goes LAST, on purpose. It is the one thing in this command's output a reader\n * must not miss — their files left the working tree — and the cleanup and sweep that follow the\n * checkout print enough to scroll it away if it were emitted where it happened.\n */\n private reportStashed(stashed: StashedFiles): void {\n const rendered = stashed.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"wp-checkout-clean-main.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-checkout-clean-main.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAAqE;AACrE,+CAA0C;AAE1C,mGAAmG;AACnG,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,sGAAsG;IACtG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,YAAY,CAAC,IAAI,uBAAQ,CAC5C,wBAAwB,EACxB,8FAA8F,CAAC,CAAC,CAAC;IACrG,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,iBAAiB,EAAE,CAAC;AACvD,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs, CliUsage } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\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 `--help`/bogus flags BEFORE the app touches git — an ignored flag must never start the flow.\n container.get(CliArgs).assertNoArgs(new CliUsage(\n 'wp-checkout-clean-main',\n 'Go to main, fast-forward it, reap dead worktrees and branches, and sweep orphan directories.'));\n await container.get(PrGateApp).checkoutCleanMain();\n});\n"]}