@webpieces/pr-gate 0.4.552 → 0.4.553
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.
|
|
3
|
+
"version": "0.4.553",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.553",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -121,7 +121,7 @@ let LandedWorktreeReaper = class LandedWorktreeReaper {
|
|
|
121
121
|
const why = handoff.blockedBecause !== '' ? ` (${handoff.blockedBecause})\n` : '';
|
|
122
122
|
return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\n'
|
|
123
123
|
+ ' be removed from in here. Run cleanup from the primary clone instead:\n'
|
|
124
|
-
+ `
|
|
124
|
+
+ ` ${(0, rules_config_1.atRoot)(handoff.primaryPath, 'pnpm wp-cleanup')}\n`
|
|
125
125
|
+ why
|
|
126
126
|
+ ` It archives ${handoff.branch} as a tag, removes ${handoff.worktreePath}, then deletes the branch.\n`;
|
|
127
127
|
}
|
|
@@ -133,7 +133,9 @@ let LandedWorktreeReaper = class LandedWorktreeReaper {
|
|
|
133
133
|
afterReap(handoff) {
|
|
134
134
|
return `\n ⚠️ ${handoff.worktreePath} NO LONGER EXISTS — your shell is standing in a deleted\n`
|
|
135
135
|
+ ' directory, and every following command will fail until you move:\n'
|
|
136
|
-
|
|
136
|
+
// Single-quoted for the same reason atRoot() quotes: a primary clone under a path with a
|
|
137
|
+
// space (`/Users/dean hiller/…`, "Google Drive", iCloud) makes a bare `cd` two arguments.
|
|
138
|
+
+ ` cd '${handoff.primaryPath}'\n`;
|
|
137
139
|
}
|
|
138
140
|
/**
|
|
139
141
|
* WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"landed-worktree-reaper.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/landed-worktree-reaper.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { Worktree, WorktreeService } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The RE-EXEC half of `wp-land-pr`: reap the worktree the command is standing in, from a process that\n * is NOT standing in it.\n *\n * THE PROBLEM. `wp-land-pr` squash-merges the branch checked out in the worktree it runs in. The\n * moment that lands, both the branch and the directory are dead — but removing them from in here means\n * deleting the files underneath the running process, which is exactly what every rail in WorktreeReaper\n * refuses. #512 took this to an honest halfway point: print `cd <primary> && pnpm wp-cleanup` and stop.\n * Correct, and the single most-skipped step in the whole flow, because the PR is already landed and the\n * work FEELS done. Every skipped one leaves a corpse, and corpses are what trip branch-creation-guard.\n *\n * THE FIX. Do not reap in this process at all — hand the reap to a CHILD process whose `cwd` is the\n * PRIMARY CLONE. The child's cwd is a directory nobody is deleting, so `git worktree remove` is an\n * ordinary removal of somebody else's directory, and every existing rail still holds inside the child\n * (it refuses its own cwd, which is now the primary clone, and it refuses the primary clone by name).\n *\n * WHY `WorktreeService` AND NOT `EffectiveTreeResolver`. EffectiveTreeResolver (ai-hook-rules) answers\n * \"which tree does this BASH COMMAND STRING act on\" — it takes a command, resolves its leading `cd`\n * run, and classifies foreign/outside repos for the PreToolUse guards. There is no command string\n * here, pr-gate does not (and must not) depend on ai-hook-rules, and the only question being asked is\n * \"which of git's worktrees is the primary clone, and which one holds this branch\". That is\n * WorktreeService's whole job, and it is already the authority WorktreeReaper itself uses to decide\n * what is protected — using a second resolver would risk the two disagreeing about which tree we are\n * in, which for a directory deletion is the one disagreement nobody survives.\n *\n * WHY A CHILD AND NOT AN IN-PROCESS `chdir`. `process.chdir(primary)` would leave this node process's\n * own module graph rooted in a directory that is about to be deleted, and its lazily-resolved requires\n * with it. The child is spawned from THIS package's own files (same version — no skew with whatever\n * the primary clone has installed), and it finishes loading them before it removes anything.\n */\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which\n * directory, which branch, where the reap must run FROM, and — when it cannot run — why not.\n */\nexport class WorktreeReapHandoff {\n /** The linked worktree holding the branch that was just landed. */\n readonly worktreePath: string;\n readonly branch: string;\n /** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */\n readonly primaryPath: string;\n /** The compiled entry the child runs. '' when it could not be located on disk. */\n readonly entryScript: string;\n /** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */\n readonly blockedBecause: string;\n /** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */\n readonly canReap: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n worktreePath: string,\n branch: string,\n primaryPath: string,\n entryScript: string,\n blockedBecause: string,\n ) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n this.primaryPath = primaryPath;\n this.entryScript = entryScript;\n this.blockedBecause = blockedBecause;\n this.canReap = blockedBecause === '';\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class LandedWorktreeReaper {\n constructor(private readonly worktrees: WorktreeService) {}\n\n /**\n * Is there a worktree to reap at all, and can the hand-off run?\n *\n * `null` means \"nothing to do here\" — we are in the primary clone, or in a worktree that does not\n * hold the branch that just landed. That is the ordinary `pnpm wp-cleanup` case and needs no\n * special sentence. A non-null handoff with `canReap === false` means there IS a corpse but the\n * re-exec is not safely achievable, which is the case that must keep the manual notice.\n */\n plan(repoRoot: string, landedBranch: string): WorktreeReapHandoff | null {\n const here = this.worktrees.currentWorktree(repoRoot);\n if (here === null || here.isMain || here.branch !== landedBranch) return null;\n\n const primary = this.worktrees.listWorktrees(repoRoot)\n .find((tree: Worktree): boolean => tree.isMain);\n if (primary === undefined) {\n return new WorktreeReapHandoff(\n here.path, landedBranch, '<the primary clone>', '',\n 'git did not report a primary clone, so there is no safe directory to reap from');\n }\n\n const entry = this.reapEntryScript();\n if (entry === '') {\n return new WorktreeReapHandoff(\n here.path, landedBranch, primary.path, '',\n 'the reap entry point is not on disk (this package is running unbuilt)');\n }\n return new WorktreeReapHandoff(here.path, landedBranch, primary.path, entry, '');\n }\n\n /**\n * Run the reap in a child process rooted in the primary clone, and render what happened.\n *\n * stdin is `ignore` deliberately: the child must never be able to ask a question. A prompt printed\n * into a landing recap nobody is watching is not consent, and this reap is authorised by the merge\n * that just succeeded, not by an answer.\n */\n handOff(handoff: WorktreeReapHandoff): string {\n const result = spawnSync(\n process.execPath, [handoff.entryScript, handoff.worktreePath, handoff.branch],\n { cwd: handoff.primaryPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });\n\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd();\n const head = '\\n' + SEP + `🌲 Reaping this worktree from ${handoff.primaryPath}\\n` + SEP + '\\n'\n + ` The branch just landed, so this directory is dead. The reap runs in a child process\\n`\n + ` whose cwd is the primary clone — nothing deletes the directory it is standing in.\\n\\n`;\n\n if (result.status !== 0) {\n return head + (output !== '' ? output + '\\n' : '')\n + `\\n ⚠️ The reap did not complete (exit ${String(result.status ?? -1)}). Nothing was forced.\\n`\n + this.manualNotice(handoff);\n }\n return head + (output !== '' ? output + '\\n' : '') + this.afterReap(handoff);\n }\n\n /**\n * The #512 notice, kept verbatim in spirit for every case the re-exec cannot cover. An honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n manualNotice(handoff: WorktreeReapHandoff): string {\n const why = handoff.blockedBecause !== '' ? ` (${handoff.blockedBecause})\\n` : '';\n return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\\n'\n + ' be removed from in here. Run cleanup from the primary clone instead:\\n'\n + ` cd ${handoff.primaryPath} && pnpm wp-cleanup\\n`\n + why\n + ` It archives ${handoff.branch} as a tag, removes ${handoff.worktreePath}, then deletes the branch.\\n`;\n }\n\n /**\n * The one thing a human or agent MUST be told after a successful reap: the shell they typed this\n * into is now sitting in a directory that no longer exists. Every relative path from here on is a\n * mystery ENOENT unless they move.\n */\n private afterReap(handoff: WorktreeReapHandoff): string {\n return `\\n ⚠️ ${handoff.worktreePath} NO LONGER EXISTS — your shell is standing in a deleted\\n`\n + ' directory, and every following command will fail until you move:\\n'\n + ` cd ${handoff.primaryPath}\\n`;\n }\n\n /**\n * WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this\n * package runs from `.ts` sources and the compiled sibling does not exist.\n *\n * Resolved from `__dirname`, i.e. from THIS package's own installed files, NOT by remapping a path\n * into the primary clone. The primary clone may have a different @webpieces/pr-gate version\n * installed, and re-exec'ing a build that predates this feature would silently do something else.\n * The files are read into the child before it removes anything, so the directory going away\n * underneath them afterwards is harmless.\n */\n protected reapEntryScript(): string {\n const entry = path.join(__dirname, '..', 'wp-reap-worktree.js');\n return fs.existsSync(entry) ? entry : '';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"landed-worktree-reaper.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/landed-worktree-reaper.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { Worktree, WorktreeService, atRoot } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The RE-EXEC half of `wp-land-pr`: reap the worktree the command is standing in, from a process that\n * is NOT standing in it.\n *\n * THE PROBLEM. `wp-land-pr` squash-merges the branch checked out in the worktree it runs in. The\n * moment that lands, both the branch and the directory are dead — but removing them from in here means\n * deleting the files underneath the running process, which is exactly what every rail in WorktreeReaper\n * refuses. #512 took this to an honest halfway point: print `cd <primary> && pnpm wp-cleanup` and stop.\n * Correct, and the single most-skipped step in the whole flow, because the PR is already landed and the\n * work FEELS done. Every skipped one leaves a corpse, and corpses are what trip branch-creation-guard.\n *\n * THE FIX. Do not reap in this process at all — hand the reap to a CHILD process whose `cwd` is the\n * PRIMARY CLONE. The child's cwd is a directory nobody is deleting, so `git worktree remove` is an\n * ordinary removal of somebody else's directory, and every existing rail still holds inside the child\n * (it refuses its own cwd, which is now the primary clone, and it refuses the primary clone by name).\n *\n * WHY `WorktreeService` AND NOT `EffectiveTreeResolver`. EffectiveTreeResolver (ai-hook-rules) answers\n * \"which tree does this BASH COMMAND STRING act on\" — it takes a command, resolves its leading `cd`\n * run, and classifies foreign/outside repos for the PreToolUse guards. There is no command string\n * here, pr-gate does not (and must not) depend on ai-hook-rules, and the only question being asked is\n * \"which of git's worktrees is the primary clone, and which one holds this branch\". That is\n * WorktreeService's whole job, and it is already the authority WorktreeReaper itself uses to decide\n * what is protected — using a second resolver would risk the two disagreeing about which tree we are\n * in, which for a directory deletion is the one disagreement nobody survives.\n *\n * WHY A CHILD AND NOT AN IN-PROCESS `chdir`. `process.chdir(primary)` would leave this node process's\n * own module graph rooted in a directory that is about to be deleted, and its lazily-resolved requires\n * with it. The child is spawned from THIS package's own files (same version — no skew with whatever\n * the primary clone has installed), and it finishes loading them before it removes anything.\n */\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which\n * directory, which branch, where the reap must run FROM, and — when it cannot run — why not.\n */\nexport class WorktreeReapHandoff {\n /** The linked worktree holding the branch that was just landed. */\n readonly worktreePath: string;\n readonly branch: string;\n /** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */\n readonly primaryPath: string;\n /** The compiled entry the child runs. '' when it could not be located on disk. */\n readonly entryScript: string;\n /** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */\n readonly blockedBecause: string;\n /** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */\n readonly canReap: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n worktreePath: string,\n branch: string,\n primaryPath: string,\n entryScript: string,\n blockedBecause: string,\n ) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n this.primaryPath = primaryPath;\n this.entryScript = entryScript;\n this.blockedBecause = blockedBecause;\n this.canReap = blockedBecause === '';\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class LandedWorktreeReaper {\n constructor(private readonly worktrees: WorktreeService) {}\n\n /**\n * Is there a worktree to reap at all, and can the hand-off run?\n *\n * `null` means \"nothing to do here\" — we are in the primary clone, or in a worktree that does not\n * hold the branch that just landed. That is the ordinary `pnpm wp-cleanup` case and needs no\n * special sentence. A non-null handoff with `canReap === false` means there IS a corpse but the\n * re-exec is not safely achievable, which is the case that must keep the manual notice.\n */\n plan(repoRoot: string, landedBranch: string): WorktreeReapHandoff | null {\n const here = this.worktrees.currentWorktree(repoRoot);\n if (here === null || here.isMain || here.branch !== landedBranch) return null;\n\n const primary = this.worktrees.listWorktrees(repoRoot)\n .find((tree: Worktree): boolean => tree.isMain);\n if (primary === undefined) {\n return new WorktreeReapHandoff(\n here.path, landedBranch, '<the primary clone>', '',\n 'git did not report a primary clone, so there is no safe directory to reap from');\n }\n\n const entry = this.reapEntryScript();\n if (entry === '') {\n return new WorktreeReapHandoff(\n here.path, landedBranch, primary.path, '',\n 'the reap entry point is not on disk (this package is running unbuilt)');\n }\n return new WorktreeReapHandoff(here.path, landedBranch, primary.path, entry, '');\n }\n\n /**\n * Run the reap in a child process rooted in the primary clone, and render what happened.\n *\n * stdin is `ignore` deliberately: the child must never be able to ask a question. A prompt printed\n * into a landing recap nobody is watching is not consent, and this reap is authorised by the merge\n * that just succeeded, not by an answer.\n */\n handOff(handoff: WorktreeReapHandoff): string {\n const result = spawnSync(\n process.execPath, [handoff.entryScript, handoff.worktreePath, handoff.branch],\n { cwd: handoff.primaryPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });\n\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd();\n const head = '\\n' + SEP + `🌲 Reaping this worktree from ${handoff.primaryPath}\\n` + SEP + '\\n'\n + ` The branch just landed, so this directory is dead. The reap runs in a child process\\n`\n + ` whose cwd is the primary clone — nothing deletes the directory it is standing in.\\n\\n`;\n\n if (result.status !== 0) {\n return head + (output !== '' ? output + '\\n' : '')\n + `\\n ⚠️ The reap did not complete (exit ${String(result.status ?? -1)}). Nothing was forced.\\n`\n + this.manualNotice(handoff);\n }\n return head + (output !== '' ? output + '\\n' : '') + this.afterReap(handoff);\n }\n\n /**\n * The #512 notice, kept verbatim in spirit for every case the re-exec cannot cover. An honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n manualNotice(handoff: WorktreeReapHandoff): string {\n const why = handoff.blockedBecause !== '' ? ` (${handoff.blockedBecause})\\n` : '';\n return ' Next: this branch is checked out in THIS worktree, so neither it nor the worktree can\\n'\n + ' be removed from in here. Run cleanup from the primary clone instead:\\n'\n + ` ${atRoot(handoff.primaryPath, 'pnpm wp-cleanup')}\\n`\n + why\n + ` It archives ${handoff.branch} as a tag, removes ${handoff.worktreePath}, then deletes the branch.\\n`;\n }\n\n /**\n * The one thing a human or agent MUST be told after a successful reap: the shell they typed this\n * into is now sitting in a directory that no longer exists. Every relative path from here on is a\n * mystery ENOENT unless they move.\n */\n private afterReap(handoff: WorktreeReapHandoff): string {\n return `\\n ⚠️ ${handoff.worktreePath} NO LONGER EXISTS — your shell is standing in a deleted\\n`\n + ' directory, and every following command will fail until you move:\\n'\n // Single-quoted for the same reason atRoot() quotes: a primary clone under a path with a\n // space (`/Users/dean hiller/…`, \"Google Drive\", iCloud) makes a bare `cd` two arguments.\n + ` cd '${handoff.primaryPath}'\\n`;\n }\n\n /**\n * WHERE the child's entry point lives — a seam, overridden in the spec, because under vitest this\n * package runs from `.ts` sources and the compiled sibling does not exist.\n *\n * Resolved from `__dirname`, i.e. from THIS package's own installed files, NOT by remapping a path\n * into the primary clone. The primary clone may have a different @webpieces/pr-gate version\n * installed, and re-exec'ing a build that predates this feature would silently do something else.\n * The files are read into the child before it removes anything, so the directory going away\n * underneath them afterwards is harmless.\n */\n protected reapEntryScript(): string {\n const entry = path.join(__dirname, '..', 'wp-reap-worktree.js');\n return fs.existsSync(entry) ? entry : '';\n }\n}\n"]}
|