superkit-mcp-server 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/ARCHITECTURE.md +2 -3
  2. package/README.md +1 -0
  3. package/build/index.js +75 -0
  4. package/build/tools/autoPreview.js +99 -0
  5. package/build/tools/checklist.js +120 -0
  6. package/build/tools/sessionManager.js +107 -0
  7. package/build/tools/validators/__tests__/apiSchema.test.js +77 -0
  8. package/build/tools/validators/__tests__/convertRules.test.js +38 -0
  9. package/build/tools/validators/__tests__/frontendDesign.test.js +55 -0
  10. package/build/tools/validators/__tests__/geoChecker.test.js +45 -0
  11. package/build/tools/validators/__tests__/i18nChecker.test.js +32 -0
  12. package/build/tools/validators/__tests__/lintRunner.test.js +65 -0
  13. package/build/tools/validators/__tests__/mobileAudit.test.js +40 -0
  14. package/build/tools/validators/__tests__/playwrightRunner.test.js +55 -0
  15. package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +49 -0
  16. package/build/tools/validators/__tests__/securityScan.test.js +42 -0
  17. package/build/tools/validators/__tests__/seoChecker.test.js +44 -0
  18. package/build/tools/validators/__tests__/testRunner.test.js +49 -0
  19. package/build/tools/validators/__tests__/typeCoverage.test.js +62 -0
  20. package/build/tools/validators/accessibilityChecker.js +124 -0
  21. package/build/tools/validators/apiValidator.js +140 -0
  22. package/build/tools/validators/convertRules.js +170 -0
  23. package/build/tools/validators/geoChecker.js +176 -0
  24. package/build/tools/validators/i18nChecker.js +205 -0
  25. package/build/tools/validators/lighthouseAudit.js +50 -0
  26. package/build/tools/validators/lintRunner.js +106 -0
  27. package/build/tools/validators/mobileAudit.js +190 -0
  28. package/build/tools/validators/playwrightRunner.js +101 -0
  29. package/build/tools/validators/reactPerformanceChecker.js +199 -0
  30. package/build/tools/validators/schemaValidator.js +105 -0
  31. package/build/tools/validators/securityScan.js +215 -0
  32. package/build/tools/validators/seoChecker.js +122 -0
  33. package/build/tools/validators/testRunner.js +111 -0
  34. package/build/tools/validators/typeCoverage.js +150 -0
  35. package/build/tools/validators/uxAudit.js +222 -0
  36. package/build/tools/verifyAll.js +159 -0
  37. package/package.json +5 -3
  38. package/skills/tech/api-patterns/SKILL.md +1 -1
  39. package/skills/tech/clean-code/SKILL.md +14 -14
  40. package/skills/tech/doc.md +3 -3
  41. package/skills/tech/frontend-design/SKILL.md +1 -1
  42. package/skills/tech/geo-fundamentals/SKILL.md +1 -1
  43. package/skills/tech/i18n-localization/SKILL.md +1 -1
  44. package/skills/tech/lint-and-validate/SKILL.md +2 -2
  45. package/skills/tech/mobile-design/SKILL.md +1 -1
  46. package/skills/tech/nextjs-react-expert/SKILL.md +1 -1
  47. package/skills/tech/parallel-agents/SKILL.md +3 -3
  48. package/skills/tech/performance-profiling/SKILL.md +1 -1
  49. package/skills/tech/vulnerability-scanner/SKILL.md +1 -1
  50. package/skills/tech/webapp-testing/SKILL.md +3 -3
  51. package/workflows/review-compound.md +1 -1
  52. package/skills/tech/api-patterns/scripts/api_validator.py +0 -211
  53. package/skills/tech/database-design/scripts/schema_validator.py +0 -172
  54. package/skills/tech/frontend-design/scripts/accessibility_checker.py +0 -183
  55. package/skills/tech/frontend-design/scripts/ux_audit.py +0 -722
  56. package/skills/tech/geo-fundamentals/scripts/geo_checker.py +0 -289
  57. package/skills/tech/i18n-localization/scripts/i18n_checker.py +0 -241
  58. package/skills/tech/lint-and-validate/scripts/lint_runner.py +0 -184
  59. package/skills/tech/lint-and-validate/scripts/type_coverage.py +0 -173
  60. package/skills/tech/mobile-design/scripts/mobile_audit.py +0 -670
  61. package/skills/tech/nextjs-react-expert/scripts/convert_rules.py +0 -222
  62. package/skills/tech/nextjs-react-expert/scripts/react_performance_checker.py +0 -252
  63. package/skills/tech/performance-profiling/scripts/lighthouse_audit.py +0 -76
  64. package/skills/tech/seo-fundamentals/scripts/seo_checker.py +0 -219
  65. package/skills/tech/testing-patterns/scripts/test_runner.py +0 -219
  66. package/skills/tech/vulnerability-scanner/scripts/security_scan.py +0 -458
  67. package/skills/tech/webapp-testing/scripts/playwright_runner.py +0 -173
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { checkGeoPage } from '../geoChecker.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('geoChecker', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ describe('checkGeoPage', () => {
10
+ it('should detect structured schema and H1s', async () => {
11
+ vi.mocked(fs.readFile).mockResolvedValue(`
12
+ <html lang="en">
13
+ <head>
14
+ <script type="application/ld+json">
15
+ {
16
+ "@type": "Article",
17
+ "author": "Test Author"
18
+ }
19
+ </script>
20
+ </head>
21
+ <body>
22
+ <h1>Main Title</h1>
23
+ <p>Some content with numbers like 50% and $100.</p>
24
+ </body>
25
+ </html>
26
+ `);
27
+ const result = await checkGeoPage('/mock/page.html');
28
+ expect(result.passed.some(m => m.includes('JSON-LD structured data'))).toBe(true);
29
+ expect(result.passed.some(m => m.includes('Single H1'))).toBe(true);
30
+ expect(result.passed.some(m => m.includes('Original statistics'))).toBe(true);
31
+ expect(result.score).toBeGreaterThan(0);
32
+ });
33
+ it('should flag missing structural tags', async () => {
34
+ vi.mocked(fs.readFile).mockResolvedValue(`
35
+ <body>
36
+ <p>No headings, no structure, no stats.</p>
37
+ </body>
38
+ `);
39
+ const result = await checkGeoPage('/mock/bad.html');
40
+ expect(result.issues.some(m => m.includes('No JSON-LD'))).toBe(true);
41
+ expect(result.issues.some(m => m.includes('No H1 heading'))).toBe(true);
42
+ expect(result.score).toBeLessThan(50);
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { checkLocaleCompleteness } from '../i18nChecker.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('i18nChecker', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ describe('checkLocaleCompleteness', () => {
10
+ it('should detect missing translation keys in other locales', async () => {
11
+ // Mock translation files
12
+ vi.mocked(fs.readFile).mockImplementation(async (file) => {
13
+ if (file.includes('en'))
14
+ return JSON.stringify({ hello: 'World', nested: { a: 1, b: 2 } });
15
+ if (file.includes('fr'))
16
+ return JSON.stringify({ hello: 'Monde', nested: { a: 1 } });
17
+ return "{}";
18
+ });
19
+ const result = await checkLocaleCompleteness(['/locales/en/common.json', '/locales/fr/common.json']);
20
+ expect(result.issues.some(i => i.includes('Missing 1 keys'))).toBe(true);
21
+ expect(result.passed.some(p => p.includes('Found 2 language'))).toBe(true);
22
+ });
23
+ it('should pass matching keys', async () => {
24
+ vi.mocked(fs.readFile).mockImplementation(async (file) => {
25
+ return JSON.stringify({ a: 1, b: 2 });
26
+ });
27
+ const result = await checkLocaleCompleteness(['/en/app.json', '/es/app.json']);
28
+ expect(result.issues.length).toBe(0);
29
+ expect(result.passed.some(p => p.includes('matching keys'))).toBe(true);
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { detectProjectType, runLinter } from '../lintRunner.js';
3
+ import * as fs from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import * as cp from 'child_process';
6
+ // Mock fs and child_process modules
7
+ vi.mock('fs/promises');
8
+ vi.mock('fs', () => ({ existsSync: vi.fn() }));
9
+ vi.mock('child_process');
10
+ describe('lintRunner', () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+ describe('detectProjectType', () => {
15
+ it('should detect a node project with lint script', async () => {
16
+ existsSync.mockImplementation((p) => p.endsWith('package.json'));
17
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
18
+ scripts: { lint: 'eslint .' }
19
+ }));
20
+ const info = await detectProjectType('/mock/path');
21
+ expect(info.type).toBe('node');
22
+ expect(info.linters).toEqual([{ name: 'npm lint', cmd: ['npm', 'run', 'lint'] }]);
23
+ });
24
+ it('should detect a python project with ruff', async () => {
25
+ existsSync.mockImplementation((p) => p.endsWith('pyproject.toml') || p.endsWith('mypy.ini'));
26
+ const info = await detectProjectType('/mock/python');
27
+ expect(info.type).toBe('python');
28
+ expect(info.linters).toContainEqual({ name: 'ruff', cmd: ['ruff', 'check', '.'] });
29
+ expect(info.linters).toContainEqual({ name: 'mypy', cmd: ['mypy', '.'] });
30
+ });
31
+ });
32
+ describe('runLinter', () => {
33
+ it('should return passed on success code', async () => {
34
+ // Mock spawn
35
+ const mockChild = {
36
+ stdout: { on: vi.fn((event, cb) => cb('success out')) },
37
+ stderr: { on: vi.fn() },
38
+ on: vi.fn((event, cb) => {
39
+ if (event === 'close')
40
+ cb(0);
41
+ }),
42
+ kill: vi.fn()
43
+ };
44
+ vi.mocked(cp.spawn).mockReturnValue(mockChild);
45
+ const result = await runLinter({ name: 'test', cmd: ['npm', 'test'] }, '/mock');
46
+ expect(result.passed).toBe(true);
47
+ expect(result.output).toContain('success out');
48
+ });
49
+ it('should return failed on error code', async () => {
50
+ const mockChild = {
51
+ stdout: { on: vi.fn() },
52
+ stderr: { on: vi.fn((event, cb) => cb('error out')) },
53
+ on: vi.fn((event, cb) => {
54
+ if (event === 'close')
55
+ cb(1);
56
+ }),
57
+ kill: vi.fn()
58
+ };
59
+ vi.mocked(cp.spawn).mockReturnValue(mockChild);
60
+ const result = await runLinter({ name: 'failtest', cmd: ['npm', 'fail'] }, '/mock');
61
+ expect(result.passed).toBe(false);
62
+ expect(result.error).toContain('error out');
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { runMobileAudit } from '../mobileAudit.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('mobileAudit', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ it('should report warnings for generic rn file issues', async () => {
10
+ vi.mocked(fs.readdir).mockResolvedValue([{
11
+ name: 'Screen.tsx',
12
+ isDirectory: () => false
13
+ }]);
14
+ vi.mocked(fs.readFile).mockResolvedValue(`
15
+ import React from 'react';
16
+ import { View, ScrollView } from 'react-native';
17
+ export const Screen = () => {
18
+ return (
19
+ <ScrollView>
20
+ {data.map(d => <View key={index} />)}
21
+ </ScrollView>
22
+ )
23
+ }
24
+ `);
25
+ const res = await runMobileAudit('.');
26
+ expect(res.passed).toBe(false);
27
+ expect(res.report).toContain('ScrollView with .map()'); // critical performance
28
+ });
29
+ it('should ignore non react native / flutter files', async () => {
30
+ vi.mocked(fs.readdir).mockResolvedValue([{
31
+ name: 'regular.tsx',
32
+ isDirectory: () => false
33
+ }]);
34
+ vi.mocked(fs.readFile).mockResolvedValue(`
35
+ export const App = () => <div>Hello Web</div>
36
+ `);
37
+ const res = await runMobileAudit('.');
38
+ expect(res.passed).toBe(true);
39
+ });
40
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { runPlaywrightTest, runPlaywrightA11y } from '../playwrightRunner.js';
3
+ vi.mock('playwright', () => {
4
+ return {
5
+ chromium: {
6
+ launch: vi.fn().mockResolvedValue({
7
+ newContext: vi.fn().mockResolvedValue({
8
+ newPage: vi.fn().mockResolvedValue({
9
+ goto: vi.fn().mockResolvedValue({
10
+ status: () => 200,
11
+ ok: () => true
12
+ }),
13
+ title: vi.fn().mockResolvedValue('Mock Title'),
14
+ url: () => 'https://example.com',
15
+ locator: vi.fn().mockReturnValue({ count: vi.fn().mockResolvedValue(5) }),
16
+ on: vi.fn(),
17
+ evaluate: vi.fn().mockResolvedValue('{"dom_content_loaded":100,"load_complete":200}'),
18
+ screenshot: vi.fn().mockResolvedValue(true)
19
+ })
20
+ }),
21
+ newPage: vi.fn().mockResolvedValue({
22
+ goto: vi.fn().mockResolvedValue({
23
+ status: () => 200,
24
+ ok: () => true
25
+ }),
26
+ locator: vi.fn().mockReturnValue({ count: vi.fn().mockResolvedValue(3) })
27
+ }),
28
+ close: vi.fn().mockResolvedValue(undefined)
29
+ })
30
+ }
31
+ };
32
+ });
33
+ describe('playwrightRunner', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+ it('should successfully run a basic browser test', async () => {
38
+ const res = await runPlaywrightTest('https://example.com');
39
+ console.log("PLAYWRIGHT TEST:", res);
40
+ expect(res.error).toBeUndefined();
41
+ expect(res.status).toBe('success');
42
+ expect(res.health.loaded).toBe(true);
43
+ expect(res.performance.dom_content_loaded).toBe(100);
44
+ expect(res.elements.links).toBe(5);
45
+ expect(res.summary).toContain('[OK] Page loaded');
46
+ });
47
+ it('should calculate accessibility counts properly', async () => {
48
+ const res = await runPlaywrightA11y('https://example.com');
49
+ console.log("PLAYWRIGHT A11y:", res);
50
+ expect(res.error).toBeUndefined();
51
+ expect(res.status).toBe('success');
52
+ expect(res.accessibility.images_with_alt).toBe(3);
53
+ expect(res.accessibility.headings.h1).toBe(3);
54
+ });
55
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { runReactPerformanceChecker } from '../reactPerformanceChecker.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('reactPerformanceChecker', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ it('should detect waterfalls and barrel imports', async () => {
10
+ vi.mocked(fs.readdir).mockResolvedValue([{
11
+ name: 'Home.tsx',
12
+ isDirectory: () => false
13
+ }]);
14
+ vi.mocked(fs.readFile).mockResolvedValue(`
15
+ import { something } from '@/components/index';
16
+
17
+ async function loadData() {
18
+ const a = await fetchA();
19
+ const b = await fetchB();
20
+ return { a, b };
21
+ }
22
+ `);
23
+ const res = await runReactPerformanceChecker('.');
24
+ console.log("RPC BAD REPORT:", res.report);
25
+ expect(res.passed).toBe(false); // CRITICAL issue makes it fail
26
+ expect(res.report).toContain('Sequential awaits detected (waterfall)');
27
+ expect(res.report).toContain('Potential barrel imports detected');
28
+ });
29
+ it('should pass cleanly configured react app', async () => {
30
+ vi.mocked(fs.readdir).mockResolvedValue([{
31
+ name: 'App.tsx',
32
+ isDirectory: () => false
33
+ }]);
34
+ vi.mocked(fs.readFile).mockResolvedValue(`
35
+ import { useQuery } from '@tanstack/react-query';
36
+ import Image from 'next/image';
37
+
38
+ const App = React.memo((props: Props) => {
39
+ const q = useQuery('data', fetchParallel);
40
+ return <Image src="foo" alt="bar" />
41
+ });
42
+ export default App;
43
+ `);
44
+ const res = await runReactPerformanceChecker('.');
45
+ console.log("RPC GOOD REPORT:", res.report);
46
+ expect(res.passed).toBe(true);
47
+ expect(res.report).toContain('[SUCCESS]');
48
+ });
49
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { scanSecrets, scanCodePatterns } from '../securityScan.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('securityScan', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ describe('scanSecrets', () => {
10
+ it('should detect AWS keys', async () => {
11
+ // Mock file system
12
+ vi.mocked(fs.readdir).mockResolvedValue([{
13
+ name: 'config.json',
14
+ isDirectory: () => false
15
+ }]);
16
+ vi.mocked(fs.readFile).mockResolvedValue(`
17
+ { "aws_key": "AKIA1234567890123456" }
18
+ `);
19
+ const result = await scanSecrets('/mock');
20
+ expect(result.findings).toBeDefined();
21
+ expect(result.findings.some((f) => f.type === 'AWS Access Key')).toBe(true);
22
+ expect(result.by_severity.critical).toBeGreaterThan(0);
23
+ });
24
+ });
25
+ describe('scanCodePatterns', () => {
26
+ it('should detect eval() and child_process.exec()', async () => {
27
+ vi.mocked(fs.readdir).mockResolvedValue([{
28
+ name: 'bad_code.js',
29
+ isDirectory: () => false
30
+ }]);
31
+ vi.mocked(fs.readFile).mockResolvedValue(`
32
+ eval('2 + 2');
33
+ import { exec } from 'child_process';
34
+ child_process.exec('rm -rf /');
35
+ `);
36
+ const result = await scanCodePatterns('/mock');
37
+ expect(result.findings.length).toBeGreaterThan(1);
38
+ expect(result.findings.some((f) => f.pattern === 'eval() usage')).toBe(true);
39
+ expect(result.findings.some((f) => f.pattern === 'child_process.exec')).toBe(true);
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { runSeoChecker } from '../seoChecker.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('seoChecker', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ it('should report SEO issues on a page', async () => {
10
+ vi.mocked(fs.readdir).mockResolvedValue([{
11
+ name: 'index.tsx', // valid page layout file
12
+ isDirectory: () => false
13
+ }]);
14
+ vi.mocked(fs.readFile).mockResolvedValue(`
15
+ export default function Index() {
16
+ return (
17
+ <html>
18
+ <head>
19
+ <title>Oops missing description and og</title>
20
+ </head>
21
+ <body>
22
+ <h1>Title</h1>
23
+ <h1>Duplicate Title</h1>
24
+ <img src="foo" />
25
+ <img src="bar" alt="" />
26
+ </body>
27
+ </html>
28
+ )
29
+ }
30
+ `);
31
+ const res = await runSeoChecker('.');
32
+ expect(res.passed).toBe(false);
33
+ expect(res.report).toContain('Image missing alt attribute');
34
+ });
35
+ it('should ignore non-pages files like utilities', async () => {
36
+ vi.mocked(fs.readdir).mockResolvedValue([{
37
+ name: 'api.util.ts',
38
+ isDirectory: () => false
39
+ }]);
40
+ const res = await runSeoChecker('.');
41
+ expect(res.passed).toBe(true);
42
+ expect(res.report).toContain('[!] No page files found.');
43
+ });
44
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { runTestRunner, detectTestFramework } from '../testRunner.js';
3
+ import * as fs from 'fs/promises';
4
+ import * as child_process from 'child_process';
5
+ vi.mock('fs/promises');
6
+ vi.mock('child_process', () => ({
7
+ exec: vi.fn((cmd, options, callback) => {
8
+ callback(null, { stdout: '1 passed', stderr: '' });
9
+ })
10
+ }));
11
+ describe('testRunner', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ it('should detect node test frameworks via package.json', async () => {
16
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
17
+ scripts: { test: "something" },
18
+ devDependencies: { vitest: "^1.0.0" }
19
+ }));
20
+ vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); // no python
21
+ const info = await detectTestFramework('.');
22
+ expect(info.type).toBe('node');
23
+ expect(info.framework).toBe('vitest');
24
+ expect(info.cmd).toEqual(['npm', 'test']);
25
+ expect(info.coverageCmd).toEqual(['npx', 'vitest', 'run', '--coverage']);
26
+ });
27
+ it('should detect python tests via pyproject.toml', async () => {
28
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT')); // no package.json
29
+ vi.mocked(fs.stat).mockResolvedValue({});
30
+ const info = await detectTestFramework('.');
31
+ expect(info.type).toBe('python');
32
+ expect(info.framework).toBe('pytest');
33
+ expect(info.cmd).toEqual(['python', '-m', 'pytest', '-v']);
34
+ });
35
+ it('should run tests and report success', async () => {
36
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
37
+ scripts: { test: "jest" }
38
+ }));
39
+ // Mock child_process.exec to simulate passing tests
40
+ vi.mocked(child_process.exec).mockImplementation((...args) => {
41
+ const cb = args[args.length - 1];
42
+ cb(null, { stdout: 'Tests: 1 passed, 1 total', stderr: '' });
43
+ return {};
44
+ });
45
+ const res = await runTestRunner('.');
46
+ expect(res.passed).toBe(true);
47
+ expect(res.report).toContain('Tests: 1 total, 1 passed, 0 failed');
48
+ });
49
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { checkTypescriptCoverage, checkPythonCoverage } from '../typeCoverage.js';
3
+ import * as fs from 'fs/promises';
4
+ vi.mock('fs/promises');
5
+ describe('typeCoverage', () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+ describe('checkTypescriptCoverage', () => {
10
+ it('should analyze a typescript file and find any types', async () => {
11
+ // Mock readdir to return 1 TS file
12
+ vi.mocked(fs.readdir).mockResolvedValue([{
13
+ name: 'index.ts',
14
+ isDirectory: () => false
15
+ }]);
16
+ // Mock readFile to return a snippet with an untyped function and an "any"
17
+ vi.mocked(fs.readFile).mockResolvedValue(`
18
+ function test(): any { return 1; }
19
+ const test2 = (x) => console.log(x);
20
+ `);
21
+ const result = await checkTypescriptCoverage('/mock/folder');
22
+ expect(result.type).toBe('typescript');
23
+ expect(result.files).toBe(1);
24
+ expect(result.stats.any_count).toBeGreaterThan(0);
25
+ expect(result.stats.untyped_functions).toBeGreaterThan(0);
26
+ });
27
+ it('should handle perfectly typed code without any', async () => {
28
+ vi.mocked(fs.readdir).mockResolvedValue([{
29
+ name: 'good.ts',
30
+ isDirectory: () => false
31
+ }]);
32
+ vi.mocked(fs.readFile).mockResolvedValue(`
33
+ function typedFunc(val: string): number { return val.length; }
34
+ `);
35
+ const result = await checkTypescriptCoverage('/mock/good');
36
+ expect(result.stats.any_count).toBe(0);
37
+ expect(result.stats.untyped_functions).toBe(0);
38
+ expect(result.passed.some(p => p.includes('100%'))).toBe(true);
39
+ });
40
+ });
41
+ describe('checkPythonCoverage', () => {
42
+ it('should detect Python type hint issues', async () => {
43
+ vi.mocked(fs.readdir).mockResolvedValue([{
44
+ name: 'script.py',
45
+ isDirectory: () => false
46
+ }]);
47
+ vi.mocked(fs.readFile).mockResolvedValue(`
48
+ def bad_func(arg):
49
+ pass
50
+
51
+ def good_func(arg: str) -> bool:
52
+ return True
53
+
54
+ def any_func(arg: Any) -> Any:
55
+ pass
56
+ `);
57
+ const result = await checkPythonCoverage('/mock/py');
58
+ expect(result.stats.any_count).toBeGreaterThan(0);
59
+ expect(result.stats.untyped_functions).toBe(1);
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,124 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ export async function findHtmlFiles(projectPath) {
4
+ let files = [];
5
+ const skipDirs = new Set(['node_modules', '.next', 'dist', 'build', '.git']);
6
+ async function search(dir) {
7
+ try {
8
+ const items = await fs.readdir(dir, { withFileTypes: true });
9
+ for (const item of items) {
10
+ if (skipDirs.has(item.name))
11
+ continue;
12
+ const fullPath = path.join(dir, item.name);
13
+ if (item.isDirectory())
14
+ await search(fullPath);
15
+ else {
16
+ const ext = path.extname(item.name).toLowerCase();
17
+ if (['.html', '.jsx', '.tsx'].includes(ext)) {
18
+ files.push(fullPath);
19
+ }
20
+ }
21
+ }
22
+ }
23
+ catch { }
24
+ }
25
+ await search(projectPath);
26
+ return files.slice(0, 50);
27
+ }
28
+ export async function checkAccessibility(filePath) {
29
+ const issues = [];
30
+ try {
31
+ const content = await fs.readFile(filePath, 'utf-8');
32
+ const lowerContent = content.toLowerCase();
33
+ const inputs = content.match(/<input[^>]*>/gi) || [];
34
+ for (const inp of inputs) {
35
+ if (!inp.toLowerCase().includes('type="hidden"')) {
36
+ if (!inp.toLowerCase().includes('aria-label') && !inp.toLowerCase().includes('id=')) {
37
+ issues.push("Input without label or aria-label");
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ const buttons = content.match(/<button[^>]*>[^<]*<\/button>/gi) || [];
43
+ for (const btn of buttons) {
44
+ if (!btn.toLowerCase().includes('aria-label')) {
45
+ const text = btn.replace(/<[^>]+>/g, '').trim();
46
+ if (!text) {
47
+ issues.push("Button without accessible text");
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ if (lowerContent.includes('<html') && !lowerContent.includes('lang=')) {
53
+ issues.push("Missing lang attribute on <html>");
54
+ }
55
+ if (lowerContent.includes('<main') || lowerContent.includes('<body')) {
56
+ if (!lowerContent.includes('skip') && !lowerContent.includes('#main')) {
57
+ issues.push("Consider adding skip-to-main-content link");
58
+ }
59
+ }
60
+ const onClickCount = (lowerContent.match(/onclick=/g) || []).length;
61
+ const keyCount = (lowerContent.match(/onkeydown=|onkeyup=/g) || []).length;
62
+ if (onClickCount > 0 && keyCount === 0) {
63
+ issues.push("onClick without keyboard handler (onKeyDown)");
64
+ }
65
+ if (lowerContent.includes('tabindex=')) {
66
+ if (!lowerContent.includes('tabindex="-1"') && !lowerContent.includes('tabindex="0"')) {
67
+ const positive = lowerContent.match(/tabindex="([1-9]\d*)"/);
68
+ if (positive)
69
+ issues.push("Avoid positive tabIndex values");
70
+ }
71
+ }
72
+ if (lowerContent.includes('autoplay') && !lowerContent.includes('muted')) {
73
+ issues.push("Autoplay media should be muted");
74
+ }
75
+ if (lowerContent.includes('role="button"')) {
76
+ const divButtons = content.match(/<div[^>]*role="button"[^>]*>/gi) || [];
77
+ for (const div of divButtons) {
78
+ if (!div.toLowerCase().includes('tabindex')) {
79
+ issues.push("role='button' without tabindex");
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ catch (e) {
86
+ issues.push(`Error reading file: ${e.message.substring(0, 50)}`);
87
+ }
88
+ return issues;
89
+ }
90
+ export async function runAccessibilityChecker(projectPath = ".") {
91
+ const root = path.resolve(projectPath);
92
+ let report = `============================================================\n`;
93
+ report += `[ACCESSIBILITY CHECKER] WCAG Compliance Audit\n`;
94
+ report += `============================================================\n`;
95
+ report += `Project: ${root}\n------------------------------------------------------------\n`;
96
+ const files = await findHtmlFiles(root);
97
+ report += `Found ${files.length} HTML/JSX/TSX files\n`;
98
+ if (files.length === 0) {
99
+ return { passed: true, report: report + "No HTML files found\n" };
100
+ }
101
+ const allIssues = [];
102
+ for (const file of files) {
103
+ const issues = await checkAccessibility(file);
104
+ if (issues.length > 0) {
105
+ allIssues.push({ file: path.basename(file), issues });
106
+ }
107
+ }
108
+ report += `\n============================================================\nACCESSIBILITY ISSUES\n============================================================\n`;
109
+ if (allIssues.length > 0) {
110
+ for (const item of allIssues.slice(0, 10)) {
111
+ report += `\n${item.file}:\n`;
112
+ for (const issue of item.issues)
113
+ report += ` - ${issue}\n`;
114
+ }
115
+ if (allIssues.length > 10)
116
+ report += `\n... and ${allIssues.length - 10} more files with issues\n`;
117
+ }
118
+ else {
119
+ report += "No accessibility issues found!\n";
120
+ }
121
+ const totalIssues = allIssues.reduce((sum, item) => sum + item.issues.length, 0);
122
+ const passed = totalIssues < 5;
123
+ return { passed, report };
124
+ }