@webpieces/pr-gate 0.4.667 → 0.4.669

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.667",
3
+ "version": "0.4.669",
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",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.667",
18
+ "@webpieces/rules-config": "0.4.669",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -7,28 +7,26 @@ import { CleanupCommand } from './cleanup-command';
7
7
  * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE "DELETING" THAT) ────
8
8
  * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull
9
9
  * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW
10
- * messages should prescribe this instead — two spellings where one sweeps and one does not is shim shape
10
+ * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape
11
11
  * #1, an agent types whichever is accepted and the corpses accumulate forever.
12
12
  *
13
- * That swap is NOT in this change, and the reason is a constraint that outranks the shim rule: the raw
14
- * pair is a TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules
13
+ * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a
14
+ * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules
15
15
  * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of
16
16
  * the few commands still permitted while an L0 block denies every other tool call. In exactly that
17
17
  * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure
18
18
  * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in
19
- * `templates/ai-hook.sh`.
19
+ * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).
20
20
  *
21
- * So the end state is TWO LAYERS, not a deletion:
22
- * • L0 recovery (l0-allowlist, ai-hook.sh, l0-matrix) → keeps the raw pair. It is not a shim there;
23
- * it is the only thing that works when the package manager's output is what is in doubt.
24
- * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message, tree-recovery,
25
- * the L2 rows/doc, CLAUDE.md's "Finishing a Feature") → prescribes `pnpm wp-checkout-clean-main`,
26
- * and stops printing the pair.
21
+ * So the end state is TWO LAYERS, not a deletion, and it is now in place:
22
+ * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim
23
+ * there; it is the only thing that works when the package manager's output is what is in doubt.
24
+ * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,
25
+ * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's "Finishing a
26
+ * Feature") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.
27
27
  *
28
- * The second bullet is ~20 files of intertwined guard text with specs pinning exact strings, and it
29
- * lands as its own change deliberately after this one, so the bin exists in a published release
30
- * before any guard names it. Until then both spellings work and only this one sweeps; that is a known,
31
- * recorded gap, not an oversight.
28
+ * The pair remains ALLOWED as a Bash command in both layers this changed what the guards TEACH, not
29
+ * what they permit. Blocking it would have deleted the L0 escape along with the shim.
32
30
  *
33
31
  * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────
34
32
  * The corpses appear when git moves the working tree onto a base where the tracked files under some
@@ -14,28 +14,26 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
14
14
  * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE "DELETING" THAT) ────
15
15
  * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull
16
16
  * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW
17
- * messages should prescribe this instead — two spellings where one sweeps and one does not is shim shape
17
+ * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape
18
18
  * #1, an agent types whichever is accepted and the corpses accumulate forever.
19
19
  *
20
- * That swap is NOT in this change, and the reason is a constraint that outranks the shim rule: the raw
21
- * pair is a TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules
20
+ * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a
21
+ * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules
22
22
  * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of
23
23
  * the few commands still permitted while an L0 block denies every other tool call. In exactly that
24
24
  * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure
25
25
  * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in
26
- * `templates/ai-hook.sh`.
26
+ * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).
27
27
  *
28
- * So the end state is TWO LAYERS, not a deletion:
29
- * • L0 recovery (l0-allowlist, ai-hook.sh, l0-matrix) → keeps the raw pair. It is not a shim there;
30
- * it is the only thing that works when the package manager's output is what is in doubt.
31
- * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message, tree-recovery,
32
- * the L2 rows/doc, CLAUDE.md's "Finishing a Feature") → prescribes `pnpm wp-checkout-clean-main`,
33
- * and stops printing the pair.
28
+ * So the end state is TWO LAYERS, not a deletion, and it is now in place:
29
+ * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim
30
+ * there; it is the only thing that works when the package manager's output is what is in doubt.
31
+ * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,
32
+ * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's "Finishing a
33
+ * Feature") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.
34
34
  *
