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.
- package/ARCHITECTURE.md +2 -3
- package/README.md +1 -0
- package/build/index.js +75 -0
- package/build/tools/autoPreview.js +99 -0
- package/build/tools/checklist.js +120 -0
- package/build/tools/sessionManager.js +107 -0
- package/build/tools/validators/__tests__/apiSchema.test.js +77 -0
- package/build/tools/validators/__tests__/convertRules.test.js +38 -0
- package/build/tools/validators/__tests__/frontendDesign.test.js +55 -0
- package/build/tools/validators/__tests__/geoChecker.test.js +45 -0
- package/build/tools/validators/__tests__/i18nChecker.test.js +32 -0
- package/build/tools/validators/__tests__/lintRunner.test.js +65 -0
- package/build/tools/validators/__tests__/mobileAudit.test.js +40 -0
- package/build/tools/validators/__tests__/playwrightRunner.test.js +55 -0
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +49 -0
- package/build/tools/validators/__tests__/securityScan.test.js +42 -0
- package/build/tools/validators/__tests__/seoChecker.test.js +44 -0
- package/build/tools/validators/__tests__/testRunner.test.js +49 -0
- package/build/tools/validators/__tests__/typeCoverage.test.js +62 -0
- package/build/tools/validators/accessibilityChecker.js +124 -0
- package/build/tools/validators/apiValidator.js +140 -0
- package/build/tools/validators/convertRules.js +170 -0
- package/build/tools/validators/geoChecker.js +176 -0
- package/build/tools/validators/i18nChecker.js +205 -0
- package/build/tools/validators/lighthouseAudit.js +50 -0
- package/build/tools/validators/lintRunner.js +106 -0
- package/build/tools/validators/mobileAudit.js +190 -0
- package/build/tools/validators/playwrightRunner.js +101 -0
- package/build/tools/validators/reactPerformanceChecker.js +199 -0
- package/build/tools/validators/schemaValidator.js +105 -0
- package/build/tools/validators/securityScan.js +215 -0
- package/build/tools/validators/seoChecker.js +122 -0
- package/build/tools/validators/testRunner.js +111 -0
- package/build/tools/validators/typeCoverage.js +150 -0
- package/build/tools/validators/uxAudit.js +222 -0
- package/build/tools/verifyAll.js +159 -0
- package/package.json +5 -3
- package/skills/tech/api-patterns/SKILL.md +1 -1
- package/skills/tech/clean-code/SKILL.md +14 -14
- package/skills/tech/doc.md +3 -3
- package/skills/tech/frontend-design/SKILL.md +1 -1
- package/skills/tech/geo-fundamentals/SKILL.md +1 -1
- package/skills/tech/i18n-localization/SKILL.md +1 -1
- package/skills/tech/lint-and-validate/SKILL.md +2 -2
- package/skills/tech/mobile-design/SKILL.md +1 -1
- package/skills/tech/nextjs-react-expert/SKILL.md +1 -1
- package/skills/tech/parallel-agents/SKILL.md +3 -3
- package/skills/tech/performance-profiling/SKILL.md +1 -1
- package/skills/tech/vulnerability-scanner/SKILL.md +1 -1
- package/skills/tech/webapp-testing/SKILL.md +3 -3
- package/workflows/review-compound.md +1 -1
- package/skills/tech/api-patterns/scripts/api_validator.py +0 -211
- package/skills/tech/database-design/scripts/schema_validator.py +0 -172
- package/skills/tech/frontend-design/scripts/accessibility_checker.py +0 -183
- package/skills/tech/frontend-design/scripts/ux_audit.py +0 -722
- package/skills/tech/geo-fundamentals/scripts/geo_checker.py +0 -289
- package/skills/tech/i18n-localization/scripts/i18n_checker.py +0 -241
- package/skills/tech/lint-and-validate/scripts/lint_runner.py +0 -184
- package/skills/tech/lint-and-validate/scripts/type_coverage.py +0 -173
- package/skills/tech/mobile-design/scripts/mobile_audit.py +0 -670
- package/skills/tech/nextjs-react-expert/scripts/convert_rules.py +0 -222
- package/skills/tech/nextjs-react-expert/scripts/react_performance_checker.py +0 -252
- package/skills/tech/performance-profiling/scripts/lighthouse_audit.py +0 -76
- package/skills/tech/seo-fundamentals/scripts/seo_checker.py +0 -219
- package/skills/tech/testing-patterns/scripts/test_runner.py +0 -219
- package/skills/tech/vulnerability-scanner/scripts/security_scan.py +0 -458
- package/skills/tech/webapp-testing/scripts/playwright_runner.py +0 -173
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export async function detectTestFramework(projectPath) {
|
|
7
|
+
const result = {
|
|
8
|
+
type: 'unknown',
|
|
9
|
+
framework: null,
|
|
10
|
+
cmd: null,
|
|
11
|
+
coverageCmd: null
|
|
12
|
+
};
|
|
13
|
+
try {
|
|
14
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
15
|
+
const pkgData = await fs.readFile(pkgPath, 'utf-8');
|
|
16
|
+
const pkg = JSON.parse(pkgData);
|
|
17
|
+
result.type = 'node';
|
|
18
|
+
const scripts = pkg.scripts || {};
|
|
19
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
20
|
+
if (scripts.test) {
|
|
21
|
+
result.framework = 'npm test';
|
|
22
|
+
result.cmd = ['npm', 'test'];
|
|
23
|
+
if (deps.vitest) {
|
|
24
|
+
result.framework = 'vitest';
|
|
25
|
+
result.coverageCmd = ['npx', 'vitest', 'run', '--coverage'];
|
|
26
|
+
}
|
|
27
|
+
else if (deps.jest) {
|
|
28
|
+
result.framework = 'jest';
|
|
29
|
+
result.coverageCmd = ['npx', 'jest', '--coverage'];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (deps.vitest) {
|
|
33
|
+
result.framework = 'vitest';
|
|
34
|
+
result.cmd = ['npx', 'vitest', 'run'];
|
|
35
|
+
result.coverageCmd = ['npx', 'vitest', 'run', '--coverage'];
|
|
36
|
+
}
|
|
37
|
+
else if (deps.jest) {
|
|
38
|
+
result.framework = 'jest';
|
|
39
|
+
result.cmd = ['npx', 'jest'];
|
|
40
|
+
result.coverageCmd = ['npx', 'jest', '--coverage'];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
// Check python
|
|
45
|
+
try {
|
|
46
|
+
const pyproject = await fs.stat(path.join(projectPath, 'pyproject.toml')).catch(() => null);
|
|
47
|
+
const reqs = await fs.stat(path.join(projectPath, 'requirements.txt')).catch(() => null);
|
|
48
|
+
if (pyproject || reqs) {
|
|
49
|
+
result.type = 'python';
|
|
50
|
+
result.framework = 'pytest';
|
|
51
|
+
result.cmd = ['python', '-m', 'pytest', '-v'];
|
|
52
|
+
result.coverageCmd = ['python', '-m', 'pytest', '--cov', '--cov-report=term-missing'];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
export async function runTestRunner(projectPath = ".", withCoverage = false) {
|
|
59
|
+
const root = path.resolve(projectPath);
|
|
60
|
+
let report = `============================================================\n`;
|
|
61
|
+
report += `[TEST RUNNER] Unified Test Execution\n`;
|
|
62
|
+
report += `============================================================\n`;
|
|
63
|
+
const info = await detectTestFramework(root);
|
|
64
|
+
report += `Type: ${info.type}\nFramework: ${info.framework}\n------------------------------------------------------------\n`;
|
|
65
|
+
if (!info.cmd) {
|
|
66
|
+
report += `No test framework found for this project.\n`;
|
|
67
|
+
return { passed: true, report };
|
|
68
|
+
}
|
|
69
|
+
const cmdArr = (withCoverage && info.coverageCmd) ? info.coverageCmd : info.cmd;
|
|
70
|
+
const cmdStr = cmdArr.join(' ');
|
|
71
|
+
report += `Running: ${cmdStr}\n------------------------------------------------------------\n`;
|
|
72
|
+
let passed = false;
|
|
73
|
+
let testsRun = 0, testsPassed = 0, testsFailed = 0;
|
|
74
|
+
let output = '';
|
|
75
|
+
try {
|
|
76
|
+
// Child process maxbuffer 5MB since test output can be huge
|
|
77
|
+
const result = await execAsync(cmdStr, { cwd: root, maxBuffer: 5 * 1024 * 1024 });
|
|
78
|
+
passed = true;
|
|
79
|
+
output = result.stdout;
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
passed = false;
|
|
83
|
+
output = e.stdout || e.stderr || e.message;
|
|
84
|
+
}
|
|
85
|
+
// Parse stats
|
|
86
|
+
const passedMatch = output.match(/(\d+)\s+passed/i);
|
|
87
|
+
if (passedMatch)
|
|
88
|
+
testsPassed = parseInt(passedMatch[1]);
|
|
89
|
+
const failedMatch = output.match(/(\d+)\s+failed/i);
|
|
90
|
+
if (failedMatch)
|
|
91
|
+
testsFailed = parseInt(failedMatch[1]);
|
|
92
|
+
testsRun = testsPassed + testsFailed;
|
|
93
|
+
const lines = output.split('\n');
|
|
94
|
+
for (const line of lines.slice(0, 30)) {
|
|
95
|
+
report += `${line}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (lines.length > 30) {
|
|
98
|
+
report += `... (${lines.length - 30} more lines)\n`;
|
|
99
|
+
}
|
|
100
|
+
report += `\n============================================================\nSUMMARY\n============================================================\n`;
|
|
101
|
+
if (passed) {
|
|
102
|
+
report += `[PASS] All tests passed\n`;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
report += `[FAIL] Some tests failed\n`;
|
|
106
|
+
}
|
|
107
|
+
if (testsRun > 0) {
|
|
108
|
+
report += `Tests: ${testsRun} total, ${testsPassed} passed, ${testsFailed} failed\n`;
|
|
109
|
+
}
|
|
110
|
+
return { passed, report };
|
|
111
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
async function getFiles(dir, extensionRegex, excludeList) {
|
|
4
|
+
let files = [];
|
|
5
|
+
try {
|
|
6
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
const fullPath = path.join(dir, item.name);
|
|
9
|
+
if (excludeList.some(ex => fullPath.includes(ex)))
|
|
10
|
+
continue;
|
|
11
|
+
if (item.isDirectory()) {
|
|
12
|
+
files = files.concat(await getFiles(fullPath, extensionRegex, excludeList));
|
|
13
|
+
}
|
|
14
|
+
else if (extensionRegex.test(item.name)) {
|
|
15
|
+
files.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { } // Ignore read errors
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
export async function checkTypescriptCoverage(projectPath) {
|
|
23
|
+
const passed = [];
|
|
24
|
+
const issues = [];
|
|
25
|
+
const stats = { any_count: 0, untyped_functions: 0, total_functions: 0 };
|
|
26
|
+
const exclude = ['node_modules', '.d.ts', 'dist', 'build', '.next'];
|
|
27
|
+
const tsFiles = await getFiles(projectPath, /\.(ts|tsx)$/, exclude);
|
|
28
|
+
if (tsFiles.length === 0) {
|
|
29
|
+
return { type: 'typescript', files: 0, passed, issues: ["[!] No TypeScript files found"], stats };
|
|
30
|
+
}
|
|
31
|
+
// Check first 30 files for performance
|
|
32
|
+
for (const filePath of tsFiles.slice(0, 30)) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
35
|
+
// any count
|
|
36
|
+
const anyMatches = content.match(/:\s*any\b/g);
|
|
37
|
+
if (anyMatches)
|
|
38
|
+
stats.any_count += anyMatches.length;
|
|
39
|
+
// untyped functions
|
|
40
|
+
const untypedFuncs = content.match(/function\s+\w+\s*\([^)]*\)\s*{/g);
|
|
41
|
+
if (untypedFuncs)
|
|
42
|
+
stats.untyped_functions += untypedFuncs.length;
|
|
43
|
+
const untypedArrows = content.match(/=\s*\([^:)]*\)\s*=>/g);
|
|
44
|
+
if (untypedArrows)
|
|
45
|
+
stats.untyped_functions += untypedArrows.length;
|
|
46
|
+
// typed functions
|
|
47
|
+
let typedCount = 0;
|
|
48
|
+
const typedFuncs = content.match(/function\s+\w+\s*\([^)]*\)\s*:\s*\w+/g);
|
|
49
|
+
if (typedFuncs)
|
|
50
|
+
typedCount += typedFuncs.length;
|
|
51
|
+
const typedArrows = content.match(/:\s*\([^)]*\)\s*=>\s*\w+/g);
|
|
52
|
+
if (typedArrows)
|
|
53
|
+
typedCount += typedArrows.length;
|
|
54
|
+
stats.total_functions += (typedCount + (untypedFuncs?.length || 0) + (untypedArrows?.length || 0));
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
if (stats.any_count === 0)
|
|
59
|
+
passed.push("[OK] No 'any' types found");
|
|
60
|
+
else if (stats.any_count <= 5)
|
|
61
|
+
issues.push(`[!] ${stats.any_count} 'any' types found (acceptable)`);
|
|
62
|
+
else
|
|
63
|
+
issues.push(`[X] ${stats.any_count} 'any' types found (too many)`);
|
|
64
|
+
if (stats.total_functions > 0) {
|
|
65
|
+
const typedRatio = ((stats.total_functions - stats.untyped_functions) / stats.total_functions) * 100;
|
|
66
|
+
if (typedRatio >= 80)
|
|
67
|
+
passed.push(`[OK] Type coverage: ${typedRatio.toFixed(0)}%`);
|
|
68
|
+
else if (typedRatio >= 50)
|
|
69
|
+
issues.push(`[!] Type coverage: ${typedRatio.toFixed(0)}% (improve)`);
|
|
70
|
+
else
|
|
71
|
+
issues.push(`[X] Type coverage: ${typedRatio.toFixed(0)}% (too low)`);
|
|
72
|
+
}
|
|
73
|
+
passed.push(`[OK] Analyzed ${tsFiles.length} TypeScript files`);
|
|
74
|
+
return { type: 'typescript', files: tsFiles.length, passed, issues, stats };
|
|
75
|
+
}
|
|
76
|
+
export async function checkPythonCoverage(projectPath) {
|
|
77
|
+
const passed = [];
|
|
78
|
+
const issues = [];
|
|
79
|
+
const stats = { any_count: 0, untyped_functions: 0, total_functions: 0, typed_functions: 0 };
|
|
80
|
+
const exclude = ['venv', '__pycache__', '.git', 'node_modules'];
|
|
81
|
+
const pyFiles = await getFiles(projectPath, /\.py$/, exclude);
|
|
82
|
+
if (pyFiles.length === 0) {
|
|
83
|
+
return { type: 'python', files: 0, passed, issues: ["[!] No Python files found"], stats };
|
|
84
|
+
}
|
|
85
|
+
for (const filePath of pyFiles.slice(0, 30)) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
88
|
+
const anyMatches = content.match(/:\s*Any\b/g);
|
|
89
|
+
if (anyMatches)
|
|
90
|
+
stats.any_count += anyMatches.length;
|
|
91
|
+
const allFuncs = content.match(/def\s+\w+\s*\([^)]*\)(?:\s*->[^:]+)?\s*:/g);
|
|
92
|
+
if (allFuncs) {
|
|
93
|
+
let localTyped = 0;
|
|
94
|
+
for (const f of allFuncs) {
|
|
95
|
+
const argsPart = f.substring(f.indexOf('('), f.lastIndexOf(')'));
|
|
96
|
+
const hasReturn = f.includes('->');
|
|
97
|
+
const hasTypedArgs = argsPart.includes(':');
|
|
98
|
+
if (hasTypedArgs || hasReturn) {
|
|
99
|
+
localTyped++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
stats.typed_functions += localTyped;
|
|
103
|
+
stats.untyped_functions += allFuncs.length - localTyped;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
}
|
|
108
|
+
stats.total_functions = stats.typed_functions + stats.untyped_functions;
|
|
109
|
+
if (stats.total_functions > 0) {
|
|
110
|
+
const typedRatio = (stats.typed_functions / stats.total_functions) * 100;
|
|
111
|
+
if (typedRatio >= 70)
|
|
112
|
+
passed.push(`[OK] Type hints coverage: ${typedRatio.toFixed(0)}%`);
|
|
113
|
+
else if (typedRatio >= 40)
|
|
114
|
+
issues.push(`[!] Type hints coverage: ${typedRatio.toFixed(0)}%`);
|
|
115
|
+
else
|
|
116
|
+
issues.push(`[X] Type hints coverage: ${typedRatio.toFixed(0)}% (add type hints)`);
|
|
117
|
+
}
|
|
118
|
+
if (stats.any_count === 0)
|
|
119
|
+
passed.push("[OK] No 'Any' types found");
|
|
120
|
+
else if (stats.any_count <= 3)
|
|
121
|
+
issues.push(`[!] ${stats.any_count} 'Any' types found`);
|
|
122
|
+
else
|
|
123
|
+
issues.push(`[X] ${stats.any_count} 'Any' types found`);
|
|
124
|
+
passed.push(`[OK] Analyzed ${pyFiles.length} Python files`);
|
|
125
|
+
return { type: 'python', files: pyFiles.length, passed, issues, stats };
|
|
126
|
+
}
|
|
127
|
+
export async function runTypeCoverage(projectPath = ".") {
|
|
128
|
+
const tsRes = await checkTypescriptCoverage(projectPath);
|
|
129
|
+
const pyRes = await checkPythonCoverage(projectPath);
|
|
130
|
+
const results = [tsRes, pyRes].filter(r => r.files > 0);
|
|
131
|
+
if (results.length === 0) {
|
|
132
|
+
return { passed: true, report: "[!] No TypeScript or Python files found." };
|
|
133
|
+
}
|
|
134
|
+
let report = "";
|
|
135
|
+
let criticalIssues = 0;
|
|
136
|
+
for (const res of results) {
|
|
137
|
+
report += `\n[${res.type.toUpperCase()}]\n----------------------------------------\n`;
|
|
138
|
+
for (const p of res.passed)
|
|
139
|
+
report += ` ${p}\n`;
|
|
140
|
+
for (const i of res.issues) {
|
|
141
|
+
report += ` ${i}\n`;
|
|
142
|
+
if (i.startsWith("[X]"))
|
|
143
|
+
criticalIssues++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
report += `\n============================================================\n`;
|
|
147
|
+
const passed = criticalIssues === 0;
|
|
148
|
+
report += passed ? `[OK] TYPE COVERAGE: ACCEPTABLE` : `[X] TYPE COVERAGE: ${criticalIssues} critical issues`;
|
|
149
|
+
return { passed, report };
|
|
150
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
class UXAuditor {
|
|
4
|
+
issues = [];
|
|
5
|
+
warnings = [];
|
|
6
|
+
passed_count = 0;
|
|
7
|
+
files_checked = 0;
|
|
8
|
+
async auditFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
11
|
+
this.files_checked++;
|
|
12
|
+
const filename = path.basename(filePath);
|
|
13
|
+
const lowerC = content.toLowerCase();
|
|
14
|
+
const hasLongText = /<p|<div.*class=.*text|article|<span.*text/i.test(content);
|
|
15
|
+
const hasForm = /<form|<input|password|credit|card|payment/i.test(content);
|
|
16
|
+
const complexElements = (content.match(/<input|<select|<textarea|<option/gi) || []).length;
|
|
17
|
+
const navItems = (content.match(/<NavLink|<Link|<a\s+href|nav-item/gi) || []).length;
|
|
18
|
+
if (navItems > 7)
|
|
19
|
+
this.issues.push(`[Hick's Law] ${filename}: ${navItems} nav items (Max 7)`);
|
|
20
|
+
if (/height:\s*([0-3]\d)px/.test(content) || /h-[1-9]\b|h-10\b/.test(content))
|
|
21
|
+
this.warnings.push(`[Fitts' Law] ${filename}: Small targets (< 44px)`);
|
|
22
|
+
if (complexElements > 7 && !/step|wizard|stage/i.test(content))
|
|
23
|
+
this.warnings.push(`[Miller's Law] ${filename}: Complex form (${complexElements} fields)`);
|
|
24
|
+
if (lowerC.includes('button') && !/primary|bg-primary|Button.*primary|variant=["']primary/i.test(content))
|
|
25
|
+
this.warnings.push(`[Von Restorff] ${filename}: No primary CTA`);
|
|
26
|
+
if (navItems > 3) {
|
|
27
|
+
const navContent = [...content.matchAll(/<NavLink|<Link|<a\s+href[^>]*>([^<]+)<\/a>/gi)];
|
|
28
|
+
if (navContent.length > 2) {
|
|
29
|
+
const last = navContent[navContent.length - 1][1].toLowerCase();
|
|
30
|
+
if (!['contact', 'login', 'sign', 'get started', 'cta', 'button'].some(x => last.includes(x))) {
|
|
31
|
+
this.warnings.push(`[Serial Position] ${filename}: Last nav item may not be important.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const hasHero = /hero|<h1|banner/i.test(content);
|
|
36
|
+
if (hasHero) {
|
|
37
|
+
const hasVis = /gradient|linear-gradient|radial-gradient|@keyframes|transition:|animate-/.test(content);
|
|
38
|
+
if (!hasVis && !/background:|bg-/.test(content)) {
|
|
39
|
+
this.warnings.push(`[Visceral] ${filename}: Hero section lacks visual appeal.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (/onClick|@click|onclick/i.test(content)) {
|
|
43
|
+
if (!/transition|animate|hover:|focus:|disabled|loading|spinner|setState|useState/i.test(content)) {
|
|
44
|
+
this.warnings.push(`[Behavioral] ${filename}: Interactive elements lack immediate feedback.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (hasLongText && !/about|story|mission|values|why we|our journey|testimonials/i.test(content)) {
|
|
48
|
+
this.warnings.push(`[Reflective] ${filename}: Long-form content without brand story/values.`);
|
|
49
|
+
}
|
|
50
|
+
if (hasForm) {
|
|
51
|
+
if (!/ssl|secure|encrypt|lock|padlock|https/i.test(content) && !/checkout|payment/i.test(content)) {
|
|
52
|
+
this.warnings.push(`[Trust] ${filename}: Form without security indicators.`);
|
|
53
|
+
}
|
|
54
|
+
if (!/<label|placeholder|aria-label/i.test(content)) {
|
|
55
|
+
this.issues.push(`[Cognitive Load] ${filename}: Form inputs without labels.`);
|
|
56
|
+
}
|
|
57
|
+
const radioInputs = (content.match(/type=["']radio/gi) || []).length;
|
|
58
|
+
if (radioInputs > 0 && !/checked|selected|default|value=["'].*["']/i.test(content)) {
|
|
59
|
+
this.warnings.push(`[Persuasion] ${filename}: Radio buttons without default selection.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const socialProof = content.match(/review|testimonial|rating|star|trust|trusted by|customer|logo/gi) || [];
|
|
63
|
+
if (socialProof.length > 0)
|
|
64
|
+
this.passed_count++;
|
|
65
|
+
else if (hasLongText)
|
|
66
|
+
this.warnings.push(`[Trust] ${filename}: No social proof detected.`);
|
|
67
|
+
if (/footer|<footer/i.test(content) && !/certif|award|media|press|featured|as seen in/i.test(content)) {
|
|
68
|
+
this.warnings.push(`[Trust] ${filename}: Footer lacks authority signals.`);
|
|
69
|
+
}
|
|
70
|
+
if (complexElements > 5 && !/step|wizard|stage|accordion|collapsible|tab|more\.\.\.|advanced|show more/i.test(content)) {
|
|
71
|
+
this.warnings.push(`[Cognitive Load] ${filename}: Many form elements without progressive disclosure.`);
|
|
72
|
+
}
|
|
73
|
+
const manyColors = (content.match(/#[0-9a-fA-F]{3,6}|rgb|hsl/g) || []).length > 15;
|
|
74
|
+
const manyBorders = (content.match(/border:|border-/g) || []).length > 10;
|
|
75
|
+
if (manyColors && manyBorders)
|
|
76
|
+
this.warnings.push(`[Cognitive Load] ${filename}: High visual noise detected.`);
|
|
77
|
+
if (/price|pricing|cost|\$\d+/i.test(content) && !/original|was|strike|del|save \d+%/i.test(content)) {
|
|
78
|
+
this.warnings.push(`[Persuasion] ${filename}: Prices without anchoring.`);
|
|
79
|
+
}
|
|
80
|
+
if (/join|subscriber|member|user/i.test(content) && !/\d+[+kmb]|\d+,\d+/.test(content)) {
|
|
81
|
+
this.warnings.push(`[Persuasion] ${filename}: Social proof without specific numbers.`);
|
|
82
|
+
}
|
|
83
|
+
if (hasForm && complexElements > 5 && !/progress|step \d+|complete|%|bar/i.test(content)) {
|
|
84
|
+
this.warnings.push(`[Persuasion] ${filename}: Long form without progress indicator.`);
|
|
85
|
+
}
|
|
86
|
+
// Typography
|
|
87
|
+
const googleFonts = [...content.matchAll(/fonts\.googleapis\.com[^"']*family=([^"&]+)/gi)];
|
|
88
|
+
if (googleFonts.length > 3)
|
|
89
|
+
this.issues.push(`[Typography] ${filename}: >3 font families detected.`);
|
|
90
|
+
if (hasLongText && !/max-w-(?:prose|[\[\\]?\d+ch[\]\\]?)|max-width:\s*\d+ch/.test(content)) {
|
|
91
|
+
this.warnings.push(`[Typography] ${filename}: No line length constraint (45-75ch).`);
|
|
92
|
+
}
|
|
93
|
+
if (/<h[1-6]|text-(?:xl|2xl|3xl|4xl|5xl|6xl)/i.test(content)) {
|
|
94
|
+
const lhMatches = [...content.matchAll(/(?:leading-|line-height:\s*)([\d.]+)/g)];
|
|
95
|
+
for (const m of lhMatches) {
|
|
96
|
+
if (parseFloat(m[1]) > 1.5)
|
|
97
|
+
this.warnings.push(`[Typography] ${filename}: Heading has line-height > 1.3.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (/uppercase|text-transform:\s*uppercase/i.test(content) && !/tracking-|letter-spacing:/.test(content)) {
|
|
101
|
+
this.warnings.push(`[Typography] ${filename}: Uppercase text without tracking.`);
|
|
102
|
+
}
|
|
103
|
+
if (/text-(?:4xl|5xl|6xl|7xl|8xl|9xl)|font-size:\s*[3-9]\dpx/.test(content) && !/tracking-tight|letter-spacing:\s*-[0-9]/.test(content)) {
|
|
104
|
+
this.warnings.push(`[Typography] ${filename}: Large display text without tracking-tight.`);
|
|
105
|
+
}
|
|
106
|
+
if (/font-size:|text-(?:xs|sm|base|lg|xl|2xl)/.test(content) && !/clamp\(|responsive:/.test(content)) {
|
|
107
|
+
this.warnings.push(`[Typography] ${filename}: Fixed font sizes without clamp().`);
|
|
108
|
+
}
|
|
109
|
+
const headings = [...content.matchAll(/<h([1-6])/gi)];
|
|
110
|
+
if (headings.length > 0) {
|
|
111
|
+
for (let i = 0; i < headings.length - 1; i++) {
|
|
112
|
+
if (parseInt(headings[i + 1][1]) > parseInt(headings[i][1]) + 1) {
|
|
113
|
+
this.warnings.push(`[Typography] ${filename}: Skipped heading level.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!headings.some(h => h[1] === '1') && hasLongText) {
|
|
117
|
+
this.warnings.push(`[Typography] ${filename}: No h1 found.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Visual Effects
|
|
121
|
+
if (/backdrop-filter|blur\(/.test(content) && !/background:\s*rgba|bg-opacity|bg-[a-z0-9]+\/\d+/.test(content)) {
|
|
122
|
+
this.warnings.push(`[Visual] ${filename}: Blur used without semi-transparent background`);
|
|
123
|
+
}
|
|
124
|
+
if (/@keyframes|transition:/.test(content) && /width|height|top|left|right|bottom|margin|padding/.test(content)) {
|
|
125
|
+
this.warnings.push(`[Performance] ${filename}: Animating expensive properties. Use transform/opacity.`);
|
|
126
|
+
if (!/prefers-reduced-motion/.test(content))
|
|
127
|
+
this.warnings.push(`[Accessibility] ${filename}: Animations found without prefers-reduced-motion check`);
|
|
128
|
+
}
|
|
129
|
+
const neoShadows = [...content.matchAll(/box-shadow:\s*([^;]+)/g)];
|
|
130
|
+
for (const s of neoShadows) {
|
|
131
|
+
if (s[1].includes(',') && s[1].includes('-') && s[1].includes('inset')) {
|
|
132
|
+
this.warnings.push(`[Visual] ${filename}: Neomorphism inset detected. Ensure adequate contrast.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const gradients = (content.match(/gradient/gi) || []).length;
|
|
136
|
+
if (gradients > 5)
|
|
137
|
+
this.warnings.push(`[Visual] ${filename}: Many gradients detected (${gradients}).`);
|
|
138
|
+
else if (hasHero && !/background:|bg-/.test(content))
|
|
139
|
+
this.warnings.push(`[Visual] ${filename}: Hero section without visual interest.`);
|
|
140
|
+
if ((content.match(/border:/g) || []).length > 8)
|
|
141
|
+
this.warnings.push(`[Visual] ${filename}: Many border declarations`);
|
|
142
|
+
// colors
|
|
143
|
+
const purples = ['#8B5CF6', '#A855F7', '#9333EA', '#7C3AED', '#6D28D9', 'purple', 'violet', 'fuchsia', 'magenta', 'lavender'];
|
|
144
|
+
for (const p of purples) {
|
|
145
|
+
if (lowerC.includes(p.toLowerCase())) {
|
|
146
|
+
this.issues.push(`[Color] ${filename}: PURPLE DETECTED ('${p}'). Banned by Maestro rules.`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (/color:\s*#000000|#000\b/.test(content))
|
|
151
|
+
this.warnings.push(`[Color] ${filename}: Pure black (#000000) detected.`);
|
|
152
|
+
// animation & UX
|
|
153
|
+
const durations = [...content.matchAll(/(?:duration|animation-duration|transition-duration):\s*([\d.]+)(s|ms)/g)];
|
|
154
|
+
for (const d of durations) {
|
|
155
|
+
const ms = parseFloat(d[1]) * (d[2] === 's' ? 1000 : 1);
|
|
156
|
+
if (ms < 50)
|
|
157
|
+
this.warnings.push(`[Animation] ${filename}: Very fast animation (${d[1]}${d[2]}).`);
|
|
158
|
+
else if (ms > 1000 && lowerC.includes('transition'))
|
|
159
|
+
this.warnings.push(`[Animation] ${filename}: Long transition.`);
|
|
160
|
+
}
|
|
161
|
+
if (/ease-in\s+.*entry|fade-in.*ease-in/.test(content))
|
|
162
|
+
this.warnings.push(`[Animation] ${filename}: Entry animation with ease-in.`);
|
|
163
|
+
if (/ease-out\s+.*exit|fade-out.*ease-out/.test(content))
|
|
164
|
+
this.warnings.push(`[Animation] ${filename}: Exit animation with ease-out.`);
|
|
165
|
+
const interactiveCount = (content.match(/<button|<a\s+href|onClick|@click/g) || []).length;
|
|
166
|
+
if (interactiveCount > 2 && !/hover:|focus:|:hover|:focus/.test(content)) {
|
|
167
|
+
this.warnings.push(`[Animation] ${filename}: Interactive elements without hover/focus states.`);
|
|
168
|
+
}
|
|
169
|
+
// accessibility basics
|
|
170
|
+
if (/<img(?![^>]*alt=)[^>]*>/.test(content))
|
|
171
|
+
this.issues.push(`[Accessibility] ${filename}: Missing img alt text`);
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
}
|
|
175
|
+
async auditDirectory(dir) {
|
|
176
|
+
try {
|
|
177
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
if (['node_modules', '.git', 'dist', 'build', '.next'].includes(item.name))
|
|
180
|
+
continue;
|
|
181
|
+
const fullPath = path.join(dir, item.name);
|
|
182
|
+
if (item.isDirectory()) {
|
|
183
|
+
await this.auditDirectory(fullPath);
|
|
184
|
+
}
|
|
185
|
+
else if (/\.(tsx|jsx|html|vue|svelte|css)$/.test(item.name)) {
|
|
186
|
+
await this.auditFile(fullPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
getReport() {
|
|
193
|
+
return {
|
|
194
|
+
files_checked: this.files_checked,
|
|
195
|
+
issues: this.issues,
|
|
196
|
+
warnings: this.warnings,
|
|
197
|
+
passed_checks: this.passed_count,
|
|
198
|
+
compliant: this.issues.length === 0
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function runUxAudit(projectPath = ".") {
|
|
203
|
+
const root = path.resolve(projectPath);
|
|
204
|
+
const auditor = new UXAuditor();
|
|
205
|
+
await auditor.auditDirectory(root);
|
|
206
|
+
const res = auditor.getReport();
|
|
207
|
+
let report = `\n[UX AUDIT] ${res.files_checked} files checked\n`;
|
|
208
|
+
report += `--------------------------------------------------\n`;
|
|
209
|
+
if (res.issues.length > 0) {
|
|
210
|
+
report += `[!] ISSUES (${res.issues.length}):\n`;
|
|
211
|
+
for (const i of res.issues.slice(0, 10))
|
|
212
|
+
report += ` - ${i}\n`;
|
|
213
|
+
}
|
|
214
|
+
if (res.warnings.length > 0) {
|
|
215
|
+
report += `[*] WARNINGS (${res.warnings.length}):\n`;
|
|
216
|
+
for (const w of res.warnings.slice(0, 15))
|
|
217
|
+
report += ` - ${w}\n`;
|
|
218
|
+
}
|
|
219
|
+
report += `[+] PASSED CHECKS: ${res.passed_checks}\n`;
|
|
220
|
+
report += `STATUS: ${res.compliant ? 'PASS' : 'FAIL'}\n`;
|
|
221
|
+
return { passed: res.compliant, report };
|
|
222
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import { runSecurityScan } from './validators/securityScan.js';
|
|
6
|
+
import { runLintRunner } from './validators/lintRunner.js';
|
|
7
|
+
import { runTypeCoverage } from './validators/typeCoverage.js';
|
|
8
|
+
import { runTestRunner } from './validators/testRunner.js';
|
|
9
|
+
import { runPlaywrightTest } from './validators/playwrightRunner.js';
|
|
10
|
+
async function hasScript(rootPath, scriptName) {
|
|
11
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
12
|
+
if (!existsSync(pkgPath))
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
16
|
+
return !!(pkg.scripts && pkg.scripts[scriptName]);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function runCommand(name, command, args, projectPath) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const child = spawn(command, args, { cwd: projectPath, shell: true });
|
|
26
|
+
let out = '';
|
|
27
|
+
child.stdout.on('data', data => out += data.toString());
|
|
28
|
+
child.stderr.on('data', data => out += data.toString());
|
|
29
|
+
child.on('close', code => {
|
|
30
|
+
const passed = code === 0;
|
|
31
|
+
const duration = (Date.now() - start) / 1000;
|
|
32
|
+
const report = passed ? `✅ ${name} (${duration}s): PASSED\n${out}` : `❌ ${name} (${duration}s): FAILED\n${out}`;
|
|
33
|
+
resolve({ passed, report, duration });
|
|
34
|
+
});
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
child.kill();
|
|
37
|
+
resolve({ passed: false, report: `❌ ${name}: TIMED OUT`, duration: (Date.now() - start) / 1000 });
|
|
38
|
+
}, 10 * 60 * 1000);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function runVerifyAll(projectPath, url, skipE2E = false, stopOnFail = false) {
|
|
42
|
+
const root = path.resolve(projectPath);
|
|
43
|
+
let report = `🚀 FULL VERIFICATION SUITE\nProject: ${root}\nURL: ${url}\n\n`;
|
|
44
|
+
let passedCount = 0;
|
|
45
|
+
let failedCount = 0;
|
|
46
|
+
let skippedCount = 0;
|
|
47
|
+
const VERIFICATION_SUITE = [
|
|
48
|
+
{
|
|
49
|
+
category: "Security",
|
|
50
|
+
checks: [
|
|
51
|
+
{
|
|
52
|
+
name: "Security Scan", required: true, fn: async () => {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const res = await runSecurityScan(root);
|
|
55
|
+
return { passed: res.passed, report: res.report, duration: (Date.now() - start) / 1000 };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
category: "Code Quality",
|
|
62
|
+
checks: [
|
|
63
|
+
{
|
|
64
|
+
name: "Lint Check", required: true, fn: async () => {
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
const res = await runLintRunner(root);
|
|
67
|
+
return { passed: res.passed, report: res.report, duration: (Date.now() - start) / 1000 };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "Type Coverage", required: false, fn: async () => {
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
const res = await runTypeCoverage(root);
|
|
74
|
+
return { passed: res.passed, report: res.report, duration: (Date.now() - start) / 1000 };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
category: "Testing",
|
|
81
|
+
checks: [
|
|
82
|
+
{
|
|
83
|
+
name: "Test Suite", required: false, fn: async () => {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
const res = await runTestRunner(root, false);
|
|
86
|
+
return { passed: res.passed, report: res.report, duration: (Date.now() - start) / 1000 };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
category: "Build",
|
|
93
|
+
checks: [
|
|
94
|
+
{
|
|
95
|
+
name: "Build App", required: false, fn: async () => {
|
|
96
|
+
if (await hasScript(root, 'build'))
|
|
97
|
+
return runCommand("Build App", 'npm', ['run', 'build'], root);
|
|
98
|
+
return { passed: true, skipped: true, report: `Script 'build' not found in package.json` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
category: "E2E Testing",
|
|
105
|
+
requiresUrl: true,
|
|
106
|
+
checks: [
|
|
107
|
+
{
|
|
108
|
+
name: "Playwright E2E", required: false, fn: async () => {
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
const res = await runPlaywrightTest(url);
|
|
111
|
+
return {
|
|
112
|
+
passed: res.status === 'success',
|
|
113
|
+
report: JSON.stringify(res, null, 2),
|
|
114
|
+
duration: (Date.now() - start) / 1000
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
for (const suite of VERIFICATION_SUITE) {
|
|
122
|
+
if (suite.requiresUrl && !url)
|
|
123
|
+
continue;
|
|
124
|
+
if (skipE2E && suite.category === "E2E Testing")
|
|
125
|
+
continue;
|
|
126
|
+
report += `📋 ${suite.category.toUpperCase()}\n`;
|
|
127
|
+
for (const check of suite.checks) {
|
|
128
|
+
try {
|
|
129
|
+
const result = await check.fn();
|
|
130
|
+
if (result.skipped) {
|
|
131
|
+
skippedCount++;
|
|
132
|
+
report += `⏭️ ${check.name}: Skipped - ${result.report}\n`;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
report += `[${result.duration?.toFixed(2)}s] ${check.name}:\n${result.report}\n`;
|
|
136
|
+
if (result.passed) {
|
|
137
|
+
passedCount++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
failedCount++;
|
|
141
|
+
if (stopOnFail && check.required) {
|
|
142
|
+
report += `CRITICAL FAILURE on ${check.name}\n`;
|
|
143
|
+
return report + `\nTotal Passed: ${passedCount} | Failed: ${failedCount} | Skipped: ${skippedCount}\n\nFAILED❗`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
failedCount++;
|
|
149
|
+
report += `❌ Error executing check ${check.name}: ${e.message}\n`;
|
|
150
|
+
if (stopOnFail && check.required) {
|
|
151
|
+
return report + `\nTotal Passed: ${passedCount} | Failed: ${failedCount} | Skipped: ${skippedCount}\n\nFAILED❗`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
report += `\n📊 FINAL REPORT\nPassed: ${passedCount}\nFailed: ${failedCount}\nSkipped: ${skippedCount}\n\n`;
|
|
157
|
+
report += failedCount > 0 ? `❌ VERIFICATION FAILED!` : `✅ ALL CHECKS PASSED - Ready for deployment! ✨`;
|
|
158
|
+
return report;
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superkit-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An MCP server for exploring and loading Super-Kit AI agent resources.",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.4.1",
|
|
17
|
+
"playwright": "^1.58.2",
|
|
17
18
|
"zod": "^3.23.8"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^22.
|
|
21
|
-
"typescript": "^5.7.2"
|
|
21
|
+
"@types/node": "^22.19.13",
|
|
22
|
+
"typescript": "^5.7.2",
|
|
23
|
+
"vitest": "^4.0.18"
|
|
22
24
|
},
|
|
23
25
|
"files": [
|
|
24
26
|
"build/",
|
|
@@ -77,5 +77,5 @@ Before designing an API:
|
|
|
77
77
|
|
|
78
78
|
| Script | Purpose | Command |
|
|
79
79
|
|--------|---------|---------|
|
|
80
|
-
| `
|
|
80
|
+
| `api_validator` validator | API endpoint validation | `call_tool_checklist` |
|
|
81
81
|
|