pr-checkmate 1.21.0 → 1.21.2

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.
@@ -15,31 +15,43 @@ async function runDependencyCheck() {
15
15
  utils_1.logger.warn('[runDependencyCheck]: ⚠️ No package.json found, skipping');
16
16
  return;
17
17
  }
18
+ // execa v9 always throws on non-zero exit; depcheck exits 255 when it finds issues
19
+ // but the JSON result is still in error.stdout — extract it either way
20
+ let stdout;
18
21
  try {
19
- const { stdout } = await (0, execa_1.execa)('npx', ['depcheck', '--json'], { cwd: projectRoot });
20
- const result = JSON.parse(stdout);
21
- const unused = result.dependencies ?? [];
22
- const missing = Object.keys(result.missing ?? {});
23
- if (unused.length === 0 && missing.length === 0) {
24
- utils_1.logger.info('[runDependencyCheck]: ✅ Dependencies are clean');
25
- return;
26
- }
27
- if (unused.length > 0) {
28
- utils_1.logger.warn(`[runDependencyCheck]: ⚠️ ${unused.length} unused dependency(ies):`);
29
- unused.forEach(dep => utils_1.logger.warn(` ${dep}`));
30
- }
31
- if (missing.length > 0) {
32
- utils_1.logger.error(`[runDependencyCheck]: ❌ ${missing.length} missing dependency(ies):`);
33
- missing.forEach(dep => utils_1.logger.error(` ${dep} (used in: ${result.missing[dep]?.join(', ')})`));
34
- throw new Error(`Missing dependencies: ${missing.join(', ')}`);
35
- }
36
- // Unused deps are warnings — don't block the build
22
+ ({ stdout } = await (0, execa_1.execa)('npx', ['depcheck', '--json'], { cwd: projectRoot }));
37
23
  }
38
24
  catch (err) {
39
- const error = err;
40
- if (error.message?.startsWith('Missing dependencies'))
25
+ if (err instanceof execa_1.ExecaError && err.stdout) {
26
+ stdout = err.stdout;
27
+ }
28
+ else {
29
+ utils_1.logger.error(`[runDependencyCheck]: ❌ Dependency check failed\n${err}`);
41
30
  throw err;
42
- utils_1.logger.error(`[runDependencyCheck]: ❌ Dependency check failed\n${err}`);
43
- throw err;
31
+ }
32
+ }
33
+ let result;
34
+ try {
35
+ result = JSON.parse(stdout);
36
+ }
37
+ catch {
38
+ utils_1.logger.error('[runDependencyCheck]: ❌ Failed to parse depcheck output');
39
+ throw new Error('depcheck produced invalid JSON');
40
+ }
41
+ const unused = result.dependencies ?? [];
42
+ const missing = Object.keys(result.missing ?? {});
43
+ if (unused.length === 0 && missing.length === 0) {
44
+ utils_1.logger.info('[runDependencyCheck]: ✅ Dependencies are clean');
45
+ return;
46
+ }
47
+ if (unused.length > 0) {
48
+ utils_1.logger.warn(`[runDependencyCheck]: ⚠️ ${unused.length} unused dependency(ies):`);
49
+ unused.forEach(dep => utils_1.logger.warn(` ${dep}`));
50
+ }
51
+ if (missing.length > 0) {
52
+ utils_1.logger.error(`[runDependencyCheck]: ❌ ${missing.length} missing dependency(ies):`);
53
+ missing.forEach(dep => utils_1.logger.error(` ${dep} (used in: ${result.missing[dep]?.join(', ')})`));
54
+ throw new Error(`Missing dependencies: ${missing.join(', ')}`);
44
55
  }
56
+ // Unused deps are warnings — don't block the build
45
57
  }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDiffSmell = runDiffSmell;
