reactjsquality-check911 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.
- package/bin/reactjsquality-check911.js +6 -1
- package/commands/audit.js +64 -0
- package/commands/hooks.js +8 -4
- package/commands/init.js +5 -3
- package/commands/quality.js +67 -2
- package/config/security.eslintrc.json +16 -0
- package/config/sonarjs.eslintrc.json +16 -0
- package/package.json +5 -2
|
@@ -33,9 +33,14 @@ program
|
|
|
33
33
|
.description("Scan components, pages, and services — rebuild architecture.md")
|
|
34
34
|
.action(() => require("../commands/scan")());
|
|
35
35
|
|
|
36
|
+
program
|
|
37
|
+
.command("audit")
|
|
38
|
+
.description("Run npm audit for high/critical CVEs (only if vulnerabilities check is enabled)")
|
|
39
|
+
.action(() => require("../commands/audit")());
|
|
40
|
+
|
|
36
41
|
program
|
|
37
42
|
.command("hooks")
|
|
38
|
-
.description("Install pre-commit (ESLint + coverage) and pre-push (Playwright) git hooks")
|
|
43
|
+
.description("Install pre-commit (ESLint + coverage) and pre-push (Playwright + audit) git hooks")
|
|
39
44
|
.action(() => require("../commands/hooks")());
|
|
40
45
|
|
|
41
46
|
program.parse(process.argv);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
function loadConfig() {
|
|
7
|
+
try { return JSON.parse(fs.readFileSync(".reactjs-quality-agent.json", "utf8")); } catch (_) { return null; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = function audit() {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.log("No .reactjs-quality-agent.json found — skipping audit.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!config.checks || !config.checks.vulnerabilities) {
|
|
17
|
+
return; // not opted in — silent skip
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
process.stdout.write("Running npm audit... ");
|
|
21
|
+
try {
|
|
22
|
+
let auditOutput = "";
|
|
23
|
+
let hasVulns = false;
|
|
24
|
+
try {
|
|
25
|
+
execSync("npm audit --audit-level=high --json", {
|
|
26
|
+
encoding: "utf8", stdio: ["pipe", "pipe", "pipe"]
|
|
27
|
+
});
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// npm audit exits 1 when vulnerabilities found — stdout still has JSON
|
|
30
|
+
auditOutput = e.stdout || "{}";
|
|
31
|
+
hasVulns = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!hasVulns) {
|
|
35
|
+
console.log("✅");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let high = 0, critical = 0;
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(auditOutput);
|
|
42
|
+
if (data.metadata && data.metadata.vulnerabilities) {
|
|
43
|
+
// npm v7+
|
|
44
|
+
high = data.metadata.vulnerabilities.high || 0;
|
|
45
|
+
critical = data.metadata.vulnerabilities.critical || 0;
|
|
46
|
+
} else if (data.vulnerabilities) {
|
|
47
|
+
// npm v6
|
|
48
|
+
for (const v of Object.values(data.vulnerabilities)) {
|
|
49
|
+
if (v.severity === "critical") critical++;
|
|
50
|
+
else if (v.severity === "high") high++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
|
|
55
|
+
console.log("❌");
|
|
56
|
+
console.log(` ${critical} critical, ${high} high severity vulnerabilities in dependencies`);
|
|
57
|
+
console.log(" Fix: npm audit fix — auto-fix compatible updates");
|
|
58
|
+
console.log(" npm audit fix --force — force upgrades (may include breaking changes)");
|
|
59
|
+
console.log(" npm audit — see full vulnerability details");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.log("⚠️ npm audit unavailable:", e.message.slice(0, 80));
|
|
63
|
+
}
|
|
64
|
+
};
|
package/commands/hooks.js
CHANGED
|
@@ -27,7 +27,7 @@ reactjsquality-check911 coverage
|
|
|
27
27
|
|
|
28
28
|
const PRE_PUSH = `#!/bin/sh
|
|
29
29
|
# reactjsquality-check911 — pre-push hook
|
|
30
|
-
# Runs Playwright smoke tests before every push
|
|
30
|
+
# Runs Playwright smoke tests and npm audit before every push
|
|
31
31
|
|
|
32
32
|
set -e
|
|
33
33
|
|
|
@@ -43,6 +43,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
43
43
|
fi
|
|
44
44
|
|
|
45
45
|
reactjsquality-check911 playwright
|
|
46
|
+
reactjsquality-check911 audit
|
|
46
47
|
`;
|
|
47
48
|
|
|
48
49
|
module.exports = function hooks() {
|
|
@@ -66,9 +67,12 @@ module.exports = function hooks() {
|
|
|
66
67
|
console.log(`
|
|
67
68
|
Hooks installed. From now on:
|
|
68
69
|
|
|
69
|
-
git commit → ESLint errors
|
|
70
|
-
|
|
70
|
+
git commit → ESLint errors block the commit (changed chunks only)
|
|
71
|
+
Code Smells (SonarJS) block the commit [if enabled]
|
|
72
|
+
Security violations block the commit [if enabled]
|
|
73
|
+
Jest coverage < 80% blocks the commit [if enabled]
|
|
71
74
|
|
|
72
|
-
git push → Playwright smoke tests must
|
|
75
|
+
git push → Playwright smoke tests must pass [if enabled]
|
|
76
|
+
npm audit for high/critical CVEs [if enabled]
|
|
73
77
|
`);
|
|
74
78
|
};
|
package/commands/init.js
CHANGED
|
@@ -15,9 +15,11 @@ const FRAMEWORKS = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const CHECKS = [
|
|
18
|
-
{ id: "eslint",
|
|
19
|
-
{ id: "
|
|
20
|
-
{ id: "
|
|
18
|
+
{ id: "eslint", label: "ESLint — code style and error rules on staged chunks" },
|
|
19
|
+
{ id: "codesmells", label: "Code Smells — SonarJS: cognitive complexity, duplicate code, dead code" },
|
|
20
|
+
{ id: "vulnerabilities", label: "Vulnerabilities — eslint-security: eval/injection/unsafe regex + npm audit CVE scan on push" },
|
|
21
|
+
{ id: "coverage", label: "Coverage — Jest line coverage (80% threshold per changed file)" },
|
|
22
|
+
{ id: "playwright", label: "Playwright — E2E smoke tests run before every git push" }
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
const COPILOT_INSTRUCTIONS = {
|
package/commands/quality.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { execSync } = require("child_process");
|
|
4
|
-
const fs
|
|
5
|
-
const path
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
6
7
|
|
|
7
8
|
const CONFIG_FILE = ".reactjs-quality-agent.json";
|
|
8
9
|
|
|
@@ -96,6 +97,58 @@ function runEslint(staged, changedRanges) {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function runWithPlugin(staged, changedRanges, configFile, label) {
|
|
101
|
+
process.stdout.write(`Running ${label}...`.padEnd(30));
|
|
102
|
+
const eslintJs = path.join(PKG_ROOT, "node_modules", "eslint", "bin", "eslint.js");
|
|
103
|
+
const configPath = path.join(PKG_ROOT, "config", configFile);
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(eslintJs)) {
|
|
106
|
+
console.log("⚠️ (ESLint not in package — reinstall reactjsquality-check911)");
|
|
107
|
+
return { passed: true, summary: `⚠️ ${label}: ESLint not found` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const fileArgs = staged.map(f => `"${f}"`).join(" ");
|
|
112
|
+
let output = "";
|
|
113
|
+
try {
|
|
114
|
+
output = execSync(
|
|
115
|
+
`node "${eslintJs}" ${fileArgs} --format json --no-eslintrc --resolve-plugins-relative-to "${PKG_ROOT}" --config "${configPath}"`,
|
|
116
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
117
|
+
);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
output = e.stdout || "[]";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = JSON.parse(output || "[]");
|
|
123
|
+
const viols = [];
|
|
124
|
+
const errors = [];
|
|
125
|
+
|
|
126
|
+
for (const file of data) {
|
|
127
|
+
const ranges = findRanges(file.filePath, changedRanges);
|
|
128
|
+
if (!ranges) continue;
|
|
129
|
+
for (const msg of file.messages) {
|
|
130
|
+
if (!inChangedRange(msg.line, ranges)) continue;
|
|
131
|
+
const lbl = msg.severity === 2 ? "error" : "warn";
|
|
132
|
+
const entry = ` [${label}:${msg.ruleId || "?"}] ${path.basename(file.filePath)}:${msg.line} — ${msg.message} (${lbl})`;
|
|
133
|
+
viols.push(entry);
|
|
134
|
+
if (msg.severity === 2) errors.push(entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (errors.length) {
|
|
139
|
+
console.log("❌");
|
|
140
|
+
viols.forEach(v => console.log(v));
|
|
141
|
+
return { passed: false, summary: `❌ ${label}: ${errors.length} issue(s) in changed lines` };
|
|
142
|
+
}
|
|
143
|
+
console.log("✅");
|
|
144
|
+
if (viols.length) viols.forEach(v => console.log(v));
|
|
145
|
+
return { passed: true, summary: `✅ ${label} passed` };
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.log("⚠️");
|
|
148
|
+
return { passed: true, summary: `⚠️ ${label}: ${e.message.slice(0, 80)}` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
99
152
|
function runTypeScript(staged) {
|
|
100
153
|
const tsFiles = staged.filter(f => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
101
154
|
process.stdout.write("Running TypeScript... ");
|
|
@@ -141,6 +194,18 @@ module.exports = function quality() {
|
|
|
141
194
|
if (!r.passed) failed = true;
|
|
142
195
|
}
|
|
143
196
|
|
|
197
|
+
if (config.checks.codesmells) {
|
|
198
|
+
const r = runWithPlugin(staged, changedRanges, "sonarjs.eslintrc.json", "Code Smells");
|
|
199
|
+
results.push(r.summary);
|
|
200
|
+
if (!r.passed) failed = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (config.checks.vulnerabilities) {
|
|
204
|
+
const r = runWithPlugin(staged, changedRanges, "security.eslintrc.json", "Security");
|
|
205
|
+
results.push(r.summary);
|
|
206
|
+
if (!r.passed) failed = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
144
209
|
const r2 = runTypeScript(staged);
|
|
145
210
|
results.push(r2.summary);
|
|
146
211
|
if (!r2.passed) failed = true;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": { "browser": true, "es2022": true, "node": true },
|
|
3
|
+
"parserOptions": { "ecmaVersion": 2022, "sourceType": "module" },
|
|
4
|
+
"plugins": ["security"],
|
|
5
|
+
"rules": {
|
|
6
|
+
"security/detect-eval-with-expression": "error",
|
|
7
|
+
"security/detect-non-literal-regexp": "warn",
|
|
8
|
+
"security/detect-non-literal-require": "warn",
|
|
9
|
+
"security/detect-object-injection": "warn",
|
|
10
|
+
"security/detect-possible-timing-attacks": "warn",
|
|
11
|
+
"security/detect-pseudoRandomBytes": "error",
|
|
12
|
+
"security/detect-unsafe-regex": "error",
|
|
13
|
+
"security/detect-no-csrf-before-method-override": "error",
|
|
14
|
+
"security/detect-disable-mustache-escape": "error"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": { "browser": true, "es2022": true, "node": true },
|
|
3
|
+
"parserOptions": { "ecmaVersion": 2022, "sourceType": "module" },
|
|
4
|
+
"plugins": ["sonarjs"],
|
|
5
|
+
"rules": {
|
|
6
|
+
"sonarjs/cognitive-complexity": ["error", 15],
|
|
7
|
+
"sonarjs/no-duplicate-string": ["warn", { "threshold": 3 }],
|
|
8
|
+
"sonarjs/no-identical-functions": "error",
|
|
9
|
+
"sonarjs/no-empty-collection": "error",
|
|
10
|
+
"sonarjs/no-unused-collection": "error",
|
|
11
|
+
"sonarjs/no-redundant-boolean": "error",
|
|
12
|
+
"sonarjs/no-small-switch": "warn",
|
|
13
|
+
"sonarjs/prefer-immediate-return": "warn",
|
|
14
|
+
"sonarjs/no-nested-switch": "warn"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactjsquality-check911",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "GitHub Copilot agent for React/JS projects — enforces ESLint, Jest coverage, and Playwright tests on every commit and push",
|
|
5
5
|
"bin": {
|
|
6
6
|
"reactjsquality-check911": "./bin/reactjsquality-check911.js"
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
],
|
|
40
40
|
"type": "commonjs",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"commander": "^15.0.0"
|
|
42
|
+
"commander": "^15.0.0",
|
|
43
|
+
"eslint": "^8.57.0",
|
|
44
|
+
"eslint-plugin-sonarjs": "^0.25.0",
|
|
45
|
+
"eslint-plugin-security": "^3.0.0"
|
|
43
46
|
}
|
|
44
47
|
}
|