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,205 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ const HARDCODED_PATTERNS = {
4
+ 'jsx': [
5
+ // Text directly in JSX: <div>Hello World</div>
6
+ '>\\s*[A-Z][a-zA-Z\\s]{3,30}\\s*</',
7
+ // JSX attribute strings: title="Welcome"
8
+ '(title|placeholder|label|alt|aria-label)="[A-Z][a-zA-Z\\s]{2,}"',
9
+ // Button/heading text
10
+ '<(button|h[1-6]|p|span|label)[^>]*>\\s*[A-Z][a-zA-Z\\s!?.,]{3,}\\s*</',
11
+ ],
12
+ 'vue': [
13
+ '>\\s*[A-Z][a-zA-Z\\s]{3,30}\\s*</',
14
+ '(placeholder|label|title)="[A-Z][a-zA-Z\\s]{2,}"',
15
+ ],
16
+ 'python': [
17
+ '(print|raise\\s+\\w+)\\s*\\(\\s*["\'][A-Z][^"\']{5,}["\']',
18
+ 'flash\\s*\\(\\s*["\'][A-Z][^"\']{5,}["\']',
19
+ ]
20
+ };
21
+ const I18N_PATTERNS = [
22
+ 't\\(["\']',
23
+ 'useTranslation',
24
+ '\\$t\\(',
25
+ '_\\(["\']',
26
+ 'gettext\\(',
27
+ 'useTranslations',
28
+ 'FormattedMessage',
29
+ 'i18n\\.'
30
+ ];
31
+ export async function findLocaleFiles(projectPath) {
32
+ let files = [];
33
+ async function search(dir) {
34
+ try {
35
+ const items = await fs.readdir(dir, { withFileTypes: true });
36
+ for (const item of items) {
37
+ if (item.name === 'node_modules' || item.name === '.git')
38
+ continue;
39
+ const fullPath = path.join(dir, item.name);
40
+ if (item.isDirectory())
41
+ await search(fullPath);
42
+ else {
43
+ const lpath = fullPath.toLowerCase();
44
+ if ((lpath.includes('locales') || lpath.includes('translations') || lpath.includes('lang') ||
45
+ lpath.includes('i18n') || lpath.includes('messages')) && lpath.endsWith('.json')) {
46
+ files.push(fullPath);
47
+ }
48
+ else if (lpath.endsWith('.po')) {
49
+ files.push(fullPath);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ catch { }
55
+ }
56
+ await search(projectPath);
57
+ return files;
58
+ }
59
+ function flattenKeys(d, prefix = '') {
60
+ let keys = new Set();
61
+ for (const [k, v] of Object.entries(d)) {
62
+ const newKey = prefix ? `${prefix}.${k}` : k;
63
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
64
+ for (const childKey of flattenKeys(v, newKey))
65
+ keys.add(childKey);
66
+ }
67
+ else {
68
+ keys.add(newKey);
69
+ }
70
+ }
71
+ return keys;
72
+ }
73
+ export async function checkLocaleCompleteness(localeFiles) {
74
+ const passed = [];
75
+ const issues = [];
76
+ if (localeFiles.length === 0)
77
+ return { passed, issues: ["[!] No locale files found"] };
78
+ const locales = {};
79
+ for (const file of localeFiles) {
80
+ if (file.endsWith('.json')) {
81
+ try {
82
+ const lang = path.basename(path.dirname(file));
83
+ const content = JSON.parse(await fs.readFile(file, 'utf-8'));
84
+ if (!locales[lang])
85
+ locales[lang] = {};
86
+ locales[lang][path.parse(file).name] = flattenKeys(content);
87
+ }
88
+ catch { }
89
+ }
90
+ }
91
+ const allLangs = Object.keys(locales);
92
+ if (allLangs.length < 2) {
93
+ passed.push(`[OK] Found ${localeFiles.length} locale file(s)`);
94
+ return { passed, issues };
95
+ }
96
+ passed.push(`[OK] Found ${allLangs.length} language(s): ${allLangs.join(', ')}`);
97
+ const baseLang = allLangs[0];
98
+ for (const [namespace, baseKeys] of Object.entries(locales[baseLang] || {})) {
99
+ for (let i = 1; i < allLangs.length; i++) {
100
+ const lang = allLangs[i];
101
+ const otherKeys = (locales[lang] || {})[namespace] || new Set();
102
+ let missing = 0;
103
+ for (const k of baseKeys)
104
+ if (!otherKeys.has(k))
105
+ missing++;
106
+ if (missing > 0)
107
+ issues.push(`[X] ${lang}/${namespace}: Missing ${missing} keys`);
108
+ let extra = 0;
109
+ for (const k of otherKeys)
110
+ if (!baseKeys.has(k))
111
+ extra++;
112
+ if (extra > 0)
113
+ issues.push(`[!] ${lang}/${namespace}: ${extra} extra keys`);
114
+ }
115
+ }
116
+ if (issues.length === 0)
117
+ passed.push("[OK] All locales have matching keys");
118
+ return { passed, issues };
119
+ }
120
+ export async function checkHardcodedStrings(projectPath) {
121
+ const passed = [];
122
+ const issues = [];
123
+ const exts = { '.tsx': 'jsx', '.jsx': 'jsx', '.ts': 'jsx', '.js': 'jsx', '.vue': 'vue', '.py': 'python' };
124
+ let codeFiles = [];
125
+ async function search(dir) {
126
+ try {
127
+ const items = await fs.readdir(dir, { withFileTypes: true });
128
+ for (const item of items) {
129
+ if (['node_modules', '.git', 'dist', 'build', '__pycache__', 'venv', 'test', 'spec'].includes(item.name))
130
+ continue;
131
+ const fullPath = path.join(dir, item.name);
132
+ if (item.isDirectory())
133
+ await search(fullPath);
134
+ else if (exts[path.extname(item.name)])
135
+ codeFiles.push(fullPath);
136
+ }
137
+ }
138
+ catch { }
139
+ }
140
+ await search(projectPath);
141
+ if (codeFiles.length === 0)
142
+ return { passed: ["[!] No code files found"], issues };
143
+ let filesWithI18n = 0;
144
+ let filesWithHardcoded = 0;
145
+ const examples = [];
146
+ for (const file of codeFiles.slice(0, 50)) {
147
+ try {
148
+ const content = await fs.readFile(file, 'utf-8');
149
+ const type = exts[path.extname(file)] || 'jsx';
150
+ const hasI18n = I18N_PATTERNS.some(p => new RegExp(p).test(content));
151
+ if (hasI18n)
152
+ filesWithI18n++;
153
+ let hardcodedFound = false;
154
+ for (const pattern of HARDCODED_PATTERNS[type] || []) {
155
+ const match = content.match(new RegExp(pattern));
156
+ if (match && !hasI18n) {
157
+ hardcodedFound = true;
158
+ if (examples.length < 5)
159
+ examples.push(`${path.basename(file)}: ${match[0].substring(0, 40)}...`);
160
+ }
161
+ }
162
+ if (hardcodedFound)
163
+ filesWithHardcoded++;
164
+ }
165
+ catch { }
166
+ }
167
+ passed.push(`[OK] Analyzed ${codeFiles.length} code files`);
168
+ if (filesWithI18n > 0)
169
+ passed.push(`[OK] ${filesWithI18n} files use i18n`);
170
+ if (filesWithHardcoded > 0) {
171
+ issues.push(`[X] ${filesWithHardcoded} files may have hardcoded strings`);
172
+ for (const ex of examples)
173
+ issues.push(` -> ${ex}`);
174
+ }
175
+ else {
176
+ passed.push("[OK] No obvious hardcoded strings detected");
177
+ }
178
+ return { passed, issues };
179
+ }
180
+ export async function runI18nChecker(projectPath = ".") {
181
+ const root = path.resolve(projectPath);
182
+ let report = `============================================================\n`;
183
+ report += ` i18n CHECKER - Internationalization Audit\n`;
184
+ report += `============================================================\n\n`;
185
+ const localeFiles = await findLocaleFiles(root);
186
+ const localeRes = await checkLocaleCompleteness(localeFiles);
187
+ const codeRes = await checkHardcodedStrings(root);
188
+ report += `[LOCALE FILES]\n----------------------------------------\n`;
189
+ for (const p of localeRes.passed)
190
+ report += ` ${p}\n`;
191
+ for (const i of localeRes.issues)
192
+ report += ` ${i}\n`;
193
+ report += `\n[CODE ANALYSIS]\n----------------------------------------\n`;
194
+ for (const p of codeRes.passed)
195
+ report += ` ${p}\n`;
196
+ for (const i of codeRes.issues)
197
+ report += ` ${i}\n`;
198
+ const criticals = [...localeRes.issues, ...codeRes.issues].filter(i => i.startsWith("[X]")).length;
199
+ report += `\n============================================================\n`;
200
+ if (criticals === 0)
201
+ report += `[OK] i18n CHECK: PASSED\n`;
202
+ else
203
+ report += `[X] i18n CHECK: ${criticals} issues found\n`;
204
+ return { passed: criticals === 0, report };
205
+ }
@@ -0,0 +1,50 @@
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
+ import * as os from 'os';
6
+ const execAsync = promisify(exec);
7
+ export async function runLighthouseAudit(url) {
8
+ const tempFilePath = path.join(os.tmpdir(), `lighthouse-${Date.now()}.json`);
9
+ let report = `============================================================\n`;
10
+ report += `LIGHTHOUSE AUDIT REPORT for ${url}\n`;
11
+ report += `============================================================\n`;
12
+ try {
13
+ await execAsync(`lighthouse "${url}" --output=json --output-path="${tempFilePath}" --chrome-flags="--headless" --only-categories=performance,accessibility,best-practices,seo`);
14
+ }
15
+ catch (error) {
16
+ // Lighthouse sometimes exits with code 1 even when the report is valid generated
17
+ // We will proceed to check if the file was created
18
+ }
19
+ try {
20
+ const fileContent = await fs.readFile(tempFilePath, 'utf-8');
21
+ const data = JSON.parse(fileContent);
22
+ await fs.unlink(tempFilePath).catch(() => { }); // cleanup
23
+ const categories = data.categories || {};
24
+ const performance = Math.round((categories.performance?.score || 0) * 100);
25
+ const accessibility = Math.round((categories.accessibility?.score || 0) * 100);
26
+ const bestPractices = Math.round((categories['best-practices']?.score || 0) * 100);
27
+ const seo = Math.round((categories.seo?.score || 0) * 100);
28
+ report += `Performance: ${performance}/100\n`;
29
+ report += `Accessibility: ${accessibility}/100\n`;
30
+ report += `Best Practices: ${bestPractices}/100\n`;
31
+ report += `SEO: ${seo}/100\n\n`;
32
+ // Using average for pass/fail logic
33
+ const avg = (performance + accessibility + bestPractices + seo) / 4;
34
+ if (performance >= 90) {
35
+ report += `[OK] Excellent performance\n`;
36
+ }
37
+ else if (performance >= 50) {
38
+ report += `[!] Needs improvement\n`;
39
+ }
40
+ else {
41
+ report += `[X] Poor performance\n`;
42
+ }
43
+ return { passed: avg >= 80, report };
44
+ }
45
+ catch (e) {
46
+ report += `[ERROR] Failed to read or parse Lighthouse report: ${e.message}\n`;
47
+ report += `Ensure Lighthouse CLI is installed globally (npm install -g lighthouse).\n`;
48
+ return { passed: false, report };
49
+ }
50
+ }
@@ -0,0 +1,106 @@
1
+ import * as fs from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import * as path from 'path';
4
+ import { spawn } from 'child_process';
5
+ export async function detectProjectType(projectPath) {
6
+ const result = { type: 'unknown', linters: [] };
7
+ const root = path.resolve(projectPath);
8
+ const pkgPath = path.join(root, 'package.json');
9
+ if (existsSync(pkgPath)) {
10
+ result.type = 'node';
11
+ try {
12
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
13
+ const scripts = pkg.scripts || {};
14
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
15
+ if (scripts['lint']) {
16
+ result.linters.push({ name: 'npm lint', cmd: ['npm', 'run', 'lint'] });
17
+ }
18
+ else if (deps['eslint']) {
19
+ result.linters.push({ name: 'eslint', cmd: ['npx', 'eslint', '.'] });
20
+ }
21
+ if (deps['typescript'] || existsSync(path.join(root, 'tsconfig.json'))) {
22
+ result.linters.push({ name: 'tsc', cmd: ['npx', 'tsc', '--noEmit'] });
23
+ }
24
+ }
25
+ catch { }
26
+ }
27
+ if (existsSync(path.join(root, 'pyproject.toml')) || existsSync(path.join(root, 'requirements.txt'))) {
28
+ result.type = 'python';
29
+ result.linters.push({ name: 'ruff', cmd: ['ruff', 'check', '.'] });
30
+ if (existsSync(path.join(root, 'mypy.ini')) || existsSync(path.join(root, 'pyproject.toml'))) {
31
+ result.linters.push({ name: 'mypy', cmd: ['mypy', '.'] });
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ export async function runLinter(linter, cwd) {
37
+ return new Promise((resolve) => {
38
+ let cmd = linter.cmd[0];
39
+ const args = linter.cmd.slice(1);
40
+ // Handle npx/npm on windows
41
+ if (process.platform === 'win32' && (cmd === 'npm' || cmd === 'npx')) {
42
+ cmd += '.cmd';
43
+ }
44
+ const child = spawn(cmd, args, { cwd, shell: true });
45
+ let out = '';
46
+ let err = '';
47
+ child.stdout.on('data', d => out += d.toString());
48
+ child.stderr.on('data', d => err += d.toString());
49
+ child.on('close', code => {
50
+ resolve({
51
+ name: linter.name,
52
+ passed: code === 0,
53
+ output: out.substring(0, 2000),
54
+ error: err.substring(0, 500)
55
+ });
56
+ });
57
+ child.on('error', e => {
58
+ resolve({
59
+ name: linter.name,
60
+ passed: false,
61
+ output: '',
62
+ error: e.message
63
+ });
64
+ });
65
+ setTimeout(() => {
66
+ child.kill();
67
+ resolve({
68
+ name: linter.name,
69
+ passed: false,
70
+ output: '',
71
+ error: 'Timeout after 120s'
72
+ });
73
+ }, 120000);
74
+ });
75
+ }
76
+ export async function runLintRunner(projectPath = ".") {
77
+ const root = path.resolve(projectPath);
78
+ let report = `============================================================\n`;
79
+ report += `[LINT RUNNER] Unified Linting\n`;
80
+ report += `============================================================\n`;
81
+ report += `Project: ${root}\n`;
82
+ const info = await detectProjectType(root);
83
+ report += `Type: ${info.type}\n`;
84
+ report += `Linters: ${info.linters.length}\n------------------------------------------------------------\n`;
85
+ if (info.linters.length === 0) {
86
+ report += "No linters found for this project type.\n";
87
+ return { passed: true, report };
88
+ }
89
+ let allPassed = true;
90
+ for (const linter of info.linters) {
91
+ report += `\nRunning: ${linter.name}...\n`;
92
+ const result = await runLinter(linter, root);
93
+ if (result.passed) {
94
+ report += ` [PASS] ${linter.name}\n`;
95
+ }
96
+ else {
97
+ report += ` [FAIL] ${linter.name}\n`;
98
+ if (result.error)
99
+ report += ` Error: ${result.error.substring(0, 200)}\n`;
100
+ allPassed = false;
101
+ }
102
+ }
103
+ report += `\n============================================================\nSUMMARY\n============================================================\n`;
104
+ report += allPassed ? "[OK] LINT CHECKS PASSED\n" : "[X] LINT CHECKS FAILED\n";
105
+ return { passed: allPassed, report };
106
+ }
@@ -0,0 +1,190 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ class MobileAuditor {
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
+ const isReactNative = /react-native|@react-navigation|React\.Native/.test(content);
12
+ const isFlutter = /import 'package:flutter|MaterialApp|Widget\.build/.test(content);
13
+ if (!isReactNative && !isFlutter)
14
+ return;
15
+ this.files_checked++;
16
+ const filename = path.basename(filePath);
17
+ // Touch Psychology
18
+ const smallSizes = [...content.matchAll(/(?:width|height|size):\s*([0-3]\d)/g)];
19
+ for (const match of smallSizes) {
20
+ if (parseInt(match[1]) < 44)
21
+ this.issues.push(`[Touch Target] ${filename}: size ${match[1]}px < 44px`);
22
+ }
23
+ const smallGaps = [...content.matchAll(/(?:margin|gap):\s*([0-7])\s*(?:px|dp)/g)];
24
+ for (const match of smallGaps) {
25
+ if (parseInt(match[1]) < 8)
26
+ this.warnings.push(`[Touch Spacing] ${filename}: gap ${match[1]}px < 8px`);
27
+ }
28
+ const hasPrimaryBtn = /(?:testID|id):\s*["'](?:.*(?:primary|cta|submit|confirm)[^"']*)["']/i.test(content);
29
+ const hasBottomObj = /position:\s*["']?absolute["']?|bottom:\s*\d+|style.*bottom|justifyContent:\s*["']?flex-end/.test(content);
30
+ if (hasPrimaryBtn && !hasBottomObj)
31
+ this.warnings.push(`[Thumb Zone] ${filename}: Primary CTA may not be at bottom`);
32
+ if (/Swipeable|onSwipe|PanGestureHandler|swipe/.test(content) && !/Button.*(?:delete|archive|more)|TouchableOpacity|Pressable/.test(content)) {
33
+ this.warnings.push(`[Gestures] ${filename}: Swipe without visible button alternative`);
34
+ }
35
+ if (/(?:onPress|onSubmit|delete|remove|confirm|purchase)/.test(content) && !/Haptics|Vibration|react-native-haptic-feedback|FeedbackManager/.test(content)) {
36
+ this.warnings.push(`[Haptics] ${filename}: Important action without haptics`);
37
+ }
38
+ if (isReactNative && /Pressable|TouchableOpacity/.test(content) && !/pressed|style.*opacity|underlay/.test(content)) {
39
+ this.warnings.push(`[Touch Feedback] ${filename}: Pressable without visual feedback state`);
40
+ }
41
+ // Performance
42
+ if (/<ScrollView|ScrollView\./.test(content) && /ScrollView.*\.map\(|ScrollView.*\{.*\.map/s.test(content)) {
43
+ this.issues.push(`[Performance CRITICAL] ${filename}: ScrollView with .map() detected. Use FlatList.`);
44
+ }
45
+ if (isReactNative && /FlatList|FlashList|SectionList/.test(content) && !/React\.memo|memo\(/.test(content)) {
46
+ this.warnings.push(`[Performance] ${filename}: List without React.memo`);
47
+ }
48
+ if (isReactNative && /FlatList|FlashList/.test(content) && !/useCallback/.test(content)) {
49
+ this.warnings.push(`[Performance] ${filename}: FlatList renderItem without useCallback`);
50
+ }
51
+ if (isReactNative && /FlatList/.test(content)) {
52
+ if (!/keyExtractor/.test(content))
53
+ this.issues.push(`[Performance CRITICAL] ${filename}: FlatList without keyExtractor`);
54
+ if (/key=\{.*index.*?\}|key:\s*index/.test(content))
55
+ this.issues.push(`[Performance CRITICAL] ${filename}: Using index as key`);
56
+ }
57
+ if (isReactNative && /Animated\./.test(content)) {
58
+ if (/useNativeDriver:\s*false/.test(content))
59
+ this.warnings.push(`[Performance] ${filename}: Animation.useNativeDriver is false`);
60
+ if (!/useNativeDriver/.test(content))
61
+ this.warnings.push(`[Performance] ${filename}: Animated without useNativeDriver`);
62
+ }
63
+ if (isReactNative && /useEffect/.test(content) && /addEventListener|subscribe|\.focus\(\)|\.off\(/.test(content) && !/return\s*\(\)\s*=>|return\s+function/.test(content)) {
64
+ this.issues.push(`[Memory Leak] ${filename}: useEffect subscription without cleanup`);
65
+ }
66
+ const consoleCount = (content.match(/console\.(log|warn|error|debug)/g) || []).length;
67
+ if (consoleCount > 5)
68
+ this.warnings.push(`[Performance] ${filename}: ${consoleCount} console statements logs`);
69
+ if (isReactNative) {
70
+ const inlines = (content.match(/(?:onPress|onPressIn|onPressOut|renderItem):\s*\([^)]*\)\s*=>/g) || []).length;
71
+ if (inlines > 3)
72
+ this.warnings.push(`[Performance] ${filename}: ${inlines} inline functions in props`);
73
+ }
74
+ if (/Animated\.timing.*(?:width|height|margin|padding)/.test(content)) {
75
+ this.issues.push(`[Performance] ${filename}: Animating layout properties instead of transform/opacity`);
76
+ }
77
+ // Navigation
78
+ const tabs = (content.match(/Tab\.Screen|createBottomTabNavigator|BottomTab/g) || []).length;
79
+ if (tabs > 5)
80
+ this.warnings.push(`[Navigation] ${filename}: ${tabs} tab items (max 5)`);
81
+ if (/createBottomTabNavigator|Tab\.Navigator/.test(content) && !/lazy:\s*false/.test(content)) {
82
+ this.warnings.push(`[Navigation] ${filename}: Tab bar without lazy: false`);
83
+ }
84
+ if (/(onBackPress|handleBackPress)/.test(content) && !/(BackHandler|useFocusEffect|navigation\.addListener)/.test(content)) {
85
+ this.warnings.push(`[Navigation] ${filename}: Custom back handling without BackHandler listnener`);
86
+ }
87
+ const hasLinkConfig = /apollo-link|react-native-screens|navigation\.link/.test(content);
88
+ if (/Linking\.|Linking\.openURL|deepLink|universalLink/.test(content) && !hasLinkConfig) {
89
+ this.warnings.push(`[Navigation] ${filename}: Deep linking detected but lacks configuration.`);
90
+ }
91
+ else if (!hasLinkConfig) {
92
+ this.passed_count++;
93
+ }
94
+ // Typography
95
+ if (isReactNative) {
96
+ if (/fontFamily:\s*["'][^"']+/.test(content) && !/fontFamily:\s*["']?(?:System|San Francisco|Roboto|-apple-system)/.test(content)) {
97
+ this.warnings.push(`[Typography] ${filename}: Custom font detected instead of system fonts`);
98
+ }
99
+ if (/fontSize:/.test(content) && !/allowFontScaling:\s*true|responsiveFontSize|useWindowDimensions/.test(content)) {
100
+ this.warnings.push(`[Typography] ${filename}: Fixed font sizes without scaling support`);
101
+ }
102
+ }
103
+ const lineHeights = [...content.matchAll(/lineHeight:\s*([\d.]+)/g)];
104
+ for (const lh of lineHeights) {
105
+ if (parseFloat(lh[1]) > 1.8)
106
+ this.warnings.push(`[Typography] ${filename}: lineHeight > 1.8`);
107
+ }
108
+ const fontSizes = [...content.matchAll(/fontSize:\s*([\d.]+)/g)];
109
+ for (const fs of fontSizes) {
110
+ const size = parseFloat(fs[1]);
111
+ if (size < 12)
112
+ this.warnings.push(`[Typography] ${filename}: fontSize < 12px`);
113
+ else if (size > 32)
114
+ this.warnings.push(`[Typography] ${filename}: fontSize > 32px`);
115
+ }
116
+ // Colors
117
+ if (/#000000|color:\s*black|backgroundColor:\s*["']?black/.test(content)) {
118
+ this.warnings.push(`[Color] ${filename}: Pure black #000000 detected`);
119
+ }
120
+ if (!/useColorScheme|colorScheme|appearance:\s*["']?dark/.test(content) && !/\\\?.*dark|style:\s*.*dark|isDark/.test(content)) {
121
+ this.warnings.push(`[Color] ${filename}: No dark mode support detected`);
122
+ }
123
+ // Backend rules
124
+ if (/(token|jwt|auth.*storage)/i.test(content) && /AsyncStorage|@react-native-async-storage/.test(content) && !/SecureStore|Keychain|EncryptedSharedPreferences/.test(content)) {
125
+ this.issues.push(`[Security] ${filename}: Storing auth token in AsyncStorage`);
126
+ }
127
+ if (/fetch|axios|netinfo|@react-native-community\/netinfo/.test(content) && !/offline|isConnected|netInfo|cache.*offline/.test(content)) {
128
+ this.warnings.push(`[Offline] ${filename}: Network request without offline handling`);
129
+ }
130
+ if (/Notifications|pushNotification|Firebase\.messaging|PushNotificationIOS/.test(content) && !/onNotification|addNotificationListener|notification\.open/.test(content)) {
131
+ this.warnings.push(`[Push] ${filename}: Push notifications without handler`);
132
+ }
133
+ // OLED & visibility
134
+ if (/backgroundColor:\s*["']?#[0-9A-Fa-f]{6}/.test(content) && !/#121212|#1A1A1A|#0D0D0D|#000000/.test(content)) {
135
+ this.warnings.push(`[Mobile Color] ${filename}: Consider OLED-optimized dark backgrounds`);
136
+ }
137
+ if (/(dark:\s*|isDark|useColorScheme|colorScheme:\s*["']?dark)/.test(content) && /(color:\s*["']?#ffffff|#fff["']?\}|textColor:\s*["']?white)/.test(content)) {
138
+ this.warnings.push(`[Mobile Color] ${filename}: Pure white text in dark mode`);
139
+ }
140
+ }
141
+ catch { }
142
+ }
143
+ async auditDirectory(dir) {
144
+ try {
145
+ const items = await fs.readdir(dir, { withFileTypes: true });
146
+ for (const item of items) {
147
+ if (['node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', '.idea'].includes(item.name))
148
+ continue;
149
+ const fullPath = path.join(dir, item.name);
150
+ if (item.isDirectory()) {
151
+ await this.auditDirectory(fullPath);
152
+ }
153
+ else if (/\.(tsx|ts|jsx|js|dart)$/.test(item.name)) {
154
+ await this.auditFile(fullPath);
155
+ }
156
+ }
157
+ }
158
+ catch { }
159
+ }
160
+ getReport() {
161
+ return {
162
+ files_checked: this.files_checked,
163
+ issues: this.issues,
164
+ warnings: this.warnings,
165
+ passed_checks: this.passed_count,
166
+ compliant: this.issues.length === 0
167
+ };
168
+ }
169
+ }
170
+ export async function runMobileAudit(projectPath = ".") {
171
+ const root = path.resolve(projectPath);
172
+ const auditor = new MobileAuditor();
173
+ await auditor.auditDirectory(root);
174
+ const res = auditor.getReport();
175
+ let report = `\n[MOBILE AUDIT] ${res.files_checked} mobile files checked\n`;
176
+ report += `--------------------------------------------------\n`;
177
+ if (res.issues.length > 0) {
178
+ report += `[!] ISSUES (${res.issues.length}):\n`;
179
+ for (const i of res.issues.slice(0, 10))
180
+ report += ` - ${i}\n`;
181
+ }
182
+ if (res.warnings.length > 0) {
183
+ report += `[*] WARNINGS (${res.warnings.length}):\n`;
184
+ for (const w of res.warnings.slice(0, 15))
185
+ report += ` - ${w}\n`;
186
+ }
187
+ report += `[+] PASSED CHECKS: ${res.passed_checks}\n`;
188
+ report += `STATUS: ${res.compliant ? 'PASS' : 'FAIL'}\n`;
189
+ return { passed: res.compliant, report };
190
+ }
@@ -0,0 +1,101 @@
1
+ import { chromium } from 'playwright';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ export async function runPlaywrightTest(url, takeScreenshot = false) {
5
+ const result = {
6
+ url,
7
+ timestamp: new Date().toISOString(),
8
+ status: 'pending'
9
+ };
10
+ let browser;
11
+ try {
12
+ browser = await chromium.launch({ headless: true });
13
+ const context = await browser.newContext({
14
+ viewport: { width: 1280, height: 720 },
15
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
16
+ });
17
+ const page = await context.newPage();
18
+ const consoleErrors = [];
19
+ page.on('console', msg => {
20
+ if (msg.type() === 'error')
21
+ consoleErrors.push(msg.text());
22
+ });
23
+ const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 }).catch(() => null);
24
+ result.page = {
25
+ title: await page.title().catch(() => ''),
26
+ url: page.url(),
27
+ status_code: response ? response.status() : null
28
+ };
29
+ result.health = {
30
+ loaded: response ? response.ok() : false,
31
+ has_title: !!result.page.title,
32
+ has_h1: (await page.locator('h1').count()) > 0,
33
+ has_links: (await page.locator('a').count()) > 0,
34
+ has_images: (await page.locator('img').count()) > 0
35
+ };
36
+ const timingStr = await page.evaluate(() => JSON.stringify({
37
+ dom_content_loaded: window.performance.timing.domContentLoadedEventEnd - window.performance.timing.navigationStart,
38
+ load_complete: window.performance.timing.loadEventEnd - window.performance.timing.navigationStart
39
+ }));
40
+ result.performance = JSON.parse(timingStr);
41
+ if (takeScreenshot) {
42
+ const screenshotDir = path.join(os.tmpdir(), "maestro_screenshots");
43
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
44
+ const screenshotPath = path.join(screenshotDir, `screenshot_${timestamp}.png`);
45
+ // Note: mkdirs internally handled by playwright or Node requires `fs.mkdir`
46
+ await page.screenshot({ path: screenshotPath, fullPage: true });
47
+ result.screenshot = screenshotPath;
48
+ result.screenshot_note = "Saved to temp directory (auto-cleaned by OS)";
49
+ }
50
+ result.elements = {
51
+ links: await page.locator('a').count(),
52
+ buttons: await page.locator('button').count(),
53
+ inputs: await page.locator('input').count(),
54
+ images: await page.locator('img').count(),
55
+ forms: await page.locator('form').count()
56
+ };
57
+ result.status = result.health.loaded ? 'success' : 'failed';
58
+ result.summary = result.status === 'success' ? '[OK] Page loaded successfully' : '[X] Page failed to load';
59
+ }
60
+ catch (e) {
61
+ result.status = 'error';
62
+ result.error = e.message;
63
+ result.summary = `[X] Error: ${e.message.substring(0, 100)}`;
64
+ }
65
+ finally {
66
+ if (browser)
67
+ await browser.close().catch(() => { });
68
+ }
69
+ return result;
70
+ }
71
+ export async function runPlaywrightA11y(url) {
72
+ const result = { url, accessibility: {} };
73
+ let browser;
74
+ try {
75
+ browser = await chromium.launch({ headless: true });
76
+ const page = await browser.newPage();
77
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
78
+ result.accessibility = {
79
+ images_with_alt: await page.locator('img[alt]').count(),
80
+ images_without_alt: await page.locator('img:not([alt])').count(),
81
+ buttons_with_label: (await page.locator('button[aria-label]').count()) + (await page.locator('button:has-text("")').count()),
82
+ links_with_text: await page.locator('a:has-text("")').count(),
83
+ form_labels: await page.locator('label').count(),
84
+ headings: {
85
+ h1: await page.locator('h1').count(),
86
+ h2: await page.locator('h2').count(),
87
+ h3: await page.locator('h3').count()
88
+ }
89
+ };
90
+ result.status = 'success';
91
+ }
92
+ catch (e) {
93
+ result.status = 'error';
94
+ result.error = e.message;
95
+ }
96
+ finally {
97
+ if (browser)
98
+ await browser.close().catch(() => { });
99
+ }
100
+ return result;
101
+ }