@webpieces/pr-gate 0.4.493 → 0.4.494

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.493",
3
+ "version": "0.4.494",
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",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/tooling/pr-gate"
26
26
  },
27
27
  "dependencies": {
28
- "@webpieces/rules-config": "0.4.493",
28
+ "@webpieces/rules-config": "0.4.494",
29
29
  "@inversifyjs/binding-decorators": "1.1.5",
30
30
  "inversify": "7.10.4",
31
31
  "reflect-metadata": "0.2.2"
@@ -1,9 +1,20 @@
1
1
  import { RepoRootFinder } from '@webpieces/rules-config';
2
- /** 30-day cleanup of stale per-feature merge/pr-review dirs (and the legacy flat layout). */
2
+ /**
3
+ * 30-day garbage collection of the whole `.webpieces` tree.
4
+ *
5
+ * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:
6
+ * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and
7
+ * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).
8
+ *
9
+ * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on
10
+ * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state
11
+ * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a
12
+ * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review
13
+ * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.
14
+ */
3
15
  export declare class CleanTmp {
4
16
  private readonly repoRootFinder;
5
17
  constructor(repoRootFinder: RepoRootFinder);
6
18
  cleanTmp(): Promise<void>;
7
- private cleanStaleSubdirs;
8
- private cleanLegacyTopLevel;
19
+ private sweep;
9
20
  }
@@ -8,12 +8,28 @@ const rules_config_1 = require("@webpieces/rules-config");
8
8
  const inversify_1 = require("inversify");
9
9
  const CUTOFF_DAYS = 30;
10
10
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
11
- // Per-feature workflow dirs live under `.webpieces/merge-info/<feature>` and `.webpieces/pr-review/
12
- // <feature>`; those two homes are permanent (like hooks/ and instruct-ai/) and only their stale
13
- // subdirs are cleaned. `LEGACY_PREFIXES` sweeps the old flat top-level layout so it self-clears — which
14
- // also reaps the retired `pr-info/` home (it matches `pr-` and is no longer the active PR home).
15
- const LEGACY_PREFIXES = ['merge-', 'review-', 'pr-'];
16
- /** 30-day cleanup of stale per-feature merge/pr-review dirs (and the legacy flat layout). */
11
+ // Result of one sweep: how many files were reaped for age, and how many now-empty dirs were pruned.
12
+ class SweepResult {
13
+ files;
14
+ dirs;
15
+ constructor(files, dirs) {
16
+ this.files = files;
17
+ this.dirs = dirs;
18
+ }
19
+ }
20
+ /**
21
+ * 30-day garbage collection of the whole `.webpieces` tree.
22
+ *
23
+ * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:
24
+ * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and
25
+ * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).
26
+ *
27
+ * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on
28
+ * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state
29
+ * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a
30
+ * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review
31
+ * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.
32
+ */
17
33
  let CleanTmp = class CleanTmp {
18
34
  repoRootFinder;
19
35
  constructor(repoRootFinder) {
@@ -27,69 +43,58 @@ let CleanTmp = class CleanTmp {
27
43
  }
28
44
  process.stdout.write('\n');
29
45
  process.stdout.write(SEP);
30
- process.stdout.write('🧹 Cleaning Old Temporary Directories\n');
46
+ process.stdout.write('🧹 Garbage-Collecting .webpieces\n');
31
47
  process.stdout.write(SEP);
32
48
  process.stdout.write('\n');
33
49
  process.stdout.write(`Location: ${tmpBase}\n`);
34
- process.stdout.write(`Retention: ${CUTOFF_DAYS} days\n`);
50
+ process.stdout.write(`Retention: ${CUTOFF_DAYS} days (older files reaped; empty dirs pruned)\n`);
35
51
  process.stdout.write('\n');
36
52
  const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;
37
53
  const now = Date.now();
38
- let deletedCount = 0;
39
- // Stale per-feature subdirs under each permanent home.
40
- deletedCount += this.cleanStaleSubdirs(path.join(tmpBase, rules_config_1.MERGE_INFO_DIR), rules_config_1.MERGE_INFO_DIR, now, cutoffMs);
41
- deletedCount += this.cleanStaleSubdirs(path.join(tmpBase, rules_config_1.PR_REVIEW_DIR), rules_config_1.PR_REVIEW_DIR, now, cutoffMs);
42
- // Legacy flat top-level dirs from before the merge-info/pr-review nesting (also reaps old pr-info/).
43
- deletedCount += this.cleanLegacyTopLevel(tmpBase, now, cutoffMs);
44
- if (deletedCount === 0) {
45
- process.stdout.write(` ✅ No directories older than ${CUTOFF_DAYS} days found\n`);
54
+ // `true` => this is the root, which is swept into but never itself removed.
55
+ const result = this.sweep(tmpBase, tmpBase, now, cutoffMs, true);
56
+ if (result.files === 0 && result.dirs === 0) {
57
+ process.stdout.write(` ✅ Nothing older than ${CUTOFF_DAYS} days; no empty directories\n`);
46
58
  }
47
59
  else {
60
+ const fileWord = result.files === 1 ? 'file' : 'files';
61
+ const dirWord = result.dirs === 1 ? 'directory' : 'directories';
48
62
  process.stdout.write('\n');
49
- process.stdout.write(` ✅ Deleted ${deletedCount} old director${deletedCount === 1 ? 'y' : 'ies'}\n`);
63
+ process.stdout.write(` ✅ Reaped ${result.files} old ${fileWord} and pruned ${result.dirs} empty ${dirWord}\n`);
50
64
  }
51
65
  process.stdout.write('\n');
52
66
  process.stdout.write(SEP);
53
67
  process.stdout.write('\n');
54
68
  }
55
- // Delete immediate subdirs of `parentDir` whose mtime is older than the cutoff. No-op if absent.
56
- cleanStaleSubdirs(parentDir, label, now, cutoffMs) {
57
- if (!fs.existsSync(parentDir))
58
- return 0;
59
- let count = 0;
60
- for (const entry of fs.readdirSync(parentDir)) {
61
- const fullPath = path.join(parentDir, entry);
62
- const stat = fs.statSync(fullPath);
63
- if (!stat.isDirectory())
64
- continue;
65
- if (now - stat.mtimeMs < cutoffMs)
66
- continue;
67
- process.stdout.write(` 🗑️ Deleting: ${label}/${entry}\n`);
68
- fs.rmSync(fullPath, { recursive: true, force: true });
69
- count += 1;
69
+ // Depth-first, post-order sweep of `dir` (rooted at `tmpBase`, only used for tidy relative logging).
70
+ // Files older than the cutoff are deleted; a directory left empty once its children are processed is
71
+ // pruned — except the root, which is kept even when empty so `.webpieces/` itself survives.
72
+ sweep(dir, tmpBase, now, cutoffMs, isRoot) {
73
+ let files = 0;
74
+ let dirs = 0;
75
+ for (const entry of fs.readdirSync(dir)) {
76
+ const fullPath = path.join(dir, entry);
77
+ // lstat, not stat: a symlink is treated as a leaf (aged out like a file), never followed —
78
+ // so we can never wander outside `.webpieces` or delete a link target elsewhere.
79
+ const stat = fs.lstatSync(fullPath);
80
+ if (stat.isDirectory()) {
81
+ const nested = this.sweep(fullPath, tmpBase, now, cutoffMs, false);
82
+ files += nested.files;
83
+ dirs += nested.dirs;
84
+ }
85
+ else if (now - stat.mtimeMs >= cutoffMs) {
86
+ process.stdout.write(` 🗑️ file: ${path.relative(tmpBase, fullPath)}\n`);
87
+ fs.rmSync(fullPath, { force: true });
88
+ files += 1;
89
+ }
70
90
  }
71
- return count;
72
- }
73
- // Sweep the pre-nesting flat layout (`merge-<feature>` etc.), skipping the current homes so this
74
- // never deletes merge-info/ or pr-review/ themselves. Retired `pr-info/` IS swept (matches `pr-`).
75
- cleanLegacyTopLevel(tmpBase, now, cutoffMs) {
76
- let count = 0;
77
- for (const entry of fs.readdirSync(tmpBase)) {
78
- if (entry === rules_config_1.MERGE_INFO_DIR || entry === rules_config_1.PR_REVIEW_DIR)
79
- continue;
80
- if (!LEGACY_PREFIXES.some((prefix) => entry.startsWith(prefix)))
81
- continue;
82
- const fullPath = path.join(tmpBase, entry);
83
- const stat = fs.statSync(fullPath);
84
- if (!stat.isDirectory())
85
- continue;
86
- if (now - stat.mtimeMs < cutoffMs)
87
- continue;
88
- process.stdout.write(` 🗑️ Deleting: ${entry}\n`);
89
- fs.rmSync(fullPath, { recursive: true, force: true });
90
- count += 1;
91
+ // Post-order: prune this dir if reaping its children (or nothing) left it empty.
92
+ if (!isRoot && fs.readdirSync(dir).length === 0) {
93
+ process.stdout.write(` 🗑️ dir: ${path.relative(tmpBase, dir)}/\n`);
94
+ fs.rmdirSync(dir);
95
+ dirs += 1;
91
96
  }
92
- return count;
97
+ return new SweepResult(files, dirs);
93
98
  }
94
99
  };
95
100
  exports.CleanTmp = CleanTmp;
@@ -1 +1 @@
1
- {"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { WEBPIECES_TMP_DIR, MERGE_INFO_DIR, PR_REVIEW_DIR, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst CUTOFF_DAYS = 30;\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Per-feature workflow dirs live under `.webpieces/merge-info/<feature>` and `.webpieces/pr-review/\n// <feature>`; those two homes are permanent (like hooks/ and instruct-ai/) and only their stale\n// subdirs are cleaned. `LEGACY_PREFIXES` sweeps the old flat top-level layout so it self-clears which\n// also reaps the retired `pr-info/` home (it matches `pr-` and is no longer the active PR home).\nconst LEGACY_PREFIXES = ['merge-', 'review-', 'pr-'];\n\n/** 30-day cleanup of stale per-feature merge/pr-review dirs (and the legacy flat layout). */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanTmp {\n constructor(private readonly repoRootFinder: RepoRootFinder) {}\n\n async cleanTmp(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const tmpBase = path.join(repoRoot, WEBPIECES_TMP_DIR);\n\n if (!fs.existsSync(tmpBase)) {\n return;\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('🧹 Cleaning Old Temporary Directories\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n process.stdout.write(`Location: ${tmpBase}\\n`);\n process.stdout.write(`Retention: ${CUTOFF_DAYS} days\\n`);\n process.stdout.write('\\n');\n\n const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n let deletedCount = 0;\n\n // Stale per-feature subdirs under each permanent home.\n deletedCount += this.cleanStaleSubdirs(path.join(tmpBase, MERGE_INFO_DIR), MERGE_INFO_DIR, now, cutoffMs);\n deletedCount += this.cleanStaleSubdirs(path.join(tmpBase, PR_REVIEW_DIR), PR_REVIEW_DIR, now, cutoffMs);\n // Legacy flat top-level dirs from before the merge-info/pr-review nesting (also reaps old pr-info/).\n deletedCount += this.cleanLegacyTopLevel(tmpBase, now, cutoffMs);\n\n if (deletedCount === 0) {\n process.stdout.write(` ✅ No directories older than ${CUTOFF_DAYS} days found\\n`);\n } else {\n process.stdout.write('\\n');\n process.stdout.write(` ✅ Deleted ${deletedCount} old director${deletedCount === 1 ? 'y' : 'ies'}\\n`);\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n }\n\n // Delete immediate subdirs of `parentDir` whose mtime is older than the cutoff. No-op if absent.\n private cleanStaleSubdirs(parentDir: string, label: string, now: number, cutoffMs: number): number {\n if (!fs.existsSync(parentDir)) return 0;\n let count = 0;\n for (const entry of fs.readdirSync(parentDir)) {\n const fullPath = path.join(parentDir, entry);\n const stat = fs.statSync(fullPath);\n if (!stat.isDirectory()) continue;\n if (now - stat.mtimeMs < cutoffMs) continue;\n process.stdout.write(` 🗑️ Deleting: ${label}/${entry}\\n`);\n fs.rmSync(fullPath, { recursive: true, force: true });\n count += 1;\n }\n return count;\n }\n\n // Sweep the pre-nesting flat layout (`merge-<feature>` etc.), skipping the current homes so this\n // never deletes merge-info/ or pr-review/ themselves. Retired `pr-info/` IS swept (matches `pr-`).\n private cleanLegacyTopLevel(tmpBase: string, now: number, cutoffMs: number): number {\n let count = 0;\n for (const entry of fs.readdirSync(tmpBase)) {\n if (entry === MERGE_INFO_DIR || entry === PR_REVIEW_DIR) continue;\n if (!LEGACY_PREFIXES.some((prefix: string): boolean => entry.startsWith(prefix))) continue;\n const fullPath = path.join(tmpBase, entry);\n const stat = fs.statSync(fullPath);\n if (!stat.isDirectory()) continue;\n if (now - stat.mtimeMs < cutoffMs) continue;\n process.stdout.write(` 🗑️ Deleting: ${entry}\\n`);\n fs.rmSync(fullPath, { recursive: true, force: true });\n count += 1;\n }\n return count;\n }\n}\n"]}
1
+ {"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { WEBPIECES_TMP_DIR, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst CUTOFF_DAYS = 30;\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Result of one sweep: how many files were reaped for age, and how many now-empty dirs were pruned.\nclass SweepResult {\n constructor(\n public files: number,\n public dirs: number,\n ) {}\n}\n\n/**\n * 30-day garbage collection of the whole `.webpieces` tree.\n *\n * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:\n * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and\n * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).\n *\n * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on\n * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state\n * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a\n * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review\n * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanTmp {\n constructor(private readonly repoRootFinder: RepoRootFinder) {}\n\n async cleanTmp(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const tmpBase = path.join(repoRoot, WEBPIECES_TMP_DIR);\n\n if (!fs.existsSync(tmpBase)) {\n return;\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('🧹 Garbage-Collecting .webpieces\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n process.stdout.write(`Location: ${tmpBase}\\n`);\n process.stdout.write(`Retention: ${CUTOFF_DAYS} days (older files reaped; empty dirs pruned)\\n`);\n process.stdout.write('\\n');\n\n const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // `true` => this is the root, which is swept into but never itself removed.\n const result = this.sweep(tmpBase, tmpBase, now, cutoffMs, true);\n\n if (result.files === 0 && result.dirs === 0) {\n process.stdout.write(` ✅ Nothing older than ${CUTOFF_DAYS} days; no empty directories\\n`);\n } else {\n const fileWord = result.files === 1 ? 'file' : 'files';\n const dirWord = result.dirs === 1 ? 'directory' : 'directories';\n process.stdout.write('\\n');\n process.stdout.write(` ✅ Reaped ${result.files} old ${fileWord} and pruned ${result.dirs} empty ${dirWord}\\n`);\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n }\n\n // Depth-first, post-order sweep of `dir` (rooted at `tmpBase`, only used for tidy relative logging).\n // Files older than the cutoff are deleted; a directory left empty once its children are processed is\n // pruned — except the root, which is kept even when empty so `.webpieces/` itself survives.\n private sweep(dir: string, tmpBase: string, now: number, cutoffMs: number, isRoot: boolean): SweepResult {\n let files = 0;\n let dirs = 0;\n\n for (const entry of fs.readdirSync(dir)) {\n const fullPath = path.join(dir, entry);\n // lstat, not stat: a symlink is treated as a leaf (aged out like a file), never followed —\n // so we can never wander outside `.webpieces` or delete a link target elsewhere.\n const stat = fs.lstatSync(fullPath);\n if (stat.isDirectory()) {\n const nested = this.sweep(fullPath, tmpBase, now, cutoffMs, false);\n files += nested.files;\n dirs += nested.dirs;\n } else if (now - stat.mtimeMs >= cutoffMs) {\n process.stdout.write(` 🗑️ file: ${path.relative(tmpBase, fullPath)}\\n`);\n fs.rmSync(fullPath, { force: true });\n files += 1;\n }\n }\n\n // Post-order: prune this dir if reaping its children (or nothing) left it empty.\n if (!isRoot && fs.readdirSync(dir).length === 0) {\n process.stdout.write(` 🗑️ dir: ${path.relative(tmpBase, dir)}/\\n`);\n fs.rmdirSync(dir);\n dirs += 1;\n }\n\n return new SweepResult(files, dirs);\n }\n}\n"]}