commit-cop 1.0.0 → 1.0.1

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 (52) hide show
  1. package/README.md +31 -15
  2. package/dist/checks/binaryFileCheck.d.ts +3 -0
  3. package/dist/checks/binaryFileCheck.d.ts.map +1 -0
  4. package/dist/checks/binaryFileCheck.js +53 -0
  5. package/dist/checks/binaryFileCheck.js.map +1 -0
  6. package/dist/checks/debuggerCheck.d.ts +3 -0
  7. package/dist/checks/debuggerCheck.d.ts.map +1 -0
  8. package/dist/checks/debuggerCheck.js +28 -0
  9. package/dist/checks/debuggerCheck.js.map +1 -0
  10. package/dist/checks/generatedFolderCheck.d.ts.map +1 -1
  11. package/dist/checks/generatedFolderCheck.js +7 -2
  12. package/dist/checks/generatedFolderCheck.js.map +1 -1
  13. package/dist/checks/junkFileCheck.d.ts +3 -0
  14. package/dist/checks/junkFileCheck.d.ts.map +1 -0
  15. package/dist/checks/junkFileCheck.js +30 -0
  16. package/dist/checks/junkFileCheck.js.map +1 -0
  17. package/dist/checks/lockfileDriftCheck.d.ts +3 -0
  18. package/dist/checks/lockfileDriftCheck.d.ts.map +1 -0
  19. package/dist/checks/lockfileDriftCheck.js +32 -0
  20. package/dist/checks/lockfileDriftCheck.js.map +1 -0
  21. package/dist/checks/mergeConflictCheck.d.ts +3 -0
  22. package/dist/checks/mergeConflictCheck.d.ts.map +1 -0
  23. package/dist/checks/mergeConflictCheck.js +33 -0
  24. package/dist/checks/mergeConflictCheck.js.map +1 -0
  25. package/dist/checks/secretCheck.d.ts.map +1 -1
  26. package/dist/checks/secretCheck.js +15 -4
  27. package/dist/checks/secretCheck.js.map +1 -1
  28. package/dist/checks/sensitiveFilenameCheck.d.ts +3 -0
  29. package/dist/checks/sensitiveFilenameCheck.d.ts.map +1 -0
  30. package/dist/checks/sensitiveFilenameCheck.js +41 -0
  31. package/dist/checks/sensitiveFilenameCheck.js.map +1 -0
  32. package/dist/checks/utils.d.ts +9 -0
  33. package/dist/checks/utils.d.ts.map +1 -0
  34. package/dist/checks/utils.js +48 -0
  35. package/dist/checks/utils.js.map +1 -0
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/scanner.d.ts.map +1 -1
  39. package/dist/scanner.js +14 -2
  40. package/dist/scanner.js.map +1 -1
  41. package/package.json +4 -4
  42. package/src/checks/binaryFileCheck.ts +64 -0
  43. package/src/checks/debuggerCheck.ts +32 -0
  44. package/src/checks/generatedFolderCheck.ts +11 -3
  45. package/src/checks/junkFileCheck.ts +39 -0
  46. package/src/checks/lockfileDriftCheck.ts +38 -0
  47. package/src/checks/mergeConflictCheck.ts +40 -0
  48. package/src/checks/secretCheck.ts +22 -6
  49. package/src/checks/sensitiveFilenameCheck.ts +50 -0
  50. package/src/checks/utils.ts +62 -0
  51. package/src/index.ts +1 -1
  52. package/src/scanner.ts +15 -3
package/README.md CHANGED
@@ -1,36 +1,52 @@
1
- # CommitClean
1
+ # Commit Cop
2
2
 
3
- CommitClean is a pre-commit safety checker that scans your **staged files** and warns you about common mistakes before they get pushed to GitHub.
3
+ Commit Cop is a pre-commit safety checker that scans your **staged files** and warns you about common mistakes before they get pushed to GitHub.
4
4
 
5
5
  Built for students, hackathons, and dev teams who want practical guardrails—not just formatting checks.
6
6
 
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install commit-cop
11
+ ```
12
+
13
+ Run it on staged changes:
14
+
15
+ ```bash
16
+ npx commit-cop
17
+ ```
18
+
19
+ Treat warnings as errors:
20
+
21
+ ```bash
22
+ npx commit-cop --strict
23
+ ```
24
+
7
25
  ## What it checks
8
26
 
9
27
  | Check | Severity | What it catches |
10
28
  | --- | --- | --- |
29
+ | Merge conflicts | Error | `<<<<<<<`, `=======`, `>>>>>>>` markers left in code |
11
30
  | Environment files | Error | `.env`, `.env.local`, and other `.env.*` files |
12
- | Generated folders | Error | `node_modules/`, `dist/`, `build/`, `.next/`, `coverage/` |
13
- | Secrets | Error | API keys, JWT secrets, database URLs, and similar patterns |
31
+ | Sensitive filenames | Error | Keys, certs, `credentials.json`, `.npmrc`, and similar |
32
+ | Generated folders | Error | `node_modules/`, `dist/`, `build/`, `.next/`, `coverage/` (including nested paths) |
33
+ | Secrets | Error | API keys, GitHub/AWS/Stripe tokens, JWT secrets, database URLs |
14
34
  | Focused tests | Error | `test.only`, `it.only`, `describe.only` left in test files |
15
35
  | Debug logs | Warning | `console.log` in staged JS/TS code |
36
+ | Debugger statements | Warning | `debugger` in staged JS/TS code |
16
37
  | Localhost URLs | Warning | Hardcoded `localhost` or `127.0.0.1` URLs |
38
+ | Junk files | Warning | `.DS_Store`, `Thumbs.db`, swap/backup files |
39
+ | Lockfile drift | Warning | `package.json` staged without `package-lock.json` (or vice versa) |
17
40
  | Large files | Warning | Staged files over 5 MB |
41
+ | Binary files | Warning | `.zip`, `.exe`, `.mp4`, and other non-text files |
18
42
 
19
43
  Errors block the commit. Warnings are reported but do not block unless you use `--strict`.
20
44
 
21
- ## Usage
22
-
23
- Run inside a Git repository with staged changes:
45
+ ## Local development
24
46
 
25
47
  ```bash
26
48
  npm install
