@hatem427/code-guard-ci 3.3.0 โ†’ 3.4.0

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.
Files changed (86) hide show
  1. package/config/angular.config.ts +29 -708
  2. package/config/guidelines.config.ts +5 -130
  3. package/config/nextjs.config.ts +27 -511
  4. package/config/react.config.ts +19 -614
  5. package/dist/config/angular.config.d.ts +5 -8
  6. package/dist/config/angular.config.d.ts.map +1 -1
  7. package/dist/config/angular.config.js +28 -666
  8. package/dist/config/angular.config.js.map +1 -1
  9. package/dist/config/guidelines.config.d.ts.map +1 -1
  10. package/dist/config/guidelines.config.js +5 -127
  11. package/dist/config/guidelines.config.js.map +1 -1
  12. package/dist/config/nextjs.config.d.ts +7 -9
  13. package/dist/config/nextjs.config.d.ts.map +1 -1
  14. package/dist/config/nextjs.config.js +26 -472
  15. package/dist/config/nextjs.config.js.map +1 -1
  16. package/dist/config/react.config.d.ts +4 -5
  17. package/dist/config/react.config.d.ts.map +1 -1
  18. package/dist/config/react.config.js +19 -586
  19. package/dist/config/react.config.js.map +1 -1
  20. package/dist/scripts/auto-fix.d.ts +0 -5
  21. package/dist/scripts/auto-fix.d.ts.map +1 -1
  22. package/dist/scripts/auto-fix.js +0 -5
  23. package/dist/scripts/auto-fix.js.map +1 -1
  24. package/dist/scripts/cli.js +211 -415
  25. package/dist/scripts/cli.js.map +1 -1
  26. package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
  27. package/dist/scripts/config-generators/ai-config-generator.js +71 -15
  28. package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
  29. package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
  30. package/dist/scripts/config-generators/eslint-generator.js +13 -625
  31. package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
  32. package/dist/scripts/config-generators/index.d.ts +0 -1
  33. package/dist/scripts/config-generators/index.d.ts.map +1 -1
  34. package/dist/scripts/config-generators/index.js +1 -5
  35. package/dist/scripts/config-generators/index.js.map +1 -1
  36. package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
  37. package/dist/scripts/config-generators/typescript-generator.js +0 -33
  38. package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
  39. package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
  40. package/dist/scripts/config-generators/vscode-generator.js +28 -171
  41. package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
  42. package/dist/scripts/generate-pr-checklist.d.ts +0 -5
  43. package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
  44. package/dist/scripts/generate-pr-checklist.js +1 -6
  45. package/dist/scripts/generate-pr-checklist.js.map +1 -1
  46. package/dist/scripts/postinstall.js +0 -38
  47. package/dist/scripts/postinstall.js.map +1 -1
  48. package/dist/scripts/precommit-check.d.ts +0 -5
  49. package/dist/scripts/precommit-check.d.ts.map +1 -1
  50. package/dist/scripts/precommit-check.js +92 -149
  51. package/dist/scripts/precommit-check.js.map +1 -1
  52. package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
  53. package/dist/scripts/utils/naming-validator.js +2 -96
  54. package/dist/scripts/utils/naming-validator.js.map +1 -1
  55. package/dist/scripts/utils/project-detector.d.ts +9 -12
  56. package/dist/scripts/utils/project-detector.d.ts.map +1 -1
  57. package/dist/scripts/utils/project-detector.js +11 -63
  58. package/dist/scripts/utils/project-detector.js.map +1 -1
  59. package/dist/scripts/utils/report-generator.js +5 -17
  60. package/dist/scripts/utils/report-generator.js.map +1 -1
  61. package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
  62. package/dist/scripts/utils/structure-validator.js +0 -50
  63. package/dist/scripts/utils/structure-validator.js.map +1 -1
  64. package/package.json +1 -12
  65. package/scripts/auto-fix.ts +0 -5
  66. package/scripts/cli.ts +226 -451
  67. package/scripts/config-generators/ai-config-generator.ts +78 -28
  68. package/scripts/config-generators/eslint-generator.ts +7 -621
  69. package/scripts/config-generators/index.ts +0 -1
  70. package/scripts/config-generators/typescript-generator.ts +0 -36
  71. package/scripts/config-generators/vscode-generator.ts +40 -178
  72. package/scripts/generate-pr-checklist.ts +1 -6
  73. package/scripts/postinstall.ts +0 -38
  74. package/scripts/precommit-check.ts +113 -278
  75. package/scripts/utils/naming-validator.ts +2 -104
  76. package/scripts/utils/project-detector.ts +11 -78
  77. package/scripts/utils/report-generator.ts +5 -19
  78. package/scripts/utils/structure-validator.ts +0 -54
  79. package/config/fastify.config.ts +0 -326
  80. package/config/hono.config.ts +0 -331
  81. package/config/nestjs.config.ts +0 -500
  82. package/config/python.config.ts +0 -512
  83. package/templates/feature-doc-api.md +0 -101
  84. package/templates/feature-doc-backend.md +0 -114
  85. package/templates/feature-doc-service.md +0 -113
  86. package/templates/feature-doc-ui.md +0 -91