7
+ const execa_1 = require("execa");
8
+ const utils_1 = require("../utils");
9
+ const result_collector_1 = require("../utils/result-collector");
10
+ const loader_1 = require("../config/loader");
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const SMELL_PATTERNS = [
13
+ // console.* calls added in diff (not in comments)
14
+ { pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/, label: 'console.*()' },
15
+ { pattern: /^\+(?!\+).*\bdebugger\b/, label: 'debugger statement' },
16
+ { pattern: /^\+\s*\/\/\s*TODO\b/i, label: 'TODO comment' },
17
+ { pattern: /^\+\s*\/\/\s*FIXME\b/i, label: 'FIXME comment' },
18
+ { pattern: /^\+\s*\/\/\s*HACK\b/i, label: 'HACK comment' },
19
+ { pattern: /^\+\s*\/\/\s*XXX\b/i, label: 'XXX marker' },
20
+ ];
21
+ function readBaseSha() {
22
+ const eventName = process.env.GITHUB_EVENT_NAME;
23
+ const eventPath = process.env.GITHUB_EVENT_PATH;
24
+ if (eventName !== 'pull_request' || !eventPath || !node_fs_1.default.existsSync(eventPath)) {
25
+ return null;
26
+ }
27
+ try {
28
+ const event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
29
+ return event?.pull_request?.base?.sha ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ async function runDiffSmell() {
36
+ const cfg = (0, loader_1.loadProjectConfig)();
37
+ if (cfg.diffSmell?.enabled === false) {
38
+ utils_1.logger.info('[runDiffSmell]: Disabled in pr-checkmate.json, skipping');
39
+ result_collector_1.resultCollector.register('Diff Smells', 'skip');
40
+ return;
41
+ }
42
+ utils_1.logger.info('[runDiffSmell]: Scanning diff for code smells...');
43
+ const baseSha = readBaseSha();
44
+ if (!baseSha) {
45
+ utils_1.logger.info('[runDiffSmell]: Not in GitHub PR context, skipping');
46
+ result_collector_1.resultCollector.register('Diff Smells', 'skip');
47
+ return;
48
+ }
49
+ try {
50
+ const { stdout } = await (0, execa_1.execa)('git', [
51
+ 'diff',
52
+ '--unified=0',
53
+ '--diff-filter=ACMR',
54
+ baseSha,
55
+ 'HEAD',
56
+ '--',
57
+ '*.ts',
58
+ '*.tsx',
59
+ '*.js',
60
+ '*.jsx',
61
+ ]);
62
+ const lines = stdout.split('\n');
63
+ const findings = {};
64
+ for (const line of lines) {
65
+ for (const { pattern, label } of SMELL_PATTERNS) {
66
+ if (pattern.test(line)) {
67
+ if (!findings[label])
68
+ findings[label] = [];
69
+ // Strip leading + and trim for display
70
+ findings[label].push(line.slice(1).trim().substring(0, 100));
71
+ }
72
+ }
73
+ }
74
+ const totalFindings = Object.values(findings).reduce((sum, arr) => sum + arr.length, 0);
75
+ if (totalFindings === 0) {
76
+ utils_1.logger.info('[runDiffSmell]: ✅ No code smells detected in diff');
77
+ result_collector_1.resultCollector.register('Diff Smells', 'pass');
78
+ return;
79
+ }
80
+ utils_1.logger.warn(`[runDiffSmell]: ⚠️ Found ${totalFindings} code smell(s) in diff:`);
81
+ for (const [label, occurrences] of Object.entries(findings)) {
82
+ utils_1.logger.warn(` ${label}: ${occurrences.length} occurrence(s)`);
83
+ occurrences.slice(0, 3).forEach(line => utils_1.logger.warn(` ${line}`));
84
+ if (occurrences.length > 3) {
85
+ utils_1.logger.warn(` ... and ${occurrences.length - 3} more`);
86
+ }
87
+ }
88
+ const summary = Object.entries(findings)
89
+ .map(([label, arr]) => `${arr.length}× ${label}`)
90
+ .join(', ');
91
+ result_collector_1.resultCollector.register('Diff Smells', 'warn', summary);
92
+ }
93
+ catch (err) {
94
+ utils_1.logger.warn(`[runDiffSmell]: ⚠️ Could not analyze diff\n${err}`);
95
+ result_collector_1.resultCollector.register('Diff Smells', 'skip');
96
+ }
97
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDuplicateCheck = runDuplicateCheck;
7
+ const execa_1 = require("execa");
8
+ const utils_1 = require("../utils");
9
+ const result_collector_1 = require("../utils/result-collector");
10
+ const loader_1 = require("../config/loader");
11
+ const delta_1 = require("../utils/delta");
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ async function runDuplicateCheck() {
15
+ const cfg = (0, loader_1.loadProjectConfig)();
16
+ if (cfg.duplicate?.enabled === false) {
17
+ utils_1.logger.info('[runDuplicateCheck]: Disabled in pr-checkmate.json, skipping');
18
+ result_collector_1.resultCollector.register('Duplicate Code', 'skip');
19
+ return;
20
+ }
21
+ const minLines = cfg.duplicate?.minLines ?? 5;
22
+ const minTokens = cfg.duplicate?.minTokens ?? 50;
23
+ utils_1.logger.info('[runDuplicateCheck]: Checking for duplicate code...');
24
+ const projectRoot = process.cwd();
25
+ const changedFiles = await (0, delta_1.getChangedFiles)(['.ts', '.tsx', '.js', '.jsx']);
26
+ // In PR context but no relevant files changed
27
+ if (changedFiles !== null && changedFiles.length === 0) {
28
+ utils_1.logger.info('[runDuplicateCheck]: No relevant files changed, skipping');
29
+ result_collector_1.resultCollector.register('Duplicate Code', 'skip');
30
+ return;
31
+ }
32
+ // Fall back to src/ or project root when not in delta mode
33
+ const srcPath = node_path_1.default.join(projectRoot, 'src');
34
+ const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [projectRoot]);
35
+ try {
36
+ const result = await (0, execa_1.execa)('npx', [
37
+ 'jscpd',
38
+ '--min-lines',
39
+ String(minLines),
40
+ '--min-tokens',
41
+ String(minTokens),
42
+ '--reporters',
43
+ 'console',
44
+ '--ignore',
45
+ 'node_modules/**,dist/**,.git/**',
46
+ '--quiet',
47
+ ...targetPaths,
48
+ ], { reject: false });
49
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
50
+ const clonesMatch = output.match(/(\d+)\s+clone/i);
51
+ const cloneCount = clonesMatch ? parseInt(clonesMatch[1], 10) : 0;
52
+ if (cloneCount > 0) {
53
+ utils_1.logger.warn(`[runDuplicateCheck]: ⚠️ Found ${cloneCount} duplicate block(s)`);
54
+ if (output)
55
+ utils_1.logger.warn(output);
56
+ result_collector_1.resultCollector.register('Duplicate Code', 'warn', `${cloneCount} duplicate block(s)`);
57
+ }
58
+ else {
59
+ utils_1.logger.info('[runDuplicateCheck]: ✅ No significant code duplication found');
60
+ result_collector_1.resultCollector.register('Duplicate Code', 'pass');
61
+ }
62
+ }
63
+ catch (err) {
64
+ utils_1.logger.warn(`[runDuplicateCheck]: ⚠️ Could not run duplicate check\n${err}`);
65
+ result_collector_1.resultCollector.register('Duplicate Code', 'skip');
66
+ }
67
+ }
@@ -25,7 +25,14 @@ const license_check_1 = require("./license-check");
25
25
  const pr_size_check_1 = require("./pr-size-check");
26
26
  const pr_title_lint_1 = require("./pr-title-lint");
27
27
  const outdated_deps_1 = require("./outdated-deps");
28
+ const markdown_lint_1 = require("./markdown-lint");
29
+ const typecheck_1 = require("./typecheck");
30
+ const diff_smell_1 = require("./diff-smell");
31
+ const pr_body_1 = require("./pr-body");
32
+ const duplicate_check_1 = require("./duplicate-check");
33
+ const pr_summary_1 = require("./pr-summary");
28
34
  const utils_1 = require("../utils");
35
+ const result_collector_1 = require("../utils/result-collector");
29
36
  const lint_1 = require("./lint");
30
37
  __exportStar(require("./dependency-check"), exports);
31
38
  __exportStar(require("./lint"), exports);
@@ -37,34 +44,70 @@ __exportStar(require("./license-check"), exports);
37
44
  __exportStar(require("./pr-size-check"), exports);
38
45
  __exportStar(require("./pr-title-lint"), exports);
39
46
  __exportStar(require("./outdated-deps"), exports);
47
+ __exportStar(require("./markdown-lint"), exports);
48
+ __exportStar(require("./typecheck"), exports);
49
+ __exportStar(require("./diff-smell"), exports);
50
+ __exportStar(require("./pr-body"), exports);
51
+ __exportStar(require("./duplicate-check"), exports);
52
+ __exportStar(require("./pr-summary"), exports);
53
+ /**
54
+ * Wraps a blocking check: registers 'pass' on success, 'fail' on throw (and re-throws).
55
+ */
56
+ async function trackCheck(name, fn) {
57
+ try {
58
+ await fn();
59
+ result_collector_1.resultCollector.register(name, 'pass');
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message.split('\n')[0].substring(0, 120) : String(err);
63
+ result_collector_1.resultCollector.register(name, 'fail', msg);
64
+ throw err;
65
+ }
66
+ }
67
+ /**
68
+ * Wraps an informational check: registers 'pass' on success, 'warn' on throw (never re-throws).
69
+ */
70
+ async function trackSoftCheck(name, fn) {
71
+ try {
72
+ await fn();
73
+ result_collector_1.resultCollector.register(name, 'pass');
74
+ }
75
+ catch (err) {
76
+ const msg = err instanceof Error ? err.message.split('\n')[0].substring(0, 120) : String(err);
77
+ result_collector_1.resultCollector.register(name, 'warn', msg);
78
+ }
79
+ }
40
80
  async function runJob(jobName) {
41
81
  utils_1.logger.info(`[runJob]: 🚀 Running job: ${jobName}`);
42
82
  try {
43
83
  switch (jobName) {
44
84
  case 'all':
45
- // Phase 1: blocking checks in parallel
85
+ // Phase 1: blocking quality gates — run in parallel, fail fast on any throw
46
86
  await Promise.all([
47
- (0, lint_1.runLint)(),
48
- (0, dependency_check_1.runDependencyCheck)(),
49
- (0, npm_audit_1.runNpmAudit)(),
50
- (0, security_1.runSecurityScan)(),
51
- (0, circular_deps_1.runCircularDeps)(),
52
- (0, license_check_1.runLicenseCheck)(),
87
+ trackCheck('ESLint', lint_1.runLint),
88
+ trackCheck('Dependencies', dependency_check_1.runDependencyCheck),
89
+ trackCheck('NPM Audit', npm_audit_1.runNpmAudit),
90
+ trackCheck('Security Scan', security_1.runSecurityScan),
91
+ trackCheck('Circular Deps', circular_deps_1.runCircularDeps),
92
+ trackCheck('License Check', license_check_1.runLicenseCheck),
93
+ trackCheck('TypeScript', typecheck_1.runTypecheck),
53
94
  ]);
54
- // Phase 2: informational checks in parallel (never block)
95
+ // Phase 2: informational checks — old scripts tracked via wrapper,
96
+ // new scripts register themselves with accurate warn/skip status
55
97
  await Promise.all([
56
- (0, pr_size_check_1.runPrSizeCheck)(),
57
- (0, pr_title_lint_1.runPrTitleLint)(),
58
- (0, outdated_deps_1.runOutdatedDeps)(),
59
- (0, spellcheck_1.runSpellcheck)().catch(() => utils_1.logger.warn('⚠️ Spellcheck found issues, continuing...')),
98
+ trackSoftCheck('PR Size', pr_size_check_1.runPrSizeCheck),
99
+ trackSoftCheck('PR Title', pr_title_lint_1.runPrTitleLint),
100
+ trackSoftCheck('Outdated Deps', outdated_deps_1.runOutdatedDeps),
101
+ trackSoftCheck('Markdown', markdown_lint_1.runMarkdownLint),
102
+ trackSoftCheck('Spellcheck', spellcheck_1.runSpellcheck),
103
+ (0, diff_smell_1.runDiffSmell)(),
104
+ (0, pr_body_1.runPrBodyCheck)(),
105
+ (0, duplicate_check_1.runDuplicateCheck)(),
60
106
  ]);
61
- // Phase 3: prettier last — writes files
62
- try {
63
- await (0, prettier_autoformat_1.runPrettier)();
64
- }
65
- catch {
66
- utils_1.logger.warn('⚠️ Prettier failed, continuing...');
67
- }
107
+ // Phase 3: prettier last — may write files, then auto-commit in CI
108
+ await trackSoftCheck('Prettier', prettier_autoformat_1.runPrettier);
109
+ // Phase 4: print summary table + post GitHub PR comment
110
+ await (0, pr_summary_1.runPrSummary)();
68
111
  break;
69
112
  case 'lint':
70
113
  await (0, lint_1.runLint)();
@@ -100,8 +143,23 @@ async function runJob(jobName) {
100
143
  case 'outdated':
101
144
  await (0, outdated_deps_1.runOutdatedDeps)();
102
145
  break;
146
+ case 'markdown':
147
+ await (0, markdown_lint_1.runMarkdownLint)();
148
+ break;
149
+ case 'typecheck':
150
+ await (0, typecheck_1.runTypecheck)();
151
+ break;
152
+ case 'diff-smell':
153
+ await (0, diff_smell_1.runDiffSmell)();
154
+ break;
155
+ case 'pr-body':
156
+ await (0, pr_body_1.runPrBodyCheck)();
157
+ break;
158
+ case 'duplicate':
159
+ await (0, duplicate_check_1.runDuplicateCheck)();
160
+ break;
103
161
  default:
104
- utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated`);
162
+ utils_1.logger.warn(`[runJob]: Unknown job "${jobName}". Available: all, lint, deps, npm-audit, prettier, spellcheck, security, circular, license, pr-size, pr-title, outdated, markdown, typecheck, diff-smell, pr-body, duplicate`);
105
163
  }
106
164
  }
107
165
  catch (err) {
@@ -22,7 +22,8 @@ async function runLicenseCheck() {
22
22
  const licenses = JSON.parse(stdout);
23
23
  const violations = [];
24
24
  for (const [pkg, info] of Object.entries(licenses)) {
25
- const license = info.licenses ?? '';
25
+ const raw = info.licenses ?? '';
26
+ const license = Array.isArray(raw) ? raw.join(' OR ') : raw;
26
27
  if (FORBIDDEN_LICENSES.some(f => license.includes(f))) {
27
28
  violations.push(` ${pkg}: ${license}`);
28
29
  }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runMarkdownLint = runMarkdownLint;
7
+ const execa_1 = require("execa");
8
+ const utils_1 = require("../utils");
9
+ const delta_1 = require("../utils/delta");
10
+ const config_1 = require("../config");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ function loadUserMarkdownConfig() {
14
+ const configPath = node_path_1.default.join(process.cwd(), 'pr-checkmate.json');
15
+ if (!node_fs_1.default.existsSync(configPath)) {
16
+ return {};
17
+ }
18
+ try {
19
+ const config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
20
+ return config.markdownlint || {};
21
+ }
22
+ catch (err) {
23
+ utils_1.logger.warn(`[loadUserMarkdownConfig]: ⚠️ Failed to read markdownlint config from pr-checkmate.json: ${err}`);
24
+ return {};
25
+ }
26
+ }
27
+ async function runMarkdownLint() {
28
+ utils_1.logger.info('[runMarkdownLint]: 📝 Running Markdown lint...');
29
+ const root = process.cwd();
30
+ const changedFiles = await (0, delta_1.getChangedFiles)(['.md', '.mdx']);
31
+ const targets = changedFiles ?? [`${root}/**/*.md`];
32
+ const userConfig = loadUserMarkdownConfig();
33
+ // Prefer local .markdownlint.json → else fall back to bundled one
34
+ let configArg;
35
+ if ((0, config_1.hasLocalConfig)(['.markdownlint.json', '.markdownlint.jsonc', '.markdownlint.yaml', '.markdownlint.yml'])) {
36
+ utils_1.logger.info('[runMarkdownLint]: 📋 Using local .markdownlint config');
37
+ configArg = [];
38
+ }
39
+ else {
40
+ const bundledConfig = (0, config_1.getPackageConfigPath)('.markdownlint.json');
41
+ utils_1.logger.info('[runMarkdownLint]: 📋 Using pr-checkmate bundled markdownlint config');
42
+ configArg = ['--config', bundledConfig];
43
+ }
44
+ const extraIgnores = (userConfig.ignores ?? []).flatMap(p => ['--ignore', p]);
45
+ const args = [
46
+ 'markdownlint',
47
+ ...targets,
48
+ ...configArg,
49
+ '--ignore', 'node_modules',
50
+ '--ignore', 'dist',
51
+ '--ignore', 'coverage',
52
+ ...extraIgnores,
53
+ ];
54
+ const result = await (0, execa_1.execa)('npx', args, { reject: false, stdio: 'inherit' });
55
+ if (result.exitCode === 0) {
56
+ utils_1.logger.info('[runMarkdownLint]: ✅ Markdown lint passed');
57
+ }
58
+ else {
59
+ utils_1.logger.warn('[runMarkdownLint]: ⚠️ Markdown lint found issues (see above)');
60
+ // warning only — never block
61
+ }
62
+ }
@@ -8,8 +8,16 @@ const utils_1 = require("../utils");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const execa_1 = require("execa");
11
+ const config_1 = require("../config");
12
+ const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
13
+ function isSeverityAtLeast(severity, level) {
14
+ const sevIdx = SEVERITY_ORDER.indexOf(severity);
15
+ const lvlIdx = SEVERITY_ORDER.indexOf(level);
16
+ return sevIdx >= 0 && lvlIdx >= 0 && sevIdx >= lvlIdx;
17
+ }
11
18
  /**
12
- * Run npm audit on the current project
19
+ * Run npm audit on the current project.
20
+ * Only fails on vulnerabilities in direct dependencies — transitive ones are silently ignored.
13
21
  */
14
22
  async function runNpmAudit() {
15
23
  utils_1.logger.info('Running npm audit...');
@@ -19,25 +27,27 @@ async function runNpmAudit() {
19
27
  utils_1.logger.warn('[runNpmAudit]: ⚠️ No package.json found in project root — skipping npm audit');
20
28
  return;
21
29
  }
30
+ const config = (0, config_1.loadProjectConfig)();
31
+ const auditLevel = config.security?.['npm-audit']?.auditLevel ?? 'moderate';
32
+ let report;
22
33
  try {
23
- const { stdout, stderr } = await (0, execa_1.execa)('npm', ['audit', '--audit-level=moderate'], {
34
+ const { stdout } = await (0, execa_1.execa)('npm', ['audit', '--json'], {
24
35
  cwd: projectRoot,
36
+ reject: false,
25
37
  });
26
- if (stdout)
27
- utils_1.logger.info(stdout);
28
- if (stderr)
29
- utils_1.logger.warn(stderr);
30
- utils_1.logger.info('[runNpmAudit]: ✅ NPM audit finished successfully');
38
+ report = JSON.parse(stdout);
31
39
  }
32
40
  catch (error) {
33
- const err = error;
34
- utils_1.logger.error('[runNpmAudit]: ❌ NPM audit found issues');
35
- if (err.stdout)
36
- utils_1.logger.error(err.stdout);
37
- if (err.stderr)
38
- utils_1.logger.warn(err.stderr);
39
- if (!err.stdout && !err.stderr)
40
- utils_1.logger.error(err.message);
41
- throw err;
41
+ utils_1.logger.error('[runNpmAudit]: ❌ Failed to parse npm audit output');
42
+ throw error;
43
+ }
44
+ const directVulns = Object.values(report.vulnerabilities ?? {}).filter(v => v.isDirect && isSeverityAtLeast(v.severity, auditLevel));
45
+ if (directVulns.length === 0) {
46
+ utils_1.logger.info('[runNpmAudit]: ✅ NPM audit finished successfully');
47
+ return;
48
+ }
49
+ for (const v of directVulns) {
50
+ utils_1.logger.error(`[runNpmAudit]: ❌ ${v.name} — severity: ${v.severity}`);
42
51
  }
52
+ throw new Error(`[runNpmAudit]: ${directVulns.length} direct dependency vulnerability(ies) found at level "${auditLevel}" or above`);
43
53
  }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPrBodyCheck = runPrBodyCheck;
7
+ const utils_1 = require("../utils");
8
+ const result_collector_1 = require("../utils/result-collector");
9
+ const loader_1 = require("../config/loader");
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ // Matches "Closes #123", "Fixes #45", "Resolves #7" or JIRA-style "PROJ-456"
12
+ const TICKET_PATTERN = /(?:closes?|fixes?|resolves?)\s+#\d+|[A-Z][A-Z0-9]+-\d+/i;
13
+ async function runPrBodyCheck() {
14
+ const cfg = (0, loader_1.loadProjectConfig)();
15
+ if (cfg.prBody?.enabled === false) {
16
+ utils_1.logger.info('[runPrBodyCheck]: Disabled in pr-checkmate.json, skipping');
17
+ result_collector_1.resultCollector.register('PR Body', 'skip');
18
+ return;
19
+ }
20
+ const minLength = cfg.prBody?.minLength ?? 20;
21
+ const requireTicket = cfg.prBody?.requireTicket ?? true;
22
+ utils_1.logger.info('[runPrBodyCheck]: Checking PR description...');
23
+ const eventPath = process.env.GITHUB_EVENT_PATH;
24
+ if (!eventPath || !node_fs_1.default.existsSync(eventPath)) {
25
+ utils_1.logger.info('[runPrBodyCheck]: Not in GitHub CI, skipping');
26
+ result_collector_1.resultCollector.register('PR Body', 'skip');
27
+ return;
28
+ }
29
+ let event;
30
+ try {
31
+ event = JSON.parse(node_fs_1.default.readFileSync(eventPath, 'utf-8'));
32
+ }
33
+ catch (err) {
34
+ utils_1.logger.warn(`[runPrBodyCheck]: ⚠️ Could not read GitHub event\n${err}`);
35
+ result_collector_1.resultCollector.register('PR Body', 'skip');
36
+ return;
37
+ }
38
+ const pr = event.pull_request;
39
+ if (!pr) {
40
+ utils_1.logger.info('[runPrBodyCheck]: No PR data found, skipping');
41
+ result_collector_1.resultCollector.register('PR Body', 'skip');
42
+ return;
43
+ }
44
+ const body = pr.body ?? '';
45
+ const trimmed = body.trim();
46
+ const issues = [];
47
+ if (trimmed.length < minLength) {
48
+ issues.push(`description too short (${trimmed.length}/${minLength} chars min)`);
49
+ }
50
+ if (requireTicket && trimmed && !TICKET_PATTERN.test(trimmed)) {
51
+ issues.push('no ticket reference (e.g. "Closes #123" or "PROJ-456")');
52
+ }
53
+ if (issues.length > 0) {
54
+ utils_1.logger.warn('[runPrBodyCheck]: ⚠️ PR description issues:');
55
+ issues.forEach(issue => utils_1.logger.warn(` • ${issue}`));
56
+ result_collector_1.resultCollector.register('PR Body', 'warn', issues.join('; '));
57
+ }
58
+ else {
59
+ utils_1.logger.info('[runPrBodyCheck]: ✅ PR description looks good');
60
+ result_collector_1.resultCollector.register('PR Body', 'pass');
61
+ }
62
+ }