27
- npm run commitclean
28
- ```
29
-
30
- Treat warnings as errors:
31
-
32
- ```bash
33
- npm run commitclean -- --strict
49
+ npm run commit-cop
34
50
  ```
35
51
 
36
52
  Build and run the compiled CLI:
@@ -59,4 +75,4 @@ Each check implements the same interface: receive staged files, return findings
59
75
  1. Read staged file paths with `git diff --cached --name-only`
60
76
  2. Run every check in `src/checks/`
61
77
  3. Print a summary with file locations and fix suggestions
62
- 4. Exit with code `1` if errors are found (or warnings in strict mode)
78
+ 4. Exit with code `1` if errors are found (or warnings in strict mode)
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const binaryFileCheck: Check;
3
+ //# sourceMappingURL=binaryFileCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binaryFileCheck.d.ts","sourceRoot":"","sources":["../../src/checks/binaryFileCheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AA0BlD,eAAO,MAAM,eAAe,EAAE,KAoC7B,CAAC"}
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import { getBaseName } from "./utils.js";
3
+ const binaryExtensions = new Set([
4
+ ".zip",
5
+ ".exe",
6
+ ".dll",
7
+ ".mp4",
8
+ ".mov",
9
+ ".sqlite",
10
+ ".db",
11
+ ]);
12
+ function hasNullBytes(file) {
13
+ const buffer = fs.readFileSync(file);
14
+ const sampleSize = Math.min(buffer.length, 8192);
15
+ for (let index = 0; index < sampleSize; index += 1) {
16
+ if (buffer[index] === 0) {
17
+ return true;
18
+ }
19
+ }
20
+ return false;
21
+ }
22
+ export const binaryFileCheck = {
23
+ name: "binary-file-check",
24
+ async run(context) {
25
+ const findings = [];
26
+ for (const file of context.stagedFiles) {
27
+ if (!fs.existsSync(file))
28
+ continue;
29
+ const baseName = getBaseName(file);
30
+ const extension = baseName.includes(".")
31
+ ? baseName.slice(baseName.lastIndexOf(".")).toLowerCase()
32
+ : "";
33
+ let isBinary = binaryExtensions.has(extension);
34
+ try {
35
+ isBinary ||= hasNullBytes(file);
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ if (isBinary) {
41
+ findings.push({
42
+ severity: "warning",
43
+ checkName: this.name,
44
+ file,
45
+ message: "Binary or non-text file is staged.",
46
+ suggestion: "Remove it from the commit or store it outside Git (e.g. Git LFS).",
47
+ });
48
+ }
49
+ }
50
+ return findings;
51
+ },
52
+ };
53
+ //# sourceMappingURL=binaryFileCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binaryFileCheck.js","sourceRoot":"","sources":["../../src/checks/binaryFileCheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,SAAS;IACT,KAAK;CACN,CAAC,CAAC;AAEH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEjD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACnD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAU;IACpC,IAAI,EAAE,mBAAmB;IAEzB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEnC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;gBACtC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE;gBACzD,CAAC,CAAC,EAAE,CAAC;YAEP,IAAI,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAE/C,IAAI,CAAC;gBACH,QAAQ,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,oCAAoC;oBAC7C,UAAU,EACR,mEAAmE;iBACtE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const debuggerCheck: Check;
3
+ //# sourceMappingURL=debuggerCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debuggerCheck.d.ts","sourceRoot":"","sources":["../../src/checks/debuggerCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAGlD,eAAO,MAAM,aAAa,EAAE,KA4B3B,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { isCodeFile, readFileLines } from "./utils.js";
2
+ export const debuggerCheck = {
3
+ name: "debugger-check",
4
+ async run(context) {
5
+ const findings = [];
6
+ for (const file of context.stagedFiles) {
7
+ if (!isCodeFile(file))
8
+ continue;
9
+ const lines = readFileLines(file);
10
+ if (!lines)
11
+ continue;
12
+ lines.forEach((line, index) => {
13
+ if (/\bdebugger\b/.test(line)) {
14
+ findings.push({
15
+ severity: "warning",
16
+ checkName: this.name,
17
+ file,
18
+ line: index + 1,
19
+ message: "debugger statement found in staged code.",
20
+ suggestion: "Remove debugger statements before committing.",
21
+ });
22
+ }
23
+ });
24
+ }
25
+ return findings;
26
+ },
27
+ };
28
+ //# sourceMappingURL=debuggerCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debuggerCheck.js","sourceRoot":"","sources":["../../src/checks/debuggerCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,CAAC,MAAM,aAAa,GAAU;IAClC,IAAI,EAAE,gBAAgB;IAEtB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEhC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9B,QAAQ,CAAC,IAAI,CAAC;wBACZ,QAAQ,EAAE,SAAS;wBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;wBACpB,IAAI;wBACJ,IAAI,EAAE,KAAK,GAAG,CAAC;wBACf,OAAO,EAAE,0CAA0C;wBACnD,UAAU,EAAE,+CAA+C;qBAC5D,CAAC,CAAC;gBACL,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"generatedFolderCheck.d.ts","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAUlD,eAAO,MAAM,oBAAoB,EAAE,KA0BlC,CAAC"}
1
+ {"version":3,"file":"generatedFolderCheck.d.ts","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAkBlD,eAAO,MAAM,oBAAoB,EAAE,KA0BlC,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { normalizePath } from "./utils.js";
1
2
  const blockedFolders = [
2
3
  "node_modules/",
3
4
  "dist/",
@@ -5,13 +6,17 @@ const blockedFolders = [
5
6
  ".next/",
6
7
  "coverage/",
7
8
  ];
9
+ function matchesBlockedFolder(normalizedPath, folder) {
10
+ return (normalizedPath.startsWith(folder) ||
11
+ normalizedPath.includes(`/${folder}`));
12
+ }
8
13
  export const generatedFolderCheck = {
9
14
  name: "generated-folder-check",
10
15
  async run(context) {
11
16
  const findings = [];
12
17
  for (const file of context.stagedFiles) {
13
- const normalized = file.replaceAll("\\", "/");
14
- const matchedFolder = blockedFolders.find((folder) => normalized.startsWith(folder));
18
+ const normalized = normalizePath(file);
19
+ const matchedFolder = blockedFolders.find((folder) => matchesBlockedFolder(normalized, folder));
15
20
  if (matchedFolder) {
16
21
  findings.push({
17
22
  severity: "error",
@@ -1 +1 @@
1
- {"version":3,"file":"generatedFolderCheck.js","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAAG;IACrB,eAAe;IACf,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,WAAW;CACZ,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAU;IACzC,IAAI,EAAE,wBAAwB;IAE9B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAE9C,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACnD,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAC9B,CAAC;YAEF,IAAI,aAAa,EAAE,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,GAAG,aAAa,kEAAkE;oBAC3F,UAAU,EAAE,OAAO,aAAa,gDAAgD,IAAI,EAAE;iBACvF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"generatedFolderCheck.js","sourceRoot":"","sources":["../../src/checks/generatedFolderCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,cAAc,GAAG;IACrB,eAAe;IACf,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,WAAW;CACZ,CAAC;AAEF,SAAS,oBAAoB,CAAC,cAAsB,EAAE,MAAc;IAClE,OAAO,CACL,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC;QACjC,cAAc,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,CACtC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAU;IACzC,IAAI,EAAE,wBAAwB;IAE9B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAEvC,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CACnD,oBAAoB,CAAC,UAAU,EAAE,MAAM,CAAC,CACzC,CAAC;YAEF,IAAI,aAAa,EAAE,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,GAAG,aAAa,kEAAkE;oBAC3F,UAAU,EAAE,OAAO,aAAa,gDAAgD,IAAI,EAAE;iBACvF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const junkFileCheck: Check;
3
+ //# sourceMappingURL=junkFileCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junkFileCheck.d.ts","sourceRoot":"","sources":["../../src/checks/junkFileCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAWlD,eAAO,MAAM,aAAa,EAAE,KA2B3B,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { getBaseName, matchesAnyPattern } from "./utils.js";
2
+ const junkExactNames = new Set([
3
+ ".ds_store",
4
+ "thumbs.db",
5
+ "desktop.ini",
6
+ ]);
7
+ const junkNamePatterns = [/\.swp$/i, /\.bak$/i, /\.tmp$/i, /~$/];
8
+ export const junkFileCheck = {
9
+ name: "junk-file-check",
10
+ async run(context) {
11
+ const findings = [];
12
+ for (const file of context.stagedFiles) {
13
+ const baseName = getBaseName(file);
14
+ const normalizedBaseName = baseName.toLowerCase();
15
+ const isJunk = junkExactNames.has(normalizedBaseName) ||
16
+ matchesAnyPattern(baseName, junkNamePatterns);
17
+ if (isJunk) {
18
+ findings.push({
19
+ severity: "warning",
20
+ checkName: this.name,
21
+ file,
22
+ message: "OS or editor junk file is staged.",
23
+ suggestion: `Run: git restore --staged ${file}`,
24
+ });
25
+ }
26
+ }
27
+ return findings;
28
+ },
29
+ };
30
+ //# sourceMappingURL=junkFileCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"junkFileCheck.js","sourceRoot":"","sources":["../../src/checks/junkFileCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,WAAW;IACX,WAAW;IACX,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AAEjE,MAAM,CAAC,MAAM,aAAa,GAAU;IAClC,IAAI,EAAE,iBAAiB;IAEvB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;YAElD,MAAM,MAAM,GACV,cAAc,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBACtC,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;YAEhD,IAAI,MAAM,EAAE,CAAC;gBACX,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,SAAS;oBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,mCAAmC;oBAC5C,UAAU,EAAE,6BAA6B,IAAI,EAAE;iBAChD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const lockfileDriftCheck: Check;
3
+ //# sourceMappingURL=lockfileDriftCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockfileDriftCheck.d.ts","sourceRoot":"","sources":["../../src/checks/lockfileDriftCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAOlD,eAAO,MAAM,kBAAkB,EAAE,KA8BhC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { normalizePath } from "./utils.js";
2
+ function isStaged(stagedFiles, target) {
3
+ return stagedFiles.some((file) => normalizePath(file) === target);
4
+ }
5
+ export const lockfileDriftCheck = {
6
+ name: "lockfile-drift-check",
7
+ async run(context) {
8
+ const findings = [];
9
+ const packageJsonStaged = isStaged(context.stagedFiles, "package.json");
10
+ const lockfileStaged = isStaged(context.stagedFiles, "package-lock.json");
11
+ if (packageJsonStaged && !lockfileStaged) {
12
+ findings.push({
13
+ severity: "warning",
14
+ checkName: this.name,
15
+ file: "package.json",
16
+ message: "package.json is staged but package-lock.json is not.",
17
+ suggestion: "Run npm install and stage package-lock.json.",
18
+ });
19
+ }
20
+ if (lockfileStaged && !packageJsonStaged) {
21
+ findings.push({
22
+ severity: "warning",
23
+ checkName: this.name,
24
+ file: "package-lock.json",
25
+ message: "package-lock.json is staged but package.json is not.",
26
+ suggestion: "Stage package.json or unstage the lockfile.",
27
+ });
28
+ }
29
+ return findings;
30
+ },
31
+ };
32
+ //# sourceMappingURL=lockfileDriftCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockfileDriftCheck.js","sourceRoot":"","sources":["../../src/checks/lockfileDriftCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,SAAS,QAAQ,CAAC,WAAqB,EAAE,MAAc;IACrD,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAU;IACvC,IAAI,EAAE,sBAAsB;IAE5B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,iBAAiB,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;QAE1E,IAAI,iBAAiB,IAAI,CAAC,cAAc,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,sDAAsD;gBAC/D,UAAU,EAAE,8CAA8C;aAC3D,CAAC,CAAC;QACL,CAAC;QAED,IAAI,cAAc,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,SAAS;gBACnB,SAAS,EAAE,IAAI,CAAC,IAAI;gBACpB,IAAI,EAAE,mBAAmB;gBACzB,OAAO,EAAE,sDAAsD;gBAC/D,UAAU,EAAE,6CAA6C;aAC1D,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const mergeConflictCheck: Check;
3
+ //# sourceMappingURL=mergeConflictCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeConflictCheck.d.ts","sourceRoot":"","sources":["../../src/checks/mergeConflictCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAKlD,eAAO,MAAM,kBAAkB,EAAE,KAkChC,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { readFileLines, shouldSkipContentScan } from "./utils.js";
2
+ const conflictMarkers = ["<<<<<<<", "=======", ">>>>>>>"];
3
+ export const mergeConflictCheck = {
4
+ name: "merge-conflict-check",
5
+ async run(context) {
6
+ const findings = [];
7
+ for (const file of context.stagedFiles) {
8
+ if (shouldSkipContentScan(file))
9
+ continue;
10
+ const lines = readFileLines(file);
11
+ if (!lines)
12
+ continue;
13
+ lines.forEach((line, index) => {
14
+ const trimmed = line.trim();
15
+ for (const marker of conflictMarkers) {
16
+ if (trimmed.startsWith(marker)) {
17
+ findings.push({
18
+ severity: "error",
19
+ checkName: this.name,
20
+ file,
21
+ line: index + 1,
22
+ message: "Merge conflict marker found in staged file.",
23
+ suggestion: "Resolve the conflict, remove the markers, and restage the file.",
24
+ });
25
+ return;
26
+ }
27
+ }
28
+ });
29
+ }
30
+ return findings;
31
+ },
32
+ };
33
+ //# sourceMappingURL=mergeConflictCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeConflictCheck.js","sourceRoot":"","sources":["../../src/checks/mergeConflictCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,kBAAkB,GAAU;IACvC,IAAI,EAAE,sBAAsB;IAE5B,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,qBAAqB,CAAC,IAAI,CAAC;gBAAE,SAAS;YAE1C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBAE5B,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;oBACrC,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;wBAC/B,QAAQ,CAAC,IAAI,CAAC;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,IAAI,CAAC,IAAI;4BACpB,IAAI;4BACJ,IAAI,EAAE,KAAK,GAAG,CAAC;4BACf,OAAO,EAAE,6CAA6C;4BACtD,UAAU,EACR,iEAAiE;yBACpE,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"secretCheck.d.ts","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAYlD,eAAO,MAAM,WAAW,EAAE,KA8BzB,CAAC"}
1
+ {"version":3,"file":"secretCheck.d.ts","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAyBlD,eAAO,MAAM,WAAW,EAAE,KAkCzB,CAAC"}
@@ -1,4 +1,4 @@
1
- import fs from "node:fs";
1
+ import { isCommentLine, isPlaceholderValue, readFileLines, shouldSkipContentScan, } from "./utils.js";
2
2
  const secretPatterns = [
3
3
  /OPENAI_API_KEY\s*=/i,
4
4
  /DATABASE_URL\s*=/i,
@@ -7,17 +7,27 @@ const secretPatterns = [
7
7
  /PRIVATE_KEY\s*=/i,
8
8
  /SECRET_KEY\s*=/i,
9
9
  /sk-[A-Za-z0-9_-]{20,}/,
10
+ /ghp_[A-Za-z0-9]{36,}/,
11
+ /github_pat_[A-Za-z0-9_]+/,
12
+ /AKIA[0-9A-Z]{16}/,
13
+ /sk_live_[A-Za-z0-9]+/,
14
+ /AIza[0-9A-Za-z_-]{35}/,
15
+ /hooks\.slack\.com\/services\//,
16
+ /password\s*=\s*['"][^'"]{8,}['"]/i,
10
17
  ];
11
18
  export const secretCheck = {
12
19
  name: "secret-check",
13
20
  async run(context) {
14
21
  const findings = [];
15
22
  for (const file of context.stagedFiles) {
16
- if (!fs.existsSync(file))
23
+ if (shouldSkipContentScan(file))
24
+ continue;
25
+ const lines = readFileLines(file);
26
+ if (!lines)
17
27
  continue;
18
- const content = fs.readFileSync(file, "utf-8");
19
- const lines = content.split("\n");
20
28
  lines.forEach((line, index) => {
29
+ if (isCommentLine(line) || isPlaceholderValue(line))
30
+ return;
21
31
  for (const pattern of secretPatterns) {
22
32
  if (pattern.test(line)) {
23
33
  findings.push({
@@ -28,6 +38,7 @@ export const secretCheck = {
28
38
  message: "Possible secret found in staged file.",
29
39
  suggestion: "Remove the secret and rotate it if it was already pushed.",
30
40
  });
41
+ return;
31
42
  }
32
43
  }
33
44
  });
@@ -1 +1 @@
1
- {"version":3,"file":"secretCheck.js","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,MAAM,cAAc,GAAG;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;IAClB,iBAAiB;IACjB,uBAAuB;CACxB,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,IAAI,EAAE,cAAc;IAEpB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEnC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAElC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;oBACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACvB,QAAQ,CAAC,IAAI,CAAC;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,IAAI,CAAC,IAAI;4BACpB,IAAI;4BACJ,IAAI,EAAE,KAAK,GAAG,CAAC;4BACf,OAAO,EAAE,uCAAuC;4BAChD,UAAU,EAAE,2DAA2D;yBACxE,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"secretCheck.js","sourceRoot":"","sources":["../../src/checks/secretCheck.ts"],"names":[],"mappings":"AACA,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,MAAM,cAAc,GAAG;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;IAClB,iBAAiB;IACjB,uBAAuB;IACvB,sBAAsB;IACtB,0BAA0B;IAC1B,kBAAkB;IAClB,sBAAsB;IACtB,uBAAuB;IACvB,+BAA+B;IAC/B,mCAAmC;CACpC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,IAAI,EAAE,cAAc;IAEpB,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,qBAAqB,CAAC,IAAI,CAAC;gBAAE,SAAS;YAE1C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,SAAS;YAErB,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC5B,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC;oBAAE,OAAO;gBAE5D,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;oBACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACvB,QAAQ,CAAC,IAAI,CAAC;4BACZ,QAAQ,EAAE,OAAO;4BACjB,SAAS,EAAE,IAAI,CAAC,IAAI;4BACpB,IAAI;4BACJ,IAAI,EAAE,KAAK,GAAG,CAAC;4BACf,OAAO,EAAE,uCAAuC;4BAChD,UAAU,EACR,2DAA2D;yBAC9D,CAAC,CAAC;wBACH,OAAO;oBACT,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Check } from "../types.js";
2
+ export declare const sensitiveFilenameCheck: Check;
3
+ //# sourceMappingURL=sensitiveFilenameCheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitiveFilenameCheck.d.ts","sourceRoot":"","sources":["../../src/checks/sensitiveFilenameCheck.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAW,MAAM,aAAa,CAAC;AAmBlD,eAAO,MAAM,sBAAsB,EAAE,KA8BpC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { getBaseName, matchesAnyPattern, normalizePath } from "./utils.js";
2
+ const sensitiveExactNames = new Set([
3
+ "id_rsa",
4
+ "id_ed25519",
5
+ "credentials.json",
6
+ "serviceaccountkey.json",
7
+ ".npmrc",
8
+ ".pypirc",
9
+ ]);
10
+ const sensitiveNamePatterns = [
11
+ /\.pem$/i,
12
+ /\.p12$/i,
13
+ /\.key$/i,
14
+ /^firebase-adminsdk.*\.json$/i,
15
+ ];
16
+ export const sensitiveFilenameCheck = {
17
+ name: "sensitive-filename-check",
18
+ async run(context) {
19
+ const findings = [];
20
+ for (const file of context.stagedFiles) {
21
+ const baseName = getBaseName(file);
22
+ const normalizedBaseName = baseName.toLowerCase();
23
+ const normalizedPath = normalizePath(file).toLowerCase();
24
+ const isSensitive = sensitiveExactNames.has(normalizedBaseName) ||
25
+ matchesAnyPattern(baseName, sensitiveNamePatterns) ||
26
+ normalizedPath.endsWith("/credentials.json") ||
27
+ normalizedPath.includes("serviceaccountkey.json");
28
+ if (isSensitive) {
29
+ findings.push({
30
+ severity: "error",
31
+ checkName: this.name,
32
+ file,
33
+ message: "Sensitive credential file is staged.",
34
+ suggestion: `Run: git restore --staged ${file}`,
35
+ });
36
+ }
37
+ }
38
+ return findings;
39
+ },
40
+ };
41
+ //# sourceMappingURL=sensitiveFilenameCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensitiveFilenameCheck.js","sourceRoot":"","sources":["../../src/checks/sensitiveFilenameCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,wBAAwB;IACxB,QAAQ;IACR,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG;IAC5B,SAAS;IACT,SAAS;IACT,SAAS;IACT,8BAA8B;CAC/B,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAU;IAC3C,IAAI,EAAE,0BAA0B;IAEhC,KAAK,CAAC,GAAG,CAAC,OAAO;QACf,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACvC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;YAClD,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YAEzD,MAAM,WAAW,GACf,mBAAmB,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBAC3C,iBAAiB,CAAC,QAAQ,EAAE,qBAAqB,CAAC;gBAClD,cAAc,CAAC,QAAQ,CAAC,mBAAmB,CAAC;gBAC5C,cAAc,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;YAEpD,IAAI,WAAW,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,OAAO;oBACjB,SAAS,EAAE,IAAI,CAAC,IAAI;oBACpB,IAAI;oBACJ,OAAO,EAAE,sCAAsC;oBAC/C,UAAU,EAAE,6BAA6B,IAAI,EAAE;iBAChD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare function normalizePath(file: string): string;
2
+ export declare function getBaseName(file: string): string;
3
+ export declare function isCodeFile(file: string): boolean;
4
+ export declare function shouldSkipContentScan(file: string): boolean;
5
+ export declare function readFileLines(file: string): string[] | null;
6
+ export declare function isCommentLine(line: string): boolean;
7
+ export declare function isPlaceholderValue(line: string): boolean;
8
+ export declare function matchesAnyPattern(value: string, patterns: RegExp[]): boolean;
9
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/checks/utils.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAQ3D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAET"}
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ const CODE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
3
+ const SKIP_CONTENT_EXTENSIONS = [".md", ".example", ".sample"];
4
+ const PLACEHOLDER_PATTERNS = [
5
+ /your-api-key/i,
6
+ /changeme/i,
7
+ /xxx+/i,
8
+ /TODO/i,
9
+ /placeholder/i,
10
+ /example/i,
11
+ /replace-me/i,
12
+ ];
13
+ export function normalizePath(file) {
14
+ return file.replaceAll("\\", "/");
15
+ }
16
+ export function getBaseName(file) {
17
+ return normalizePath(file).split("/").pop() ?? "";
18
+ }
19
+ export function isCodeFile(file) {
20
+ return CODE_EXTENSIONS.some((ext) => file.endsWith(ext));
21
+ }
22
+ export function shouldSkipContentScan(file) {
23
+ return SKIP_CONTENT_EXTENSIONS.some((ext) => file.endsWith(ext));
24
+ }
25
+ export function readFileLines(file) {
26
+ if (!fs.existsSync(file))
27
+ return null;
28
+ try {
29
+ return fs.readFileSync(file, "utf-8").split("\n");
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function isCommentLine(line) {
36
+ const trimmed = line.trim();
37
+ return (trimmed.startsWith("//") ||
38
+ trimmed.startsWith("#") ||
39
+ trimmed.startsWith("*") ||
40
+ trimmed.startsWith("/*"));
41
+ }
42
+ export function isPlaceholderValue(line) {
43
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(line));
44
+ }
45
+ export function matchesAnyPattern(value, patterns) {
46
+ return patterns.some((pattern) => pattern.test(value));
47
+ }
48
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/checks/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AAEvD,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAE/D,MAAM,oBAAoB,GAAG;IAC3B,eAAe;IACf,WAAW;IACX,OAAO;IACP,OAAO;IACP,cAAc;IACd,UAAU;IACV,aAAa;CACd,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,CACL,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QACxB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QACvB,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CACzB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAa,EACb,QAAkB;IAElB,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AACzD,CAAC"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { runChecks } from "./scanner.js";
5
5
  import { printReport } from "./reporter.js";
6
6
  const program = new Command();
7
7
  program
8
- .name("commitclean")
8
+ .name("commit-cop")
9
9
  .description("Pre-commit safety checker for staged files")
10
10
  .option("--strict", "Treat warnings as errors")
11
11
  .action(async (options) => {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,4CAA4C,CAAC;KACzD,MAAM,CAAC,UAAU,EAAE,0BAA0B,CAAC;KAC9C,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACtC,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;QAC/B,WAAW;QACX,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;KAChC,CAAC,CAAC;IAEH,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAC/B,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAC5C,CAAC;IAEF,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,4CAA4C,CAAC;KACzD,MAAM,CAAC,UAAU,EAAE,0BAA0B,CAAC;KAC9C,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACtC,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC;QAC/B,WAAW;QACX,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;KAChC,CAAC,CAAC;IAEH,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAC/B,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAC5C,CAAC;IAEF,IAAI,SAAS,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAmBxD,wBAAsB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CASzE"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA+BxD,wBAAsB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CASzE"}
package/dist/scanner.js CHANGED
@@ -1,18 +1,30 @@
1
1
  import { envFileCheck } from "./checks/envFileCheck.js";
2
2
  import { generatedFolderCheck } from "./checks/generatedFolderCheck.js";
3
- import { consoleLogCheck } from "./checks/consoleLogCheck.js";
3
+ import { mergeConflictCheck } from "./checks/mergeConflictCheck.js";
4
+ import { sensitiveFilenameCheck } from "./checks/sensitiveFilenameCheck.js";
5
+ import { secretCheck } from "./checks/secretCheck.js";
4
6
  import { focusedTestCheck } from "./checks/focusedTestCheck.js";
7
+ import { consoleLogCheck } from "./checks/consoleLogCheck.js";
8
+ import { debuggerCheck } from "./checks/debuggerCheck.js";
5
9
  import { localHostCheck } from "./checks/localHostCheck.js";
10
+ import { junkFileCheck } from "./checks/junkFileCheck.js";
11
+ import { lockfileDriftCheck } from "./checks/lockfileDriftCheck.js";
6
12
  import { largeFileCheck } from "./checks/largeFileCheck.js";
7
- import { secretCheck } from "./checks/secretCheck.js";
13
+ import { binaryFileCheck } from "./checks/binaryFileCheck.js";
8
14
  const checks = [
15
+ mergeConflictCheck,
9
16
  envFileCheck,
17
+ sensitiveFilenameCheck,
10
18
  generatedFolderCheck,
11
19
  secretCheck,
12
20
  focusedTestCheck,
13
21
  consoleLogCheck,
22
+ debuggerCheck,
14
23
  localHostCheck,
24
+ junkFileCheck,
25
+ lockfileDriftCheck,
15
26
  largeFileCheck,
27
+ binaryFileCheck,
16
28
  ];
17
29
  export async function runChecks(context) {
18
30
  const allFindings = [];
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,MAAM,MAAM,GAAG;IACb,YAAY;IACZ,oBAAoB;IACpB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,cAAc;CACf,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAqB;IACnD,MAAM,WAAW,GAAc,EAAE,CAAC;IAElC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,MAAM,MAAM,GAAG;IACb,kBAAkB;IAClB,YAAY;IACZ,sBAAsB;IACtB,oBAAoB;IACpB,WAAW;IACX,gBAAgB;IAChB,eAAe;IACf,aAAa;IACb,cAAc;IACd,aAAa;IACb,kBAAkB;IAClB,cAAc;IACd,eAAe;CAChB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAqB;IACnD,MAAM,WAAW,GAAc,EAAE,CAAC;IAElC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC1C,WAAW,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "commit-cop",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A pre-commit safety checker that scans staged files for risky commits.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "commitclean": "./dist/index.js"
7
+ "commit-cop": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "tsx src/index.ts",
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
- "commitclean": "tsx src/index.ts"
13
+ "commit-cop": "tsx src/index.ts"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
@@ -38,4 +38,4 @@
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3"
40
40
  }
41
- }
41
+ }
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs";
2
+ import type { Check, Finding } from "../types.js";
3
+ import { getBaseName } from "./utils.js";
4
+
5
+ const binaryExtensions = new Set([
6
+ ".zip",
7
+ ".exe",
8
+ ".dll",
9
+ ".mp4",
10
+ ".mov",
11
+ ".sqlite",
12
+ ".db",
13
+ ]);
14
+
15
+ function hasNullBytes(file: string): boolean {
16
+ const buffer = fs.readFileSync(file);
17
+ const sampleSize = Math.min(buffer.length, 8192);
18
+
19
+ for (let index = 0; index < sampleSize; index += 1) {
20
+ if (buffer[index] === 0) {
21
+ return true;
22
+ }
23
+ }
24
+
25
+ return false;
26
+ }
27
+
28
+ export const binaryFileCheck: Check = {
29
+ name: "binary-file-check",
30
+
31
+ async run(context) {
32
+ const findings: Finding[] = [];
33
+
34
+ for (const file of context.stagedFiles) {
35
+ if (!fs.existsSync(file)) continue;
36
+
37
+ const baseName = getBaseName(file);
38
+ const extension = baseName.includes(".")
39
+ ? baseName.slice(baseName.lastIndexOf(".")).toLowerCase()
40
+ : "";
41
+
42
+ let isBinary = binaryExtensions.has(extension);
43
+
44
+ try {
45
+ isBinary ||= hasNullBytes(file);
46
+ } catch {
47
+ continue;
48
+ }
49
+
50
+ if (isBinary) {
51
+ findings.push({
52
+ severity: "warning",
53
+ checkName: this.name,
54
+ file,
55
+ message: "Binary or non-text file is staged.",
56
+ suggestion:
57
+ "Remove it from the commit or store it outside Git (e.g. Git LFS).",
58
+ });
59
+ }
60
+ }
61
+
62
+ return findings;
63
+ },
64
+ };
@@ -0,0 +1,32 @@
1
+ import type { Check, Finding } from "../types.js";
2
+ import { isCodeFile, readFileLines } from "./utils.js";
3
+
4
+ export const debuggerCheck: Check = {
5
+ name: "debugger-check",
6
+
7
+ async run(context) {
8
+ const findings: Finding[] = [];
9
+
10
+ for (const file of context.stagedFiles) {
11
+ if (!isCodeFile(file)) continue;
12
+
13
+ const lines = readFileLines(file);
14
+ if (!lines) continue;
15
+
16
+ lines.forEach((line, index) => {
17
+ if (/\bdebugger\b/.test(line)) {
18
+ findings.push({
19
+ severity: "warning",
20
+ checkName: this.name,
21
+ file,
22
+ line: index + 1,
23
+ message: "debugger statement found in staged code.",
24
+ suggestion: "Remove debugger statements before committing.",
25
+ });
26
+ }
27
+ });
28
+ }
29
+
30
+ return findings;
31
+ },
32
+ };
@@ -1,4 +1,5 @@
1
1
  import type { Check, Finding } from "../types.js";
2
+ import { normalizePath } from "./utils.js";
2
3
 
3
4
  const blockedFolders = [
4
5
  "node_modules/",
@@ -8,6 +9,13 @@ const blockedFolders = [
8
9
  "coverage/",
9
10
  ];
10
11
 
12
+ function matchesBlockedFolder(normalizedPath: string, folder: string): boolean {
13
+ return (
14
+ normalizedPath.startsWith(folder) ||
15
+ normalizedPath.includes(`/${folder}`)
16
+ );
17
+ }
18
+
11
19
  export const generatedFolderCheck: Check = {
12
20
  name: "generated-folder-check",
13
21
 
@@ -15,10 +23,10 @@ export const generatedFolderCheck: Check = {
15
23
  const findings: Finding[] = [];
16
24
 
17
25
  for (const file of context.stagedFiles) {
18
- const normalized = file.replaceAll("\\", "/");
26
+ const normalized = normalizePath(file);
19
27
 
20
28
  const matchedFolder = blockedFolders.find((folder) =>
21
- normalized.startsWith(folder)
29
+ matchesBlockedFolder(normalized, folder)
22
30
  );
23
31
 
24
32
  if (matchedFolder) {
@@ -34,4 +42,4 @@ export const generatedFolderCheck: Check = {
34
42
 
35
43
  return findings;
36
44
  },
37
- };
45
+ };
@@ -0,0 +1,39 @@
1
+ import type { Check, Finding } from "../types.js";
2
+ import { getBaseName, matchesAnyPattern } from "./utils.js";
3
+
4
+ const junkExactNames = new Set([
5
+ ".ds_store",
6
+ "thumbs.db",
7
+ "desktop.ini",
8
+ ]);
9
+
10
+ const junkNamePatterns = [/\.swp$/i, /\.bak$/i, /\.tmp$/i, /~$/];
11
+
12
+ export const junkFileCheck: Check = {
13
+ name: "junk-file-check",
14
+
15
+ async run(context) {
16
+ const findings: Finding[] = [];
17
+
18
+ for (const file of context.stagedFiles) {
19
+ const baseName = getBaseName(file);
20
+ const normalizedBaseName = baseName.toLowerCase();
21
+
22
+ const isJunk =
23
+ junkExactNames.has(normalizedBaseName) ||
24
+ matchesAnyPattern(baseName, junkNamePatterns);
25
+
26
+ if (isJunk) {
27
+ findings.push({
28
+ severity: "warning",
29
+ checkName: this.name,
30
+ file,
31
+ message: "OS or editor junk file is staged.",
32
+ suggestion: `Run: git restore --staged ${file}`,
33
+ });
34
+ }
35
+ }
36
+
37
+ return findings;
38
+ },
39
+ };
@@ -0,0 +1,38 @@
1
+ import type { Check, Finding } from "../types.js";
2
+ import { normalizePath } from "./utils.js";
3
+
4
+ function isStaged(stagedFiles: string[], target: string): boolean {
5
+ return stagedFiles.some((file) => normalizePath(file) === target);
6
+ }
7
+
8
+ export const lockfileDriftCheck: Check = {
9
+ name: "lockfile-drift-check",
10
+
11
+ async run(context) {
12
+ const findings: Finding[] = [];
13
+ const packageJsonStaged = isStaged(context.stagedFiles, "package.json");
14
+ const lockfileStaged = isStaged(context.stagedFiles, "package-lock.json");
15
+
16
+ if (packageJsonStaged && !lockfileStaged) {
17
+ findings.push({
18
+ severity: "warning",
19
+ checkName: this.name,
20
+ file: "package.json",
21
+ message: "package.json is staged but package-lock.json is not.",
22
+ suggestion: "Run npm install and stage package-lock.json.",
23
+ });
24
+ }
25
+
26
+ if (lockfileStaged && !packageJsonStaged) {
27
+ findings.push({
28
+ severity: "warning",
29
+ checkName: this.name,
30
+ file: "package-lock.json",
31
+ message: "package-lock.json is staged but package.json is not.",
32
+ suggestion: "Stage package.json or unstage the lockfile.",
33
+ });
34
+ }
35
+
36
+ return findings;
37
+ },
38
+ };
@@ -0,0 +1,40 @@
1
+ import type { Check, Finding } from "../types.js";
2
+ import { readFileLines, shouldSkipContentScan } from "./utils.js";
3
+
4
+ const conflictMarkers = ["<<<<<<<", "=======", ">>>>>>>"];
5
+
6
+ export const mergeConflictCheck: Check = {
7
+ name: "merge-conflict-check",
8
+
9
+ async run(context) {
10
+ const findings: Finding[] = [];
11
+
12
+ for (const file of context.stagedFiles) {
13
+ if (shouldSkipContentScan(file)) continue;
14
+
15
+ const lines = readFileLines(file);
16
+ if (!lines) continue;
17
+
18
+ lines.forEach((line, index) => {
19
+ const trimmed = line.trim();
20
+
21
+ for (const marker of conflictMarkers) {
22
+ if (trimmed.startsWith(marker)) {
23
+ findings.push({
24
+ severity: "error",
25
+ checkName: this.name,
26
+ file,
27
+ line: index + 1,
28
+ message: "Merge conflict marker found in staged file.",
29
+ suggestion:
30
+ "Resolve the conflict, remove the markers, and restage the file.",
31
+ });
32
+ return;
33
+ }
34
+ }
35
+ });
36
+ }
37
+
38
+ return findings;
39
+ },
40
+ };
@@ -1,5 +1,10 @@
1
- import fs from "node:fs";
2
1
  import type { Check, Finding } from "../types.js";
2
+ import {
3
+ isCommentLine,
4
+ isPlaceholderValue,
5
+ readFileLines,
6
+ shouldSkipContentScan,
7
+ } from "./utils.js";
3
8
 
4
9
  const secretPatterns = [
5
10
  /OPENAI_API_KEY\s*=/i,
@@ -9,6 +14,13 @@ const secretPatterns = [
9
14
  /PRIVATE_KEY\s*=/i,
10
15
  /SECRET_KEY\s*=/i,
11
16
  /sk-[A-Za-z0-9_-]{20,}/,
17
+ /ghp_[A-Za-z0-9]{36,}/,
18
+ /github_pat_[A-Za-z0-9_]+/,
19
+ /AKIA[0-9A-Z]{16}/,
20
+ /sk_live_[A-Za-z0-9]+/,
21
+ /AIza[0-9A-Za-z_-]{35}/,
22
+ /hooks\.slack\.com\/services\//,
23
+ /password\s*=\s*['"][^'"]{8,}['"]/i,
12
24
  ];
13
25
 
14
26
  export const secretCheck: Check = {
@@ -18,12 +30,14 @@ export const secretCheck: Check = {
18
30
  const findings: Finding[] = [];
19
31
 
20
32
  for (const file of context.stagedFiles) {
21
- if (!fs.existsSync(file)) continue;
33
+ if (shouldSkipContentScan(file)) continue;
22
34
 
23
- const content = fs.readFileSync(file, "utf-8");
24
- const lines = content.split("\n");
35
+ const lines = readFileLines(file);
36
+ if (!lines) continue;
25
37
 
26
38
  lines.forEach((line, index) => {
39
+ if (isCommentLine(line) || isPlaceholderValue(line)) return;
40
+
27
41
  for (const pattern of secretPatterns) {
28
42
  if (pattern.test(line)) {
29
43
  findings.push({
@@ -32,8 +46,10 @@ export const secretCheck: Check = {
32
46
  file,
33
47
  line: index + 1,
34
48
  message: "Possible secret found in staged file.",
35
- suggestion: "Remove the secret and rotate it if it was already pushed.",
49
+ suggestion:
50
+ "Remove the secret and rotate it if it was already pushed.",
36
51
  });
52
+ return;
37
53
  }
38
54
  }
39
55
  });
@@ -41,4 +57,4 @@ export const secretCheck: Check = {
41
57
 
42
58
  return findings;
43
59
  },
44
- };
60
+ };
@@ -0,0 +1,50 @@
1
+ import type { Check, Finding } from "../types.js";
2
+ import { getBaseName, matchesAnyPattern, normalizePath } from "./utils.js";
3
+
4
+ const sensitiveExactNames = new Set([
5
+ "id_rsa",
6
+ "id_ed25519",
7
+ "credentials.json",
8
+ "serviceaccountkey.json",
9
+ ".npmrc",
10
+ ".pypirc",
11
+ ]);
12
+
13
+ const sensitiveNamePatterns = [
14
+ /\.pem$/i,
15
+ /\.p12$/i,
16
+ /\.key$/i,
17
+ /^firebase-adminsdk.*\.json$/i,
18
+ ];
19
+
20
+ export const sensitiveFilenameCheck: Check = {
21
+ name: "sensitive-filename-check",
22
+
23
+ async run(context) {
24
+ const findings: Finding[] = [];
25
+
26
+ for (const file of context.stagedFiles) {
27
+ const baseName = getBaseName(file);
28
+ const normalizedBaseName = baseName.toLowerCase();
29
+ const normalizedPath = normalizePath(file).toLowerCase();
30
+
31
+ const isSensitive =
32
+ sensitiveExactNames.has(normalizedBaseName) ||
33
+ matchesAnyPattern(baseName, sensitiveNamePatterns) ||
34
+ normalizedPath.endsWith("/credentials.json") ||
35
+ normalizedPath.includes("serviceaccountkey.json");
36
+
37
+ if (isSensitive) {
38
+ findings.push({
39
+ severity: "error",
40
+ checkName: this.name,
41
+ file,
42
+ message: "Sensitive credential file is staged.",
43
+ suggestion: `Run: git restore --staged ${file}`,
44
+ });
45
+ }
46
+ }
47
+
48
+ return findings;
49
+ },
50
+ };
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+
3
+ const CODE_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];
4
+
5
+ const SKIP_CONTENT_EXTENSIONS = [".md", ".example", ".sample"];
6
+
7
+ const PLACEHOLDER_PATTERNS = [
8
+ /your-api-key/i,
9
+ /changeme/i,
10
+ /xxx+/i,
11
+ /TODO/i,
12
+ /placeholder/i,
13
+ /example/i,
14
+ /replace-me/i,
15
+ ];
16
+
17
+ export function normalizePath(file: string): string {
18
+ return file.replaceAll("\\", "/");
19
+ }
20
+
21
+ export function getBaseName(file: string): string {
22
+ return normalizePath(file).split("/").pop() ?? "";
23
+ }
24
+
25
+ export function isCodeFile(file: string): boolean {
26
+ return CODE_EXTENSIONS.some((ext) => file.endsWith(ext));
27
+ }
28
+
29
+ export function shouldSkipContentScan(file: string): boolean {
30
+ return SKIP_CONTENT_EXTENSIONS.some((ext) => file.endsWith(ext));
31
+ }
32
+
33
+ export function readFileLines(file: string): string[] | null {
34
+ if (!fs.existsSync(file)) return null;
35
+
36
+ try {
37
+ return fs.readFileSync(file, "utf-8").split("\n");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ export function isCommentLine(line: string): boolean {
44
+ const trimmed = line.trim();
45
+ return (
46
+ trimmed.startsWith("//") ||
47
+ trimmed.startsWith("#") ||
48
+ trimmed.startsWith("*") ||
49
+ trimmed.startsWith("/*")
50
+ );
51
+ }
52
+
53
+ export function isPlaceholderValue(line: string): boolean {
54
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(line));
55
+ }
56
+
57
+ export function matchesAnyPattern(
58
+ value: string,
59
+ patterns: RegExp[]
60
+ ): boolean {
61
+ return patterns.some((pattern) => pattern.test(value));
62
+ }
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import { printReport } from "./reporter.js";
8
8
  const program = new Command();
9
9
 
10
10
  program
11
- .name("commitclean")
11
+ .name("commit-cop")
12
12
  .description("Pre-commit safety checker for staged files")
13
13
  .option("--strict", "Treat warnings as errors")
14
14
  .action(async (options) => {
package/src/scanner.ts CHANGED
@@ -1,20 +1,32 @@
1
1
  import type { CheckContext, Finding } from "./types.js";
2
2
  import { envFileCheck } from "./checks/envFileCheck.js";
3
3
  import { generatedFolderCheck } from "./checks/generatedFolderCheck.ts";
4
- import { consoleLogCheck } from "./checks/consoleLogCheck.ts";
4
+ import { mergeConflictCheck } from "./checks/mergeConflictCheck.ts";
5
+ import { sensitiveFilenameCheck } from "./checks/sensitiveFilenameCheck.ts";
6
+ import { secretCheck } from "./checks/secretCheck.ts";
5
7
  import { focusedTestCheck } from "./checks/focusedTestCheck.ts";
8
+ import { consoleLogCheck } from "./checks/consoleLogCheck.ts";
9
+ import { debuggerCheck } from "./checks/debuggerCheck.ts";
6
10
  import { localHostCheck } from "./checks/localHostCheck.ts";
11
+ import { junkFileCheck } from "./checks/junkFileCheck.ts";
12
+ import { lockfileDriftCheck } from "./checks/lockfileDriftCheck.ts";
7
13
  import { largeFileCheck } from "./checks/largeFileCheck.ts";
8
- import { secretCheck } from "./checks/secretCheck.ts";
14
+ import { binaryFileCheck } from "./checks/binaryFileCheck.ts";
9
15
 
10
16
  const checks = [
17
+ mergeConflictCheck,
11
18
  envFileCheck,
19
+ sensitiveFilenameCheck,
12
20
  generatedFolderCheck,
13
21
  secretCheck,
14
22
  focusedTestCheck,
15
23
  consoleLogCheck,
24
+ debuggerCheck,
16
25
  localHostCheck,
26
+ junkFileCheck,
27
+ lockfileDriftCheck,
17
28
  largeFileCheck,
29
+ binaryFileCheck,
18
30
  ];
19
31
 
20
32
  export async function runChecks(context: CheckContext): Promise<Finding[]> {
@@ -26,4 +38,4 @@ export async function runChecks(context: CheckContext): Promise<Finding[]> {
26
38
  }
27
39
 
28
40
  return allFindings;
29
- }
41
+ }