@@ -36,11 +36,6 @@ import { verifyBypassPassword, recordBypass } from './utils/bypass-manager';
36
36
  import '../config/angular.config';
37
37
  import '../config/react.config';
38
38
  import '../config/nextjs.config';
39
- import '../config/nestjs.config';
40
- import '../config/node.config';
41
- import '../config/fastify.config';
42
- import '../config/hono.config';
43
- import '../config/python.config';
44
39
 
45
40
  // โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
46
41
 
@@ -50,6 +45,28 @@ const LINTABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss'];
50
45
  /** File extensions for code guardian rule checks */
51
46
  const CHECKABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss', 'sass', 'less'];
52
47
 
48
+ /**
49
+ * Returns true when lint-staged is configured in the project.
50
+ * When lint-staged is active it ALREADY ran ESLint --fix and Prettier --write
51
+ * and re-staged the modified files before this script is invoked.
52
+ * Running them again would produce a duplicate git-add and confuse developers.
53
+ */
54
+ function lintStagedIsActive(): boolean {
55
+ const fs = require('fs');
56
+ const lintStagedFiles = [
57
+ '.lintstagedrc', '.lintstagedrc.json', '.lintstagedrc.js',
58
+ '.lintstagedrc.cjs', '.lintstagedrc.yaml', '.lintstagedrc.yml',
59
+ 'lint-staged.config.js', 'lint-staged.config.cjs',
60
+ ];
61
+ if (lintStagedFiles.some((f: string) => fs.existsSync(f))) return true;
62
+ try {
63
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
64
+ return !!pkg['lint-staged'];
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
53
70
  /** Flag in commit message to request bypass */
54
71
  const BYPASS_COMMIT_FLAG = '#bypass-rules';
55
72
 
@@ -134,7 +151,12 @@ function runEslint(stagedFiles: string[]): boolean {
134
151
  return true;
135
152
  }
136
153
 
137
- logger.info(`Running ESLint on ${lintableFiles.length} file(s)...`);
154
+ const lintStagedActive = lintStagedIsActive();
155
+ if (lintStagedActive) {
156
+ logger.info(`ESLint check on ${lintableFiles.length} file(s) (lint-staged already auto-fixed)...`);
157
+ } else {
158
+ logger.info(`Running ESLint on ${lintableFiles.length} file(s)...`);
159
+ }
138
160
 
139
161
  try {
140
162
  // Security: Use array args to prevent command injection
@@ -193,6 +215,14 @@ function runEslint(stagedFiles: string[]): boolean {
193
215
  * If formatting issues are found, runs Prettier --write and re-stages the files.
194
216
  */
195
217
  function runPrettier(stagedFiles: string[]): boolean {
218
+ // When lint-staged is active it already ran `prettier --write` and
219
+ // re-staged the formatted files. Running Prettier again would produce a
220
+ // redundant `git add` and confuse developers.
221
+ if (lintStagedIsActive()) {
222
+ logger.success('Prettier: handled by lint-staged (skipping duplicate run).');
223
+ return true;
224
+ }
225
+
196
226
  const formattableFiles = stagedFiles.filter((f) => {
197
227
  const ext = path.extname(f).replace(/^\./, '');
198
228
  return LINTABLE_EXTENSIONS.includes(ext);
@@ -206,48 +236,42 @@ function runPrettier(stagedFiles: string[]): boolean {
206
236
  logger.info(`Running Prettier on ${formattableFiles.length} file(s)...`);
207
237
 
208
238
  try {
209
- // Security: Use array args to prevent command injection
210
239
  const { spawnSync } = require('child_process');
211
240
  const checkResult = spawnSync('npx', ['prettier', '--check', ...formattableFiles], {
212
241
  stdio: 'pipe',
213
242
  encoding: 'utf-8',
214
243
  });
215
-
244
+
216
245
  if (checkResult.status === 0) {
217
246
  logger.success('Prettier check passed.');
218
247
  return true;
219
248
  }
220
-
221
- logger.warn('Prettier found formatting issues โ€” auto-fixing...');
222
249
 
223
- try {
224
- const writeResult = spawnSync('npx', ['prettier', '--write', ...formattableFiles], {
225
- stdio: 'inherit',
226
- encoding: 'utf-8',
227
- });
250
+ logger.warn('Prettier found formatting issues โ€” auto-fixing...');
228
251
 
229
- if (writeResult.status !== 0) {
230
- logger.error('Prettier auto-fix failed.');
231
- return false;
232
- }
252
+ const writeResult = spawnSync('npx', ['prettier', '--write', ...formattableFiles], {
253
+ stdio: 'inherit',
254
+ encoding: 'utf-8',
255
+ });
233
256
 
234
- // Re-stage the auto-formatted files
235
- const gitResult = spawnSync('git', ['add', ...formattableFiles], {
236
- stdio: 'inherit',
237
- encoding: 'utf-8',
238
- });
257
+ if (writeResult.status !== 0) {
258
+ logger.error('Prettier auto-fix failed.');
259
+ return false;
260
+ }
239
261
 
240
- if (gitResult.status !== 0) {
241
- logger.error('Failed to re-stage auto-formatted files.');
242
- return false;
243
- }
262
+ // Re-stage only in standalone mode (lint-staged is NOT active)
263
+ const gitResult = spawnSync('git', ['add', ...formattableFiles], {
264
+ stdio: 'inherit',
265
+ encoding: 'utf-8',
266
+ });
244
267
 
245
- logger.success('Prettier auto-fixed and re-staged files.');
246
- return true;
247
- } catch (error) {
248
- logger.error('Prettier auto-fix failed.');
268
+ if (gitResult.status !== 0) {
269
+ logger.error('Failed to re-stage auto-formatted files.');
249
270
  return false;
250
271
  }
272
+
273
+ logger.success('Prettier auto-fixed and re-staged files.');
274
+ return true;
251
275
  } catch (error) {
252
276
  logger.error('Prettier check failed.');
253
277
  return false;
@@ -267,56 +291,28 @@ export interface SecurityScanResult {
267
291
 
268
292
  /**
269
293
  * Layer 1 โ€” npm audit
270
- * Checks all installed npm dependencies against the GitHub Advisory Database.
271
- * Blocks on critical/high severity by default.
272
294
  */
273
295
  function runNpmAudit(): SecurityScanResult {
274
- const result: SecurityScanResult = {
275
- tool: 'npm audit',
276
- passed: true,
277
- skipped: false,
278
- summary: '',
279
- criticalCount: 0,
280
- highCount: 0,
281
- };
282
-
283
- // Skip if not an npm project
296
+ const result: SecurityScanResult = { tool: 'npm audit', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
284
297
  const fs = require('fs');
285
298
  if (!fs.existsSync('package.json')) {
286
- result.skipped = true;
287
- result.summary = 'No package.json found โ€” skipping.';
288
- logger.dim(' npm audit: skipped (no package.json)');
289
- return result;
299
+ result.skipped = true; result.summary = 'No package.json found โ€” skipping.';
300
+ logger.dim(' npm audit: skipped (no package.json)'); return result;
290
301
  }
291
-
292
302
  logger.info('Layer 1/3 โ€” Running npm audit...');
293
-
294
303
  try {
295
304
  const { spawnSync } = require('child_process');
296
- const auditResult = spawnSync(
297
- 'npm',
298
- ['audit', '--json', '--audit-level=high'],
299
- { encoding: 'utf-8', cwd: process.cwd() }
300
- );
301
-
305
+ const auditResult = spawnSync('npm', ['audit', '--json', '--audit-level=high'], { encoding: 'utf-8', cwd: process.cwd() });
302
306
  let auditData: any = {};
303
- try {
304
- auditData = JSON.parse(auditResult.stdout || '{}');
305
- } catch {
306
- // npm audit output is not valid JSON (e.g. no lock file)
307
+ try { auditData = JSON.parse(auditResult.stdout || '{}'); } catch {
307
308
  if (auditResult.stderr?.includes('ENOLOCK') || auditResult.stdout?.includes('ENOLOCK')) {
308
- result.skipped = true;
309
- result.summary = 'No lock file found โ€” run `npm install` first.';
310
- logger.warn(' npm audit: skipped (no lock file โ€” run npm install first)');
311
- return result;
309
+ result.skipped = true; result.summary = 'No lock file found โ€” run `npm install` first.';
310
+ logger.warn(' npm audit: skipped (no lock file)'); return result;
312
311
  }
313
312
  }
314
-
315
313
  const vulns = auditData?.metadata?.vulnerabilities ?? {};
316
- result.criticalCount = vulns.critical ?? 0;
317
- result.highCount = vulns.high ?? 0;
314
+ result.criticalCount = vulns.critical ?? 0; result.highCount = vulns.high ?? 0;
318
315
  const total = Object.values(vulns).reduce((acc: number, v) => acc + (v as number), 0);
319
-
320
316
  if (result.criticalCount > 0 || result.highCount > 0) {
321
317
  result.passed = false;
322
318
  result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity vulnerabilities (${total} total).`;
@@ -329,116 +325,58 @@ function runNpmAudit(): SecurityScanResult {
329
325
  result.summary = 'No known vulnerabilities found.';
330
326
  logger.success(' npm audit: โœ… No vulnerabilities found.');
331
327
  }
332
- } catch {
333
- result.skipped = true;
334
- result.summary = 'npm not available โ€” skipping.';
335
- logger.dim(' npm audit: skipped (npm not available)');
336
- }
337
-
328
+ } catch { result.skipped = true; result.summary = 'npm not available.'; logger.dim(' npm audit: skipped'); }
338
329
  return result;
339
330
  }
340
331
 
341
332
  /**
342
- * Layer 2 โ€” RetireJS (npx retire)
343
- * Scans JavaScript files and node_modules for libraries with known CVEs.
344
- * Complements npm audit by catching bundled/vendored libraries that npm
345
- * audit cannot see.
333
+ * Layer 2 โ€” RetireJS
346
334
  */
347
335
  function runRetireJs(): SecurityScanResult {
348
- const result: SecurityScanResult = {
349
- tool: 'retire.js',
350
- passed: true,
351
- skipped: false,
352
- summary: '',
353
- criticalCount: 0,
354
- highCount: 0,
355
- };
356
-
336
+ const result: SecurityScanResult = { tool: 'retire.js', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
357
337
  logger.info('Layer 2/3 โ€” Running RetireJS...');
358
-
359
338
  try {
360
339
  const { spawnSync } = require('child_process');
361
- const retireResult = spawnSync(
362
- 'npx',
363
- ['--yes', 'retire', '--outputformat', 'json', '--exitwith', '0'],
364
- { encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 }
365
- );
366
-
340
+ const retireResult = spawnSync('npx', ['--yes', 'retire', '--outputformat', 'json', '--exitwith', '0'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 });
367
341
  let findings: any[] = [];
368
342
  try {
369
343
  const parsed = JSON.parse(retireResult.stdout || '[]');
370
344
  findings = Array.isArray(parsed) ? parsed : (parsed.data ?? []);
371
345
  } catch {
372
- // Non-JSON output means no findings or retire not installed
373
- if (retireResult.status === 0) {
374
- result.summary = 'No vulnerable libraries detected.';
375
- logger.success(' retire.js: โœ… No vulnerable libraries found.');
376
- return result;
377
- }
378
- }
379
-
380
- if (findings.length === 0) {
381
- result.summary = 'No vulnerable libraries detected.';
382
- logger.success(' retire.js: โœ… No vulnerable libraries found.');
383
- return result;
346
+ if (retireResult.status === 0) { result.summary = 'No vulnerable libraries detected.'; logger.success(' retire.js: โœ… No vulnerable libraries found.'); return result; }
384
347
  }
385
-
386
- // Count by severity
348
+ if (findings.length === 0) { result.summary = 'No vulnerable libraries detected.'; logger.success(' retire.js: โœ… No vulnerable libraries found.'); return result; }
387
349
  for (const file of findings) {
388
- for (const result_ of (file.results ?? [])) {
389
- for (const vuln of (result_.vulnerabilities ?? [])) {
350
+ for (const r of (file.results ?? [])) {
351
+ for (const vuln of (r.vulnerabilities ?? [])) {
390
352
  const sev = (vuln.severity ?? '').toLowerCase();
391
- if (sev === 'critical') result.criticalCount++;
392
- else if (sev === 'high') result.highCount++;
353
+ if (sev === 'critical') result.criticalCount++; else if (sev === 'high') result.highCount++;
393
354
  }
394
355
  }
395
356
  }
396
-
397
357
  if (result.criticalCount > 0 || result.highCount > 0) {
398
358
  result.passed = false;
399
359
  result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity vulnerable libraries (${findings.length} affected files).`;
400
360
  logger.error(` retire.js: โŒ ${result.summary}`);
401
- logger.dim(' Fix: Update or replace the flagged libraries.');
402
361
  } else {
403
362
  result.summary = `${findings.length} affected file(s) with low/moderate severity findings. Review when possible.`;
404
363
  logger.warn(` retire.js: โš ๏ธ ${result.summary}`);
405
364
  }
406
- } catch {
407
- result.skipped = true;
408
- result.summary = 'RetireJS could not be executed โ€” skipping.';
409
- logger.dim(' retire.js: skipped (could not execute npx retire)');
410
- }
411
-
365
+ } catch { result.skipped = true; result.summary = 'RetireJS could not be executed.'; logger.dim(' retire.js: skipped'); }
412
366
  return result;
413
367
  }
414
368
 
415
369
  /**
416
370
  * Layer 3 โ€” Syft + Grype
417
- * Syft generates a Software Bill of Materials (SBOM) for the project.
418
- * Grype scans the SBOM for CVEs across ALL package ecosystems (npm, pip,
419
- * Go, Maven, etc.), including OS-level packages if running inside a container.
420
- *
421
- * Falls back to running `grype dir:.` directly if Syft is not installed.
422
371
  */
423
372
  function runGrype(): SecurityScanResult {
424
- const result: SecurityScanResult = {
425
- tool: 'Syft + Grype',
426
- passed: true,
427
- skipped: false,
428
- summary: '',
429
- criticalCount: 0,
430
- highCount: 0,
431
- };
432
-
373
+ const result: SecurityScanResult = { tool: 'Syft + Grype', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
433
374
  logger.info('Layer 3/3 โ€” Running Syft + Grype...');
434
-
435
375
  const { spawnSync } = require('child_process');
436
-
437
- // Check if grype is available
438
376
  const grypeCheck = spawnSync('which', ['grype'], { encoding: 'utf-8' });
439
377
  if (grypeCheck.status !== 0) {
440
378
  result.skipped = true;
441
- result.summary = 'grype not installed โ€” install with: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin';
379
+ result.summary = 'grype not installed';
442
380
  logger.warn(' Syft+Grype: โš ๏ธ grype not installed โ€” Layer 3 skipped.');
443
381
  logger.dim('');
444
382
  logger.dim(' To enable full SBOM vulnerability scanning, install Grype once:');
@@ -450,100 +388,42 @@ function runGrype(): SecurityScanResult {
450
388
  logger.dim(' macOS (Homebrew):');
451
389
  logger.dim(' brew install anchore/grype/grype');
452
390
  logger.dim('');
453
- logger.dim(' Optionally install Syft for full SBOM generation:');
454
- logger.dim(' brew install anchore/syft/syft');
455
- logger.dim(' (or see https://github.com/anchore/syft)');
456
- logger.dim('');
457
391
  return result;
458
392
  }
459
-
460
393
  try {
461
394
  const fs = require('fs');
462
395
  const syftCheck = spawnSync('which', ['syft'], { encoding: 'utf-8' });
463
-
464
396
  if (syftCheck.status === 0) {
465
- // Use the most accurate Syft source depending on what's present:
466
- // 1. node_modules โ†’ captures every resolved transitive npm dep
467
- // 2. package-lock.json โ†’ full npm lockfile (catches same set as npm audit)
468
- // 3. dir:. โ†’ generic fallback (may miss transitive deps)
469
- let syftSource = 'dir:.';
470
- let syftMode = 'generic dir';
471
-
472
- if (fs.existsSync('node_modules')) {
473
- syftSource = 'dir:node_modules';
474
- syftMode = 'node_modules (full transitive tree)';
475
- } else if (fs.existsSync('package-lock.json')) {
476
- syftSource = 'file:package-lock.json';
477
- syftMode = 'package-lock.json';
478
- }
479
-
397
+ let syftSource = 'dir:.'; let syftMode = 'generic dir';
398
+ if (fs.existsSync('node_modules')) { syftSource = 'dir:node_modules'; syftMode = 'node_modules (full transitive tree)'; }
399
+ else if (fs.existsSync('package-lock.json')) { syftSource = 'file:package-lock.json'; syftMode = 'package-lock.json'; }
480
400
  logger.dim(` Generating SBOM with Syft (${syftMode})...`);
481
- const syftResult = spawnSync(
482
- 'syft',
483
- [syftSource, '-o', 'cyclonedx-json', '--quiet'],
484
- { encoding: 'utf-8', cwd: process.cwd(), timeout: 180_000 }
485
- );
486
-
401
+ const syftResult = spawnSync('syft', [syftSource, '-o', 'cyclonedx-json', '--quiet'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 180_000 });
487
402
  if (syftResult.status === 0 && (syftResult.stdout as string)?.trim()) {
488
- const grypeArgs = ['sbom:-', '--output', 'json', '--fail-on', 'high'];
489
- const grypeResult = spawnSync('grype', grypeArgs, {
490
- input: syftResult.stdout as string,
491
- encoding: 'utf-8',
492
- cwd: process.cwd(),
493
- timeout: 120_000,
494
- });
403
+ const grypeResult = spawnSync('grype', ['sbom:-', '--output', 'json', '--fail-on', 'high'], { input: syftResult.stdout as string, encoding: 'utf-8', cwd: process.cwd(), timeout: 120_000 });
495
404
  return _parseGrypeOutput(grypeResult, result, `Syft SBOM (${syftMode})`);
496
405
  }
497
406
  }
498
-
499
- // Fallback: run grype directly โ€” point at node_modules if it exists so
500
- // Grype resolves the full dependency tree rather than just package.json
501
- const fs2 = require('fs');
502
- const grypeTarget = fs2.existsSync('node_modules') ? 'dir:node_modules' : 'dir:.';
407
+ const grypeTarget = fs.existsSync('node_modules') ? 'dir:node_modules' : 'dir:.';
503
408
  logger.dim(` Running Grype directly on ${grypeTarget}...`);
504
- const grypeArgs = [grypeTarget, '--output', 'json', '--fail-on', 'high'];
505
- const grypeResult = spawnSync('grype', grypeArgs, {
506
- encoding: 'utf-8',
507
- cwd: process.cwd(),
508
- timeout: 120_000,
509
- });
409
+ const grypeResult = spawnSync('grype', [grypeTarget, '--output', 'json', '--fail-on', 'high'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 120_000 });
510
410
  return _parseGrypeOutput(grypeResult, result, `direct scan (${grypeTarget})`);
511
- } catch {
512
- result.skipped = true;
513
- result.summary = 'Grype scan failed โ€” check your installation.';
514
- logger.warn(' Syft+Grype: โš ๏ธ Scan failed โ€” check grype installation.');
515
- return result;
516
- }
411
+ } catch { result.skipped = true; result.summary = 'Grype scan failed.'; logger.warn(' Syft+Grype: โš ๏ธ Scan failed.'); return result; }
517
412
  }
518
413
 
519
- /** Parse grype JSON output and update the SecurityScanResult in-place. */
520
- function _parseGrypeOutput(
521
- spawnResult: ReturnType<typeof import('child_process').spawnSync>,
522
- result: SecurityScanResult,
523
- mode: string
524
- ): SecurityScanResult {
414
+ function _parseGrypeOutput(spawnResult: any, result: SecurityScanResult, mode: string): SecurityScanResult {
525
415
  let matches: any[] = [];
526
416
  try {
527
417
  const parsed = JSON.parse((spawnResult.stdout as string) || '{}');
528
418
  matches = parsed.matches ?? [];
529
419
  } catch {
530
- if (spawnResult.status === 0) {
531
- result.summary = 'No vulnerabilities found.';
532
- logger.success(` Syft+Grype: โœ… No vulnerabilities found (${mode}).`);
533
- return result;
534
- }
535
- result.skipped = true;
536
- result.summary = 'Could not parse Grype output.';
537
- logger.warn(' Syft+Grype: โš ๏ธ Could not parse output.');
538
- return result;
420
+ if (spawnResult.status === 0) { result.summary = 'No vulnerabilities found.'; logger.success(` Syft+Grype: โœ… No vulnerabilities found (${mode}).`); return result; }
421
+ result.skipped = true; result.summary = 'Could not parse Grype output.'; logger.warn(' Syft+Grype: โš ๏ธ Could not parse output.'); return result;
539
422
  }
540
-
541
423
  for (const match of matches) {
542
424
  const sev = (match.vulnerability?.severity ?? '').toLowerCase();
543
- if (sev === 'critical') result.criticalCount++;
544
- else if (sev === 'high') result.highCount++;
425
+ if (sev === 'critical') result.criticalCount++; else if (sev === 'high') result.highCount++;
545
426
  }
546
-
547
427
  if (result.criticalCount > 0 || result.highCount > 0) {
548
428
  result.passed = false;
549
429
  result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity CVEs (${matches.length} total matches).`;
@@ -556,104 +436,55 @@ function _parseGrypeOutput(
556
436
  result.summary = 'No vulnerabilities found.';
557
437
  logger.success(` Syft+Grype: โœ… No vulnerabilities found (${mode}).`);
558
438
  }
559
-
560
439
  return result;
561
440
  }
562
441
 
563
- /**
564
- * Run all three security scan layers and return the combined results.
565
- * When vulnerabilities are found the caller is responsible for prompting
566
- * the developer โ€” this function only scans and reports.
567
- */
568
442
  function runSecurityScans(): { passed: boolean; results: SecurityScanResult[] } {
569
443
  logger.header('๐Ÿ” Security Scanning (3 layers)');
570
-
571
444
  const npmResult = runNpmAudit();
572
445
  const retireResult = runRetireJs();
573
446
  const grypeResult = runGrype();
574
-
575
447
  const results = [npmResult, retireResult, grypeResult];
576
448
  const passed = results.every((r) => r.skipped || r.passed);
577
-
578
449
  console.log('');
579
450
  if (!passed) {
580
451
  const blocking = results.filter((r) => !r.skipped && !r.passed);
581
452
  logger.error(`Security scan found vulnerabilities โ€” ${blocking.length} tool(s) reported critical/high issues:`);
582
- for (const r of blocking) {
583
- logger.error(` โ€ข ${r.tool}: ${r.summary}`);
584
- }
585
- } else {
586
- logger.success('Security scans passed (all layers).');
587
- }
588
-
453
+ for (const r of blocking) { logger.error(` โ€ข ${r.tool}: ${r.summary}`); }
454
+ } else { logger.success('Security scans passed (all layers).'); }
589
455
  return { passed, results };
590
456
  }
591
457
 
592
- /**
593
- * When security scans find vulnerabilities, prompt the developer to decide
594
- * whether to proceed or abort the commit.
595
- * Returns true โ†’ developer chose to continue (risks acknowledged).
596
- * Returns false โ†’ developer chose to abort.
597
- */
598
458
  async function promptSecurityDecision(results: SecurityScanResult[]): Promise<boolean> {
599
- const pad = (line: string, width = 63) => {
600
- // strip ANSI codes for length calculation
601
- const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
602
- return line + ' '.repeat(Math.max(0, width - plain.length)) + 'โ”‚';
603
- };
604
-
605
459
  console.log('');
606
460
  console.log('โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”');
607
461
  console.log('โ”‚ โš ๏ธ VULNERABILITY REPORT โ€” all 3 layers โ”‚');
608
- console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
462
+ console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
609
463
  console.log('โ”‚ Tool โ”‚ Result โ”‚');
610
- console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
611
-
464
+ console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
612
465
  for (const r of results) {
613
466
  const toolCol = `โ”‚ ${r.tool}`.padEnd(18) + 'โ”‚';
614
- let status: string;
615
- if (r.skipped) {
616
- status = ' โฌ› skipped';
617
- } else if (!r.passed) {
618
- status = ` โŒ ${r.criticalCount} critical, ${r.highCount} high`;
619
- } else {
620
- status = ' โœ… clean';
621
- }
622
- const statusCol = status.padEnd(42) + 'โ”‚';
623
- console.log(toolCol + statusCol);
467
+ const status = r.skipped ? ' โฌ› skipped' : !r.passed ? ` โŒ ${r.criticalCount} critical, ${r.highCount} high` : ' โœ… clean';
468
+ console.log(toolCol + status.padEnd(42) + 'โ”‚');
624
469
  }
625
-
626
- console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
627
-
628
- // Detail lines for failing tools
470
+ console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
629
471
  const blocking = results.filter((r) => !r.skipped && !r.passed);
630
472
  for (const r of blocking) {
631
473
  const summary = r.summary.length > 57 ? r.summary.slice(0, 54) + '...' : r.summary;
632
- console.log(pad(`โ”‚ โ†ณ ${r.tool}: ${summary}`));
474
+ const line = `โ”‚ โคต ${r.tool}: ${summary}`;
475
+ console.log(line.padEnd(63) + 'โ”‚');
633
476
  }
634
-
635
477
  console.log('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค');
636
478
  console.log('โ”‚ Fix: npm audit fix | npm audit (for full details) โ”‚');
637
479
  console.log('โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜');
638
480
  console.log('');
639
-
640
- const answer = await promptUser(
641
- ' Proceed with commit despite vulnerabilities? [y/N]: '
642
- );
643
-
481
+ const answer = await promptUser(' Proceed with commit despite vulnerabilities? [y/N]: ');
644
482
  const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
645
-
646
- if (confirmed) {
647
- logger.warn('โš ๏ธ Proceeding with known vulnerabilities โ€” please fix them as soon as possible.');
648
- } else {
649
- logger.error('โŒ Commit aborted. Fix the vulnerabilities and try again.');
650
- }
651
-
483
+ if (confirmed) { logger.warn('โš ๏ธ Proceeding with known vulnerabilities โ€” please fix them soon.'); }
484
+ else { logger.error('โŒ Commit aborted. Fix the vulnerabilities and try again.'); }
652
485
  return confirmed;
653
486
  }
654
487
 
655
- // โ”€โ”€ Feature doc detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
656
-
657
488
  /**
658
489
  * If the commit looks like a new feature (branch name contains "feat/" or
659
490
  * "feature/"), remind the user to generate docs.
@@ -738,16 +569,22 @@ async function main(): Promise<void> {
738
569
  // Step 5: Run ESLint
739
570
  const eslintPassed = runEslint(stagedPaths);
740
571
 
741
- // Step 6: Run Prettier (auto-fix mode)
572
+ // Step 6: Run Prettier (skipped when lint-staged is active)
742
573
  const prettierPassed = runPrettier(stagedPaths);
743
574
 
744
- // Step 7: Run 3-layer security scans
745
- const { passed: securityPassed, results: securityResults } = runSecurityScans();
575
+ // Step 7: Security scans โ€” ask developer first
576
+ let securityConfirmed = true;
577
+ const runScans = await promptUser(' ๐Ÿ” Run security scans (npm audit + RetireJS + Grype)? [Y/n]: ');
578
+ const wantsScans = runScans === '' || runScans.toLowerCase() === 'y' || runScans.toLowerCase() === 'yes';
746
579
 
747
- // Step 7.5: If vulnerabilities found, ask the developer what to do
748
- let securityConfirmed = securityPassed;
749
- if (!securityPassed) {
750
- securityConfirmed = await promptSecurityDecision(securityResults);
580
+ if (wantsScans) {
581
+ const { passed: securityPassed, results: securityResults } = runSecurityScans();
582
+ securityConfirmed = securityPassed;
583
+ if (!securityPassed) {
584
+ securityConfirmed = await promptSecurityDecision(securityResults);
585
+ }
586
+ } else {
587
+ logger.dim(' Security scans skipped.');
751
588
  }
752
589
 
753
590
  // Step 8: Check for feature docs
@@ -773,8 +610,6 @@ async function main(): Promise<void> {
773
610
  if (report.errorCount > 0 || !eslintPassed || !securityConfirmed) {
774
611
  logger.error('โŒ Commit BLOCKED โ€” fix the errors above and try again.');
775
612
  logger.dim(' ๐Ÿ’ก To bypass: BYPASS_RULES=true git commit ... or add #bypass-rules to commit message');
776
-
777
- // Exit immediately with error code
778
613
  process.exitCode = 1;
779
614
  process.exit(1);
780
615
  }