35
- * The second bullet is ~20 files of intertwined guard text with specs pinning exact strings, and it
36
- * lands as its own change deliberately after this one, so the bin exists in a published release
37
- * before any guard names it. Until then both spellings work and only this one sweeps; that is a known,
38
- * recorded gap, not an oversight.
35
+ * The pair remains ALLOWED as a Bash command in both layers this changed what the guards TEACH, not
36
+ * what they permit. Blocking it would have deleted the L0 escape along with the shim.
39
37
  *
40
38
  * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────
41
39
  * The corpses appear when git moves the working tree onto a base where the tracked files under some
@@ -1 +1 @@
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 { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport {\n CliExitError,\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\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 should 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 * That swap is NOT in this change, and the reason is a constraint that outranks the shim rule: the raw\n * pair is a 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`.\n *\n * So the end state is TWO LAYERS, not a deletion:\n * • L0 recovery (l0-allowlist, ai-hook.sh, l0-matrix) → keeps the raw pair. It is not a shim there;\n * 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, tree-recovery,\n * the L2 rows/doc, CLAUDE.md's \"Finishing a Feature\") → prescribes `pnpm wp-checkout-clean-main`,\n * and stops printing the pair.\n *\n * The second bullet is ~20 files of intertwined guard text with specs pinning exact strings, and it\n * lands as its own change — deliberately after this one, so the bin exists in a published release\n * before any guard names it. Until then both spellings work and only this one sweeps; that is a known,\n * recorded gap, not an oversight.\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 ) {}\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 this.assertTreeIsClean(repoRoot);\n this.goToMain(repoRoot);\n await this.cleanupCommand.run();\n this.sweep(repoRoot);\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 * Refuse on a dirty tree instead of carrying the changes onto main. `git checkout` would happily\n * bring uncommitted work along, and the one place nobody wants to discover their work is on main.\n */\n private assertTreeIsClean(repoRoot: string): void {\n const status = this.git(repoRoot, ['status', '--porcelain']);\n if (status === null || status.trim() === '') return;\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted or untracked changes\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or stash them, then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${status}`);\n }\n\n /**\n * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a\n * developer or an agent that accidentally committed to local main would have that work silently\n * destroyed by a reset, and the whole point of this command is that it is safe to run without\n * thinking. A refusal to fast-forward is a real condition a human should see and decide about.\n */\n private goToMain(repoRoot: string): void {\n this.run_(repoRoot, ['fetch', 'origin', 'main']);\n if (this.run_(repoRoot, ['checkout', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout — its message is above.\\n');\n }\n if (this.run_(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 }\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 /** 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 /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private run_(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":"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 { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport {\n CliExitError,\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\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 ) {}\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 this.assertTreeIsClean(repoRoot);\n this.goToMain(repoRoot);\n await this.cleanupCommand.run();\n this.sweep(repoRoot);\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 * Refuse on a dirty tree instead of carrying the changes onto main. `git checkout` would happily\n * bring uncommitted work along, and the one place nobody wants to discover their work is on main.\n */\n private assertTreeIsClean(repoRoot: string): void {\n const status = this.git(repoRoot, ['status', '--porcelain']);\n if (status === null || status.trim() === '') return;\n throw new CliExitError(1,\n `${SEP}❌ Uncommitted or untracked changes\\n${SEP}\\n`\n + 'Going to main would carry them along. Commit them on this branch, or stash them, then\\n'\n + 're-run. The webpieces tooling never commits your work for you.\\n\\n'\n + `${status}`);\n }\n\n /**\n * Checkout main and fast-forward it. `--ff-only` rather than a `reset --hard`, deliberately: a\n * developer or an agent that accidentally committed to local main would have that work silently\n * destroyed by a reset, and the whole point of this command is that it is safe to run without\n * thinking. A refusal to fast-forward is a real condition a human should see and decide about.\n */\n private goToMain(repoRoot: string): void {\n this.run_(repoRoot, ['fetch', 'origin', 'main']);\n if (this.run_(repoRoot, ['checkout', 'main']) !== 0) {\n throw new CliExitError(1, `${SEP}❌ Could not check out main\\n${SEP}\\n`\n + 'git refused the checkout — its message is above.\\n');\n }\n if (this.run_(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 }\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 /** 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 /** git with its output going straight to the terminal — for the commands whose progress is the point. */\n private run_(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"]}