reactjsquality-check911 1.0.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.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { program } = require("commander");
5
+
6
+ program
7
+ .name("reactjsquality-check911")
8
+ .description("React/JS quality agent — ESLint, Jest coverage, Playwright on every commit")
9
+ .version(require("../package.json").version);
10
+
11
+ program
12
+ .command("init")
13
+ .description("Choose framework and quality checks, configure Copilot and MCP")
14
+ .action(() => require("../commands/init")());
15
+
16
+ program
17
+ .command("quality")
18
+ .description("Run ESLint and TypeScript check on staged files (changed chunks only)")
19
+ .action(() => require("../commands/quality")());
20
+
21
+ program
22
+ .command("coverage")
23
+ .description("Run Jest coverage on staged/changed files (80% threshold per file)")
24
+ .action(() => require("../commands/coverage")());
25
+
26
+ program
27
+ .command("playwright")
28
+ .description("Run Playwright smoke tests against the current build")
29
+ .action(() => require("../commands/playwright")());
30
+
31
+ program
32
+ .command("scan")
33
+ .description("Scan components, pages, and services — rebuild architecture.md")
34
+ .action(() => require("../commands/scan")());
35
+
36
+ program
37
+ .command("hooks")
38
+ .description("Install pre-commit (ESLint + coverage) and pre-push (Playwright) git hooks")
39
+ .action(() => require("../commands/hooks")());
40
+
41
+ program.parse(process.argv);
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ const { execSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const THRESHOLD = 80;
8
+ const CONFIG_FILE = ".reactjs-quality-agent.json";
9
+
10
+ function loadConfig() {
11
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
12
+ }
13
+
14
+ function getStagedSourceFiles() {
15
+ try {
16
+ const out = execSync("git diff --cached --name-only", { encoding: "utf8" });
17
+ return out.split("\n").map(f => f.trim()).filter(f =>
18
+ f &&
19
+ /\.(js|jsx|ts|tsx|vue)$/.test(f) &&
20
+ !f.includes(".test.") && !f.includes(".spec.") &&
21
+ !f.includes("__tests__") &&
22
+ fs.existsSync(f)
23
+ );
24
+ } catch (_) { return []; }
25
+ }
26
+
27
+ function findTestFile(srcFile) {
28
+ const base = srcFile.replace(/\.(js|jsx|ts|tsx|vue)$/, "");
29
+ const ext = srcFile.match(/\.(js|jsx|ts|tsx|vue)$/)[1];
30
+ const testExt = ext === "js" ? "js" : ext === "jsx" ? "tsx" : "tsx";
31
+ return [
32
+ `${base}.test.${testExt}`,
33
+ `${base}.test.${ext}`,
34
+ `${base}.spec.${testExt}`,
35
+ `${base}.spec.${ext}`,
36
+ base.replace(/\/src\//, "/src/__tests__/") + `.test.${testExt}`,
37
+ base.replace(/\/src\//, "/src/__tests__/") + `.test.${ext}`,
38
+ ].find(f => fs.existsSync(f));
39
+ }
40
+
41
+ module.exports = function coverage() {
42
+ const config = loadConfig();
43
+ if (!config) {
44
+ console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
45
+ process.exit(1);
46
+ }
47
+ if (!config.checks.coverage) {
48
+ console.log("Coverage check is disabled — skipping.");
49
+ return;
50
+ }
51
+
52
+ const sources = getStagedSourceFiles();
53
+ if (!sources.length) {
54
+ console.log("No staged source files — skipping coverage.");
55
+ return;
56
+ }
57
+
58
+ console.log(`Verifying Jest coverage for: ${sources.map(f => path.basename(f)).join(", ")}\n`);
59
+ const results = [];
60
+ let failed = false;
61
+
62
+ for (const file of sources) {
63
+ const testFile = findTestFile(file);
64
+ if (!testFile) {
65
+ const expected = path.basename(file).replace(/\.(js|jsx|ts|tsx)$/, ".test.$1");
66
+ console.log(` ⚠️ ${path.basename(file)} — no test file found (expected ${expected})`);
67
+ results.push(`⚠️ ${path.basename(file)}: no test file`);
68
+ continue;
69
+ }
70
+
71
+ try {
72
+ execSync(
73
+ `npx jest "${testFile}" --coverage --coverageReporters=json-summary --passWithNoTests --silent`,
74
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
75
+ );
76
+
77
+ const summaryPath = "coverage/coverage-summary.json";
78
+ if (!fs.existsSync(summaryPath)) {
79
+ results.push(`⚠️ ${path.basename(file)}: coverage-summary.json not generated`);
80
+ continue;
81
+ }
82
+
83
+ const summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
84
+ const absFile = path.resolve(file).replace(/\\/g, "/");
85
+ const matched = Object.entries(summary).find(([k]) =>
86
+ k.replace(/\\/g, "/") === absFile ||
87
+ k.replace(/\\/g, "/").endsWith("/" + file.replace(/\\/g, "/"))
88
+ );
89
+
90
+ if (!matched) {
91
+ results.push(`⚠️ ${path.basename(file)}: not found in coverage report`);
92
+ continue;
93
+ }
94
+
95
+ const pct = matched[1].lines.pct;
96
+ const icon = pct >= THRESHOLD ? "✅" : "❌";
97
+ console.log(` ${icon} ${path.basename(file)}: ${pct}% line coverage`);
98
+ results.push(`${icon} ${path.basename(file)}: ${pct}%`);
99
+ if (pct < THRESHOLD) failed = true;
100
+
101
+ } catch (e) {
102
+ console.log(` ❌ ${path.basename(file)}: Jest failed`);
103
+ results.push(`❌ ${path.basename(file)}: Jest execution failed`);
104
+ failed = true;
105
+ }
106
+ }
107
+
108
+ console.log("\n--- Coverage Summary ---");
109
+ results.forEach(r => console.log(" ", r));
110
+
111
+ if (failed) {
112
+ console.log(`\nCoverage below ${THRESHOLD}% — add tests and try again.`);
113
+ process.exit(1);
114
+ }
115
+
116
+ console.log(`\n✅ All changed files meet the ${THRESHOLD}% coverage threshold.`);
117
+ };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const { execSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const PRE_COMMIT = `#!/bin/sh
8
+ # reactjsquality-check911 — pre-commit hook
9
+ # Runs ESLint and Jest coverage on staged files (changed chunks only)
10
+
11
+ set -e
12
+
13
+ if ! command -v reactjsquality-check911 >/dev/null 2>&1; then
14
+ echo "reactjsquality-check911 not found — run: npm install -g reactjsquality-check911"
15
+ exit 0
16
+ fi
17
+
18
+ CONFIG=".reactjs-quality-agent.json"
19
+ if [ ! -f "$CONFIG" ]; then
20
+ echo "No $CONFIG found — run: reactjsquality-check911 init"
21
+ exit 0
22
+ fi
23
+
24
+ reactjsquality-check911 quality
25
+ reactjsquality-check911 coverage
26
+ `;
27
+
28
+ const PRE_PUSH = `#!/bin/sh
29
+ # reactjsquality-check911 — pre-push hook
30
+ # Runs Playwright smoke tests before every push
31
+
32
+ set -e
33
+
34
+ if ! command -v reactjsquality-check911 >/dev/null 2>&1; then
35
+ echo "reactjsquality-check911 not found — run: npm install -g reactjsquality-check911"
36
+ exit 0
37
+ fi
38
+
39
+ CONFIG=".reactjs-quality-agent.json"
40
+ if [ ! -f "$CONFIG" ]; then
41
+ echo "No $CONFIG found — run: reactjsquality-check911 init"
42
+ exit 0
43
+ fi
44
+
45
+ reactjsquality-check911 playwright
46
+ `;
47
+
48
+ module.exports = function hooks() {
49
+ fs.mkdirSync(".githooks", { recursive: true });
50
+
51
+ fs.writeFileSync(".githooks/pre-commit", PRE_COMMIT);
52
+ fs.chmodSync(".githooks/pre-commit", 0o755);
53
+ console.log("✅ .githooks/pre-commit created (ESLint + Jest coverage on staged files)");
54
+
55
+ fs.writeFileSync(".githooks/pre-push", PRE_PUSH);
56
+ fs.chmodSync(".githooks/pre-push", 0o755);
57
+ console.log("✅ .githooks/pre-push created (Playwright smoke tests before every push)");
58
+
59
+ try {
60
+ execSync("git config core.hooksPath .githooks", { stdio: "pipe" });
61
+ console.log("✅ git configured to use .githooks/");
62
+ } catch (_) {
63
+ console.log("⚠️ Could not set core.hooksPath — run manually: git config core.hooksPath .githooks");
64
+ }
65
+
66
+ console.log(`
67
+ Hooks installed. From now on:
68
+
69
+ git commit → ESLint errors on changed chunks block the commit
70
+ Jest coverage < 80% on changed files blocks the commit
71
+
72
+ git push → Playwright smoke tests must all pass before push is allowed
73
+ `);
74
+ };
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline");
6
+
7
+ const PKG_ROOT = path.join(__dirname, "..");
8
+
9
+ const FRAMEWORKS = [
10
+ { id: "react", label: "React" },
11
+ { id: "nextjs", label: "Next.js" },
12
+ { id: "vue", label: "Vue / Nuxt" },
13
+ { id: "angular", label: "Angular" },
14
+ { id: "vanilla", label: "Vanilla JS / TypeScript" }
15
+ ];
16
+
17
+ const CHECKS = [
18
+ { id: "eslint", label: "ESLint — code style and error rules on staged chunks" },
19
+ { id: "coverage", label: "Coverage — Jest line coverage (80% threshold per changed file)" },
20
+ { id: "playwright", label: "Playwright — smoke tests run before every git push" }
21
+ ];
22
+
23
+ const COPILOT_INSTRUCTIONS = {
24
+ react: `# React Quality Agent
25
+
26
+ You are a Senior React Engineer embedded in this repository.
27
+
28
+ ## Your Responsibilities
29
+ - Generate Jest + React Testing Library tests for every component and hook
30
+ - Fix ESLint and TypeScript violations in staged files
31
+ - Ensure 80% Jest line coverage on changed files
32
+ - Generate Playwright tests for new pages and user flows
33
+
34
+ ## How to Work Efficiently
35
+ Use the \`index\` MCP tool to find relevant components before answering.
36
+ Use the \`read_file\` MCP tool to read only the specific file needed.
37
+ Use the \`fix\` MCP tool when the user asks to fix quality issues.
38
+
39
+ ## Test Generation Rules — Jest + React Testing Library
40
+ - File naming: \`{ComponentName}.test.tsx\`
41
+ - Method naming: \`should {expected behavior} when {condition}\`
42
+ - Cover: render output, user interactions, props, error boundaries, loading states
43
+ - Use \`@testing-library/user-event\` for all user interactions (click, type, select)
44
+ - Mock API calls with \`jest.fn()\` or \`msw\` (Mock Service Worker)
45
+ - Use \`screen.getByRole\`, \`getByText\`, \`getByTestId\` — avoid querying by class/id
46
+
47
+ ## Test Generation Rules — Playwright
48
+ - File naming: \`tests/{page-name}.spec.ts\`
49
+ - Cover: happy path, form validation, navigation flows, error states
50
+ - Use page object model pattern
51
+ - Assertions: \`expect(locator).toBeVisible()\`, \`toHaveText()\`, \`toHaveURL()\`
52
+
53
+ ## Quality Standards
54
+ - ESLint: react/recommended + typescript-eslint/recommended
55
+ - No unused variables, no console.log in production code, no implicit \`any\`
56
+ - Jest: 80% minimum line coverage on changed files
57
+ - Playwright: all smoke tests must pass before every push
58
+ `,
59
+ nextjs: `# Next.js Quality Agent
60
+
61
+ You are a Senior Next.js Engineer embedded in this repository.
62
+
63
+ ## Your Responsibilities
64
+ - Generate Jest + React Testing Library tests for components and API routes
65
+ - Fix ESLint and TypeScript violations in staged files
66
+ - Ensure 80% Jest line coverage on changed files
67
+ - Generate Playwright tests for new pages
68
+
69
+ ## Test Generation Rules — Jest
70
+ - File naming: \`{name}.test.tsx\` (components), \`{route}.test.ts\` (API routes)
71
+ - For API routes: use \`NextRequest\` / \`NextResponse\` mocks
72
+ - For server components: test the data-fetching logic separately from rendering
73
+ - For client components: use React Testing Library same as React
74
+
75
+ ## Test Generation Rules — Playwright
76
+ - File naming: \`tests/{page-name}.spec.ts\`
77
+ - Cover: page navigation, SSR content visible, client-side transitions
78
+ - Use \`page.goto()\` with full paths matching Next.js routing
79
+
80
+ ## Quality Standards
81
+ - ESLint: next/core-web-vitals rules
82
+ - No \`<img>\` — use Next.js \`<Image>\`
83
+ - No \`<a>\` for internal links — use \`<Link>\`
84
+ - Jest: 80% minimum line coverage
85
+ `,
86
+ vue: `# Vue Quality Agent
87
+
88
+ You are a Senior Vue Engineer embedded in this repository.
89
+
90
+ ## Your Responsibilities
91
+ - Generate Jest + Vue Test Utils tests for every component
92
+ - Fix ESLint and TypeScript violations in staged files
93
+ - Ensure 80% Jest line coverage on changed files
94
+ - Generate Playwright tests for new pages
95
+
96
+ ## Test Generation Rules — Jest
97
+ - File naming: \`{ComponentName}.test.ts\`
98
+ - Use \`mount\` for full rendering, \`shallowMount\` for unit isolation
99
+ - Cover: props, emits, slots, user interactions, computed values
100
+ - Mock Pinia stores with \`createTestingPinia()\`
101
+
102
+ ## Quality Standards
103
+ - ESLint: vue3/recommended + TypeScript
104
+ - Composition API only — no Options API
105
+ - Jest: 80% minimum line coverage
106
+ `,
107
+ angular: `# Angular Quality Agent
108
+
109
+ You are a Senior Angular Engineer embedded in this repository.
110
+
111
+ ## Your Responsibilities
112
+ - Generate Jest tests for components, services, guards, and pipes
113
+ - Fix ESLint and TypeScript violations in staged files
114
+ - Ensure 80% Jest line coverage on changed files
115
+
116
+ ## Test Generation Rules — Jest
117
+ - File naming: \`{name}.spec.ts\`
118
+ - Use \`TestBed.configureTestingModule()\` for component and service tests
119
+ - Use \`HttpClientTestingModule\` + \`HttpTestingController\` for HTTP services
120
+ - Cover: component rendering, service methods, guards, directive behavior
121
+
122
+ ## Quality Standards
123
+ - ESLint: Angular recommended + TypeScript strict
124
+ - Jest: 80% minimum line coverage
125
+ `,
126
+ vanilla: `# Vanilla JS/TS Quality Agent
127
+
128
+ You are a Senior JavaScript/TypeScript Engineer embedded in this repository.
129
+
130
+ ## Your Responsibilities
131
+ - Generate Jest tests for every exported function and class
132
+ - Fix ESLint and TypeScript violations in staged files
133
+ - Ensure 80% Jest line coverage on changed files
134
+
135
+ ## Test Generation Rules — Jest
136
+ - File naming: \`{module-name}.test.ts\`
137
+ - Cover: return values, side effects, edge cases, thrown exceptions
138
+ - Mock dependencies with \`jest.fn()\` and \`jest.spyOn()\`
139
+
140
+ ## Quality Standards
141
+ - ESLint: recommended + TypeScript strict
142
+ - Jest: 80% minimum line coverage
143
+ `
144
+ };
145
+
146
+ function ask(rl, question) {
147
+ return new Promise(resolve => rl.question(question, resolve));
148
+ }
149
+
150
+ module.exports = async function init() {
151
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
152
+
153
+ console.log("\nreactjsquality-check911 — setup\n");
154
+
155
+ // ── Framework selection ───────────────────────────────────────────────────
156
+ console.log("Select your frontend framework:");
157
+ FRAMEWORKS.forEach((f, i) => console.log(` ${i + 1}. ${f.label}`));
158
+ const fwRaw = (await ask(rl, "\nEnter number (default: 1 — React): ")).trim() || "1";
159
+ const fwIdx = parseInt(fwRaw) - 1;
160
+ const framework = FRAMEWORKS[fwIdx] || FRAMEWORKS[0];
161
+ console.log(`Selected: ${framework.label}\n`);
162
+
163
+ // ── Quality checks selection ──────────────────────────────────────────────
164
+ console.log("Select quality checks to enable:");
165
+ CHECKS.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`));
166
+ const checksRaw = (await ask(rl, "\nEnter numbers separated by commas (press Enter to enable all): ")).trim();
167
+ rl.close();
168
+
169
+ let selectedIds;
170
+ if (!checksRaw) {
171
+ selectedIds = CHECKS.map(c => c.id);
172
+ } else {
173
+ selectedIds = checksRaw.split(",").map(n => {
174
+ const idx = parseInt(n.trim()) - 1;
175
+ return CHECKS[idx] ? CHECKS[idx].id : null;
176
+ }).filter(Boolean);
177
+ }
178
+
179
+ const checks = {};
180
+ for (const c of CHECKS) checks[c.id] = selectedIds.includes(c.id);
181
+
182
+ // ── Write .reactjs-quality-agent.json ────────────────────────────────────
183
+ const config = { framework: framework.id, checks };
184
+ fs.writeFileSync(".reactjs-quality-agent.json", JSON.stringify(config, null, 2));
185
+ console.log("\n✅ .reactjs-quality-agent.json created");
186
+
187
+ // ── .github/copilot-instructions.md ──────────────────────────────────────
188
+ fs.mkdirSync(".github/instructions", { recursive: true });
189
+ const instructions = COPILOT_INSTRUCTIONS[framework.id] || COPILOT_INSTRUCTIONS.react;
190
+ fs.writeFileSync(".github/copilot-instructions.md", instructions);
191
+ console.log("✅ .github/copilot-instructions.md created");
192
+
193
+ // ── .vscode/mcp.json ─────────────────────────────────────────────────────
194
+ fs.mkdirSync(".vscode", { recursive: true });
195
+ const mcpServerPath = path.join(PKG_ROOT, "mcp-server", "server.py");
196
+ const mcpConfig = {
197
+ servers: {
198
+ "reactjs-quality-agent": {
199
+ type: "stdio",
200
+ command: "python",
201
+ args: [mcpServerPath],
202
+ env: {}
203
+ }
204
+ }
205
+ };
206
+ fs.writeFileSync(".vscode/mcp.json", JSON.stringify(mcpConfig, null, 2));
207
+ console.log("✅ .vscode/mcp.json configured");
208
+
209
+ // ── docs stubs ────────────────────────────────────────────────────────────
210
+ fs.mkdirSync("docs", { recursive: true });
211
+ if (!fs.existsSync("docs/architecture.md"))
212
+ fs.writeFileSync("docs/architecture.md", `# Frontend Architecture\n\nRun \`reactjsquality-check911 scan\` to generate this file.\n`);
213
+ if (!fs.existsSync("docs/component-index.md"))
214
+ fs.writeFileSync("docs/component-index.md", `# Component Index\n\nRun \`reactjsquality-check911 scan\` to generate this file.\n`);
215
+ console.log("✅ docs/ stubs created");
216
+
217
+ console.log(`
218
+ Setup complete for ${framework.label}!
219
+
220
+ Enabled checks:
221
+ ${Object.entries(checks).map(([k, v]) => ` ${v ? "✅" : "❌"} ${k}`).join("\n")}
222
+
223
+ Next steps:
224
+ reactjsquality-check911 scan — index your components for Copilot
225
+ reactjsquality-check911 hooks — install pre-commit and pre-push git hooks
226
+ `);
227
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ const { execSync } = require("child_process");
4
+ const fs = require("fs");
5
+
6
+ const CONFIG_FILE = ".reactjs-quality-agent.json";
7
+
8
+ module.exports = function playwright() {
9
+ const config = (() => {
10
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
11
+ })();
12
+
13
+ if (!config) {
14
+ console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
15
+ process.exit(1);
16
+ }
17
+ if (!config.checks.playwright) {
18
+ console.log("Playwright check is disabled — skipping.");
19
+ return;
20
+ }
21
+
22
+ // Verify playwright is installed
23
+ if (!fs.existsSync("node_modules/.bin/playwright") && !fs.existsSync("node_modules/@playwright/test")) {
24
+ console.log("⚠️ Playwright not installed — skipping.");
25
+ console.log(" Run: npm install -D @playwright/test && npx playwright install");
26
+ return;
27
+ }
28
+
29
+ // Verify playwright config exists
30
+ const configFiles = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"];
31
+ if (!configFiles.some(f => fs.existsSync(f))) {
32
+ console.log("⚠️ No playwright.config.ts found — skipping.");
33
+ console.log(" Run: npx playwright init");
34
+ return;
35
+ }
36
+
37
+ console.log("Running Playwright smoke tests...\n");
38
+
39
+ try {
40
+ const out = execSync("npx playwright test --reporter=list", {
41
+ encoding: "utf8",
42
+ stdio: ["pipe", "pipe", "pipe"]
43
+ });
44
+ console.log(out);
45
+ console.log("✅ All Playwright tests passed.");
46
+ } catch (e) {
47
+ const output = (e.stdout || "") + (e.stderr || "");
48
+ console.log(output);
49
+ console.log("\n❌ Playwright tests failed — fix failing tests before pushing.");
50
+ process.exit(1);
51
+ }
52
+ };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ const { execSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const CONFIG_FILE = ".reactjs-quality-agent.json";
8
+
9
+ function loadConfig() {
10
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch (_) { return null; }
11
+ }
12
+
13
+ function getStagedFiles() {
14
+ try {
15
+ const out = execSync("git diff --cached --name-only", { encoding: "utf8" });
16
+ return out.split("\n").map(f => f.trim())
17
+ .filter(f => f && /\.(js|jsx|ts|tsx|vue|svelte)$/.test(f) && fs.existsSync(f));
18
+ } catch (_) { return []; }
19
+ }
20
+
21
+ function getChangedLineRanges() {
22
+ try {
23
+ const diff = execSync("git diff --cached --unified=0", { encoding: "utf8" });
24
+ const ranges = {};
25
+ let cur = null;
26
+ for (const line of diff.split("\n")) {
27
+ const fileM = /^\+\+\+ b\/(.+)$/.exec(line);
28
+ if (fileM) { cur = fileM[1]; if (!ranges[cur]) ranges[cur] = []; continue; }
29
+ if (line.startsWith("+++ /dev/null")) { cur = null; continue; }
30
+ const hunkM = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
31
+ if (hunkM && cur) {
32
+ const start = parseInt(hunkM[1]);
33
+ const count = hunkM[2] !== undefined ? parseInt(hunkM[2]) : 1;
34
+ if (count > 0) ranges[cur].push([start, start + count - 1]);
35
+ }
36
+ }
37
+ return ranges;
38
+ } catch (_) { return {}; }
39
+ }
40
+
41
+ function findRanges(filePath, changedRanges) {
42
+ const norm = filePath.replace(/\\/g, "/").toLowerCase();
43
+ for (const [gitPath, ranges] of Object.entries(changedRanges)) {
44
+ const gn = gitPath.toLowerCase();
45
+ if (norm === gn || norm.endsWith("/" + gn)) return ranges;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function inChangedRange(lineNum, ranges) {
51
+ if (!ranges || isNaN(lineNum)) return false;
52
+ return ranges.some(([s, e]) => lineNum >= s && lineNum <= e);
53
+ }
54
+
55
+ function runEslint(staged, changedRanges) {
56
+ process.stdout.write("Running ESLint... ");
57
+ try {
58
+ const fileArgs = staged.map(f => `"${f}"`).join(" ");
59
+ let output = "";
60
+ try {
61
+ output = execSync(`npx eslint ${fileArgs} --format json`, {
62
+ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"]
63
+ });
64
+ } catch (e) {
65
+ // ESLint exits with code 1 when violations found — stdout still has JSON
66
+ output = e.stdout || "[]";
67
+ }
68
+
69
+ const data = JSON.parse(output || "[]");
70
+ const viols = [];
71
+ const errors = [];
72
+
73
+ for (const file of data) {
74
+ const ranges = findRanges(file.filePath, changedRanges);
75
+ if (!ranges) continue;
76
+ for (const msg of file.messages) {
77
+ if (!inChangedRange(msg.line, ranges)) continue;
78
+ const label = msg.severity === 2 ? "error" : "warn";
79
+ const entry = ` [ESLint:${msg.ruleId || "unknown"}] ${path.basename(file.filePath)}:${msg.line} — ${msg.message} (${label})`;
80
+ viols.push(entry);
81
+ if (msg.severity === 2) errors.push(entry);
82
+ }
83
+ }
84
+
85
+ if (errors.length) {
86
+ console.log("❌");
87
+ viols.forEach(v => console.log(v));
88
+ return { passed: false, summary: `❌ ESLint: ${errors.length} error(s) in your changed lines` };
89
+ }
90
+ console.log("✅");
91
+ if (viols.length) viols.forEach(v => console.log(v)); // show warnings only
92
+ return { passed: true, summary: "✅ ESLint passed" };
93
+ } catch (e) {
94
+ console.log("⚠️");
95
+ return { passed: true, summary: "⚠️ ESLint: not installed — run `npm install eslint` in your project" };
96
+ }
97
+ }
98
+
99
+ function runTypeScript(staged) {
100
+ const tsFiles = staged.filter(f => f.endsWith(".ts") || f.endsWith(".tsx"));
101
+ process.stdout.write("Running TypeScript... ");
102
+ if (!tsFiles.length) {
103
+ console.log("✅ (no TS files staged)");
104
+ return { passed: true, summary: "✅ TypeScript: no TS files staged" };
105
+ }
106
+ try {
107
+ execSync("npx tsc --noEmit", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
108
+ console.log("✅");
109
+ return { passed: true, summary: "✅ TypeScript passed" };
110
+ } catch (e) {
111
+ const lines = ((e.stdout || "") + (e.stderr || ""))
112
+ .split("\n").filter(l => l.includes("error TS")).slice(0, 5);
113
+ console.log("❌");
114
+ lines.forEach(l => console.log(" ", l.trim()));
115
+ return { passed: false, summary: "❌ TypeScript: type errors found" };
116
+ }
117
+ }
118
+
119
+ module.exports = function quality() {
120
+ const config = loadConfig();
121
+ if (!config) {
122
+ console.error("No .reactjs-quality-agent.json found — run `reactjsquality-check911 init` first.");
123
+ process.exit(1);
124
+ }
125
+
126
+ const staged = getStagedFiles();
127
+ if (!staged.length) {
128
+ console.log("No staged JS/TS files — skipping quality checks.");
129
+ return;
130
+ }
131
+
132
+ const changedRanges = getChangedLineRanges();
133
+ console.log(`Checking changed chunks in: ${staged.map(f => path.basename(f)).join(", ")}\n`);
134
+
135
+ const results = [];
136
+ let failed = false;
137
+
138
+ if (config.checks.eslint !== false) {
139
+ const r = runEslint(staged, changedRanges);
140
+ results.push(r.summary);
141
+ if (!r.passed) failed = true;
142
+ }
143
+
144
+ const r2 = runTypeScript(staged);
145
+ results.push(r2.summary);
146
+ if (!r2.passed) failed = true;
147
+
148
+ console.log("\n--- Quality Summary ---");
149
+ results.forEach(r => console.log(r));
150
+
151
+ if (failed) {
152
+ console.log("\nFix violations and commit again, or ask Copilot:");
153
+ console.log(" 'Fix the quality violations in my staged files'");
154
+ process.exit(1);
155
+ }
156
+
157
+ console.log("\nAll quality checks passed.");
158
+ };