vibes-plug 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/BLUEPRINT.md +125 -0
- package/CHANGELOG.md +195 -0
- package/CONTRIBUTING.md +199 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/SECURITY.md +21 -0
- package/banner.png +0 -0
- package/package.json +25 -0
- package/plugin.json +8 -0
- package/scripts/update_skills.js +75 -0
- package/skills/ai-llm-integration-expert/SKILL.md +162 -0
- package/skills/api-design-expert/SKILL.md +310 -0
- package/skills/app-analyzer-optimizer/SKILL.md +189 -0
- package/skills/asisten_ramah/SKILL.md +41 -0
- package/skills/authentication-identity-expert/SKILL.md +45 -0
- package/skills/auto-doc-updater/SKILL.md +204 -0
- package/skills/bootstrap-to-modern/SKILL.md +87 -0
- package/skills/brainstorming/SKILL.md +353 -0
- package/skills/bun-runtime-expert/SKILL.md +211 -0
- package/skills/ci-cd-devops-architect/SKILL.md +45 -0
- package/skills/cloud-hosting-expert/SKILL.md +244 -0
- package/skills/coderabbit/SKILL.md +192 -0
- package/skills/data-telemetry-expert/SKILL.md +213 -0
- package/skills/database-orm-expert/SKILL.md +294 -0
- package/skills/design-system-architect/SKILL.md +243 -0
- package/skills/e2e-testing-expert/SKILL.md +315 -0
- package/skills/event-driven-architect/SKILL.md +81 -0
- package/skills/firebase-security-expert/SKILL.md +195 -0
- package/skills/fullstack-expert/SKILL.md +202 -0
- package/skills/fullstack-expert/references/api_design_guide.md +466 -0
- package/skills/fullstack-expert/references/devops_infrastructure.md +477 -0
- package/skills/fullstack-expert/references/multi_language_backend.md +528 -0
- package/skills/fullstack-expert/references/system_design_patterns.md +358 -0
- package/skills/fullstack-expert/scripts/api_contract_validator.py +253 -0
- package/skills/fullstack-expert/scripts/architecture_analyzer.py +326 -0
- package/skills/gemini-agent-booster/SKILL.md +135 -0
- package/skills/global-a11y-i18n-expert/SKILL.md +81 -0
- package/skills/go-programming-expert/SKILL.md +295 -0
- package/skills/hig/SKILL.md +188 -0
- package/skills/js-backend-expert/SKILL.md +192 -0
- package/skills/mcp-server-architect/SKILL.md +194 -0
- package/skills/mobile-expo-expert/SKILL.md +186 -0
- package/skills/monday-design-aesthetic/SKILL.md +67 -0
- package/skills/monorepo-architect/SKILL.md +227 -0
- package/skills/mpa-orchestrator/SKILL.md +101 -0
- package/skills/multi-agent-orchestration/SKILL.md +234 -0
- package/skills/multiple-entry-points/SKILL.md +55 -0
- package/skills/mvc-expert/SKILL.md +231 -0
- package/skills/payment-gateway-expert/SKILL.md +45 -0
- package/skills/performance-web-vitals/SKILL.md +332 -0
- package/skills/prd-architect/SKILL.md +191 -0
- package/skills/production-ready-hardener/SKILL.md +469 -0
- package/skills/production-ready-hardener/references/performance_optimization.md +441 -0
- package/skills/production-ready-hardener/references/production_checklist.md +161 -0
- package/skills/production-ready-hardener/references/security_hardening_guide.md +379 -0
- package/skills/production-ready-hardener/scripts/production_readiness_scanner.py +875 -0
- package/skills/python-programming-expert/SKILL.md +271 -0
- package/skills/realtime-collaboration-expert/SKILL.md +45 -0
- package/skills/rust-programming-expert/SKILL.md +235 -0
- package/skills/saas-billing/SKILL.md +377 -0
- package/skills/saas-multi-tenant/SKILL.md +237 -0
- package/skills/saas-mvp-launcher/SKILL.md +231 -0
- package/skills/saas-transformer/SKILL.md +446 -0
- package/skills/saas-transformer/references/billing_integration_guide.md +401 -0
- package/skills/saas-transformer/references/feature_gating_patterns.md +137 -0
- package/skills/saas-transformer/references/saas_transformation_checklist.md +121 -0
- package/skills/saas-transformer/scripts/saas_transformation_scanner.py +254 -0
- package/skills/scalability-clean-code/SKILL.md +229 -0
- package/skills/secure-fuzz-testing/SKILL.md +201 -0
- package/skills/senior-frontend/SKILL.md +161 -0
- package/skills/senior-frontend/references/frontend_best_practices.md +806 -0
- package/skills/senior-frontend/references/nextjs_optimization_guide.md +724 -0
- package/skills/senior-frontend/references/react_patterns.md +746 -0
- package/skills/senior-frontend/scripts/bundle_analyzer.py +407 -0
- package/skills/senior-frontend/scripts/component_generator.py +329 -0
- package/skills/senior-frontend/scripts/frontend_scaffolder.py +1005 -0
- package/skills/senior-fullstack/SKILL.md +167 -0
- package/skills/senior-fullstack/references/architecture_patterns.md +160 -0
- package/skills/senior-fullstack/references/development_workflows.md +222 -0
- package/skills/senior-fullstack/references/tech_stack_guide.md +190 -0
- package/skills/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
- package/skills/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
- package/skills/senior-fullstack/scripts/project_scaffolder.py +114 -0
- package/skills/seo/SKILL.md +225 -0
- package/skills/seo/references/cwv-thresholds.md +108 -0
- package/skills/seo/references/eeat-framework.md +214 -0
- package/skills/seo/references/quality-gates.md +155 -0
- package/skills/seo/references/schema-types.md +118 -0
- package/skills/seo-aeo-landing-page-writer/SKILL.md +97 -0
- package/skills/seo-geo/SKILL.md +188 -0
- package/skills/session-handoff-resume/SKILL.md +158 -0
- package/skills/skill_baru/SKILL.md +147 -0
- package/skills/spa-orchestrator/SKILL.md +288 -0
- package/skills/state-management-expert/SKILL.md +272 -0
- package/skills/supabase-migration/SKILL.md +45 -0
- package/skills/supabase-security-expert/SKILL.md +243 -0
- package/skills/tailwind-expert/SKILL.md +188 -0
- package/skills/tanstack-query-expert/SKILL.md +199 -0
- package/skills/tauri-expert/SKILL.md +97 -0
- package/skills/token-saver/SKILL.md +111 -0
- package/skills/typescript-expert/SKILL.md +279 -0
- package/skills/ui-components-expert/SKILL.md +63 -0
- package/skills/ui-ux-pro-max/SKILL.md +201 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-310.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-310.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
- package/skills/ui-ux-pro-max/scripts/core.py +257 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +493 -0
- package/skills/ui-ux-pro-max/scripts/search.py +81 -0
- package/skills/ui_ux_expert/SKILL.md +114 -0
- package/skills/vibe-code-gardener/SKILL.md +173 -0
- package/skills/web-scraper/SKILL.md +205 -0
- package/skills/web-scraper/references/data-transforms.md +397 -0
- package/skills/web-scraper/references/extraction-patterns.md +475 -0
- package/skills/web-scraper/references/output-templates.md +481 -0
- package/skills/zero-to-prod-orchestrator/SKILL.md +180 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Production Readiness Scanner
|
|
4
|
+
Automated scanner that runs all 7 phases of the Production-Ready Hardener
|
|
5
|
+
checklist against a target project.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import argparse
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Tuple
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProductionReadinessScanner:
|
|
19
|
+
"""Scans a project for production readiness across 7 phases."""
|
|
20
|
+
|
|
21
|
+
PHASE_WEIGHTS = {
|
|
22
|
+
'architecture': 0.10,
|
|
23
|
+
'frontend': 0.15,
|
|
24
|
+
'backend': 0.15,
|
|
25
|
+
'security': 0.25,
|
|
26
|
+
'testing': 0.15,
|
|
27
|
+
'performance': 0.10,
|
|
28
|
+
'devops': 0.10,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def __init__(self, target_path: str, verbose: bool = False,
|
|
32
|
+
run_tsc: bool = False, run_lint: bool = False,
|
|
33
|
+
run_tests: bool = False, run_build: bool = False):
|
|
34
|
+
self.target_path = Path(target_path).resolve()
|
|
35
|
+
self.verbose = verbose
|
|
36
|
+
self.run_tsc_cmd = run_tsc
|
|
37
|
+
self.run_lint_cmd = run_lint
|
|
38
|
+
self.run_tests_cmd = run_tests
|
|
39
|
+
self.run_build_cmd = run_build
|
|
40
|
+
|
|
41
|
+
self.phases: Dict[str, Dict] = {}
|
|
42
|
+
self.overall_score = 0
|
|
43
|
+
self.stack = {}
|
|
44
|
+
|
|
45
|
+
# Parsed log results
|
|
46
|
+
self.parsed_tsc_errors = []
|
|
47
|
+
self.parsed_eslint_errors = []
|
|
48
|
+
self.build_errors = []
|
|
49
|
+
|
|
50
|
+
# Execution logs
|
|
51
|
+
self.execution_results = {}
|
|
52
|
+
|
|
53
|
+
def strip_ansi(self, text: str) -> str:
|
|
54
|
+
"""Strip ANSI escape sequences from text."""
|
|
55
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
56
|
+
return ansi_escape.sub('', text)
|
|
57
|
+
|
|
58
|
+
def _exec_command(self, cmd: str) -> Tuple[int, str]:
|
|
59
|
+
"""Execute a shell command within the target path."""
|
|
60
|
+
try:
|
|
61
|
+
res = subprocess.run(
|
|
62
|
+
cmd,
|
|
63
|
+
shell=True,
|
|
64
|
+
cwd=str(self.target_path),
|
|
65
|
+
stdout=subprocess.PIPE,
|
|
66
|
+
stderr=subprocess.STDOUT,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=240 # 4 minutes timeout
|
|
69
|
+
)
|
|
70
|
+
return res.returncode, res.stdout
|
|
71
|
+
except subprocess.TimeoutExpired as e:
|
|
72
|
+
return -1, f"Command timed out. Output so far:\n{e.output}"
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return -1, f"Execution failed: {str(e)}"
|
|
75
|
+
|
|
76
|
+
def _detect_stack(self):
|
|
77
|
+
"""Analyze project files and package.json to identify tech stack."""
|
|
78
|
+
self.stack = {
|
|
79
|
+
'framework': 'Unknown',
|
|
80
|
+
'react': False,
|
|
81
|
+
'react_version': None,
|
|
82
|
+
'typescript': False,
|
|
83
|
+
'vite': False,
|
|
84
|
+
'next': False,
|
|
85
|
+
'supabase': False,
|
|
86
|
+
'firebase': False,
|
|
87
|
+
'vitest': False,
|
|
88
|
+
'eslint': False,
|
|
89
|
+
'playwright': False,
|
|
90
|
+
'pwa': False,
|
|
91
|
+
'analytics': False,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Check package.json
|
|
95
|
+
pkg_path = self.target_path / 'package.json'
|
|
96
|
+
if pkg_path.exists():
|
|
97
|
+
try:
|
|
98
|
+
pkg = json.loads(pkg_path.read_text(encoding='utf-8', errors='ignore'))
|
|
99
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
100
|
+
|
|
101
|
+
self.stack['typescript'] = 'typescript' in deps
|
|
102
|
+
self.stack['vite'] = 'vite' in deps
|
|
103
|
+
self.stack['next'] = 'next' in deps
|
|
104
|
+
self.stack['supabase'] = '@supabase/supabase-js' in deps
|
|
105
|
+
self.stack['firebase'] = 'firebase' in deps or 'firebase-admin' in deps
|
|
106
|
+
self.stack['vitest'] = 'vitest' in deps
|
|
107
|
+
self.stack['eslint'] = 'eslint' in deps
|
|
108
|
+
self.stack['playwright'] = '@playwright/test' in deps
|
|
109
|
+
self.stack['pwa'] = 'vite-plugin-pwa' in deps
|
|
110
|
+
|
|
111
|
+
self.stack['analytics'] = any(
|
|
112
|
+
k in deps for k in ['@vercel/analytics', '@vercel/speed-insights', 'react-ga', 'mixpanel-browser']
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if 'react' in deps:
|
|
116
|
+
self.stack['react'] = True
|
|
117
|
+
self.stack['react_version'] = deps['react']
|
|
118
|
+
if self.stack['next']:
|
|
119
|
+
self.stack['framework'] = 'Next.js'
|
|
120
|
+
elif self.stack['vite']:
|
|
121
|
+
self.stack['framework'] = 'Vite + React'
|
|
122
|
+
else:
|
|
123
|
+
self.stack['framework'] = 'React SPA'
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# Supplemental folder structure detection
|
|
128
|
+
if (self.target_path / 'supabase').is_dir():
|
|
129
|
+
self.stack['supabase'] = True
|
|
130
|
+
if (self.target_path / 'firestore.rules').exists():
|
|
131
|
+
self.stack['firebase'] = True
|
|
132
|
+
|
|
133
|
+
def _parse_logs(self):
|
|
134
|
+
"""Parse pre-existing or dynamic diagnostic logs to find actual code problems."""
|
|
135
|
+
# 1. Parse TypeScript Compiler (tsc) errors
|
|
136
|
+
tsc_log_paths = ['tsc_errors.txt', 'tsc_errors.log', 'full_errors.txt']
|
|
137
|
+
for p in tsc_log_paths:
|
|
138
|
+
full_p = self.target_path / p
|
|
139
|
+
if full_p.exists():
|
|
140
|
+
try:
|
|
141
|
+
content = full_p.read_text(encoding='utf-8', errors='ignore')
|
|
142
|
+
self.parsed_tsc_errors = self._parse_tsc_content(content)
|
|
143
|
+
if self.parsed_tsc_errors:
|
|
144
|
+
break
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# 2. Parse ESLint errors
|
|
149
|
+
eslint_log_paths = ['eslint_output.txt', 'lint_output.txt', 'lint_warnings.txt']
|
|
150
|
+
for p in eslint_log_paths:
|
|
151
|
+
full_p = self.target_path / p
|
|
152
|
+
if full_p.exists():
|
|
153
|
+
try:
|
|
154
|
+
content = full_p.read_text(encoding='utf-8', errors='ignore')
|
|
155
|
+
self.parsed_eslint_errors = self._parse_eslint_content(content)
|
|
156
|
+
if self.parsed_eslint_errors:
|
|
157
|
+
break
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# 3. Parse build error logs
|
|
162
|
+
build_log_paths = ['build_err.txt', 'build_err2.txt', 'build_errors.txt', 'build_full_log.txt']
|
|
163
|
+
for p in build_log_paths:
|
|
164
|
+
full_p = self.target_path / p
|
|
165
|
+
if full_p.exists():
|
|
166
|
+
try:
|
|
167
|
+
content = full_p.read_text(encoding='utf-8', errors='ignore')
|
|
168
|
+
if "error" in content.lower() or "failed" in content.lower() or "exception" in content.lower():
|
|
169
|
+
self.build_errors.append({
|
|
170
|
+
'source_file': p,
|
|
171
|
+
'content': content[:800] # First 800 chars
|
|
172
|
+
})
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def _parse_tsc_content(self, content: str) -> List[Dict]:
|
|
177
|
+
"""Convert tsc error console dump into structured lists."""
|
|
178
|
+
content = self.strip_ansi(content)
|
|
179
|
+
errors = []
|
|
180
|
+
lines = content.splitlines()
|
|
181
|
+
for line in lines:
|
|
182
|
+
line_str = line.strip()
|
|
183
|
+
# Match formats like: src/pages/Inventory.tsx(42,11): error TS6133: 'containerVariants'...
|
|
184
|
+
match = re.match(r'^([^(]+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)', line_str)
|
|
185
|
+
if match:
|
|
186
|
+
filepath = match.group(1).strip()
|
|
187
|
+
line_num = int(match.group(2))
|
|
188
|
+
col_num = int(match.group(3))
|
|
189
|
+
code = match.group(4)
|
|
190
|
+
message = match.group(5).strip()
|
|
191
|
+
errors.append({
|
|
192
|
+
'file': filepath,
|
|
193
|
+
'line': line_num,
|
|
194
|
+
'col': col_num,
|
|
195
|
+
'code': code,
|
|
196
|
+
'message': message
|
|
197
|
+
})
|
|
198
|
+
else:
|
|
199
|
+
# Append sub-details to last error if applicable
|
|
200
|
+
if errors and (line_str.startswith("Property") or line_str.startswith("Type") or line_str.startswith("Types")):
|
|
201
|
+
errors[-1]['message'] += f"\n {line_str}"
|
|
202
|
+
return errors
|
|
203
|
+
|
|
204
|
+
def _parse_eslint_content(self, content: str) -> List[Dict]:
|
|
205
|
+
"""Convert eslint console dump into structured lists."""
|
|
206
|
+
content = self.strip_ansi(content)
|
|
207
|
+
errors = []
|
|
208
|
+
current_file = None
|
|
209
|
+
lines = content.splitlines()
|
|
210
|
+
for line in lines:
|
|
211
|
+
line_str = line.strip()
|
|
212
|
+
if not line_str:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Check if this line is a filepath indicator (e.g. C:\kafealagi\src\components\core\Sidebar.tsx)
|
|
216
|
+
file_match = re.search(r'([a-zA-Z]:\\[^\s]+|[a-zA-Z]:/[^\s]+|[\w\-_\./\\]+\.(?:tsx?|jsx?|ts|js))', line_str)
|
|
217
|
+
if file_match and not any(kw in line_str for kw in ['problem', 'warning', 'error', 'eslint', 'package.json']):
|
|
218
|
+
path_cand = file_match.group(1)
|
|
219
|
+
try:
|
|
220
|
+
p = Path(path_cand)
|
|
221
|
+
if p.is_absolute():
|
|
222
|
+
current_file = str(p.relative_to(self.target_path))
|
|
223
|
+
else:
|
|
224
|
+
current_file = str(p)
|
|
225
|
+
except Exception:
|
|
226
|
+
current_file = path_cand
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Match error details: "131:9 error React Hook ... react-hooks/rules-of-hooks"
|
|
230
|
+
error_match = re.search(r'^(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}([a-zA-Z0-9\-/]+)$', line_str)
|
|
231
|
+
if error_match:
|
|
232
|
+
line_num = int(error_match.group(1))
|
|
233
|
+
col_num = int(error_match.group(2))
|
|
234
|
+
sev = error_match.group(3)
|
|
235
|
+
msg = error_match.group(4).strip()
|
|
236
|
+
rule = error_match.group(5)
|
|
237
|
+
errors.append({
|
|
238
|
+
'file': current_file or 'Unknown File',
|
|
239
|
+
'line': line_num,
|
|
240
|
+
'col': col_num,
|
|
241
|
+
'severity': sev,
|
|
242
|
+
'message': msg,
|
|
243
|
+
'rule': rule
|
|
244
|
+
})
|
|
245
|
+
return errors
|
|
246
|
+
|
|
247
|
+
def _run_diagnostics(self):
|
|
248
|
+
"""Optionally execute direct compiler, linter, test and build steps."""
|
|
249
|
+
if self.run_tsc_cmd:
|
|
250
|
+
print("⏳ Running TypeScript typecheck (tsc --noEmit)...")
|
|
251
|
+
ret, out = self._exec_command("npx tsc --noEmit")
|
|
252
|
+
self.execution_results['tsc'] = {'code': ret, 'output': out}
|
|
253
|
+
# Also parse these outputs
|
|
254
|
+
dyn_tsc = self._parse_tsc_content(out)
|
|
255
|
+
if dyn_tsc:
|
|
256
|
+
self.parsed_tsc_errors.extend(dyn_tsc)
|
|
257
|
+
|
|
258
|
+
if self.run_lint_cmd:
|
|
259
|
+
print("⏳ Running ESLint check...")
|
|
260
|
+
ret, out = self._exec_command("npx eslint . --format compact")
|
|
261
|
+
self.execution_results['lint'] = {'code': ret, 'output': out}
|
|
262
|
+
# Try to parse
|
|
263
|
+
dyn_eslint = []
|
|
264
|
+
for line in out.splitlines():
|
|
265
|
+
# eslint compact format: filename: line line_num, col col_num, Error - message (rule_id)
|
|
266
|
+
m = re.match(r'^([^:]+):\s*line\s*(\d+),\s*col\s*(\d+),\s*(Error|Warning)\s*-\s*(.+?)\s*\((.+)\)$', line.strip())
|
|
267
|
+
if m:
|
|
268
|
+
dyn_eslint.append({
|
|
269
|
+
'file': m.group(1).strip(),
|
|
270
|
+
'line': int(m.group(2)),
|
|
271
|
+
'col': int(m.group(3)),
|
|
272
|
+
'severity': m.group(4).lower(),
|
|
273
|
+
'message': m.group(5).strip(),
|
|
274
|
+
'rule': m.group(6)
|
|
275
|
+
})
|
|
276
|
+
if dyn_eslint:
|
|
277
|
+
self.parsed_eslint_errors.extend(dyn_eslint)
|
|
278
|
+
|
|
279
|
+
if self.run_tests_cmd:
|
|
280
|
+
print("⏳ Running unit tests...")
|
|
281
|
+
# Detect vitest
|
|
282
|
+
cmd = "npx vitest run" if self.stack['vitest'] else "npm test -- --watchAll=false"
|
|
283
|
+
ret, out = self._exec_command(cmd)
|
|
284
|
+
self.execution_results['tests'] = {'code': ret, 'output': out}
|
|
285
|
+
|
|
286
|
+
if self.run_build_cmd:
|
|
287
|
+
print("⏳ Testing application bundle build...")
|
|
288
|
+
ret, out = self._exec_command("npm run build")
|
|
289
|
+
self.execution_results['build'] = {'code': ret, 'output': out}
|
|
290
|
+
if ret != 0:
|
|
291
|
+
self.build_errors.append({
|
|
292
|
+
'source_file': 'Dynamic Build Execution',
|
|
293
|
+
'content': out[:1200]
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
def run(self) -> Dict:
|
|
297
|
+
"""Execute the full production readiness scan."""
|
|
298
|
+
print("🛡️ Production Readiness Scanner")
|
|
299
|
+
print(f"📁 Target: {self.target_path}")
|
|
300
|
+
print("=" * 60)
|
|
301
|
+
|
|
302
|
+
if not self.target_path.exists():
|
|
303
|
+
print(f"❌ Target path does not exist: {self.target_path}")
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
# Detect project stacks and load existing logs
|
|
307
|
+
self._detect_stack()
|
|
308
|
+
self._parse_logs()
|
|
309
|
+
|
|
310
|
+
# Run direct diagnostic executions if requested
|
|
311
|
+
self._run_diagnostics()
|
|
312
|
+
|
|
313
|
+
# Execute check phases
|
|
314
|
+
self._phase_1_architecture()
|
|
315
|
+
self._phase_2_frontend()
|
|
316
|
+
self._phase_3_backend()
|
|
317
|
+
self._phase_4_security()
|
|
318
|
+
self._phase_5_testing()
|
|
319
|
+
self._phase_6_performance()
|
|
320
|
+
self._phase_7_devops()
|
|
321
|
+
|
|
322
|
+
# Calculate overall score
|
|
323
|
+
self._calculate_overall_score()
|
|
324
|
+
self._generate_report()
|
|
325
|
+
self._write_markdown_report()
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
'target': str(self.target_path),
|
|
329
|
+
'overall_score': self.overall_score,
|
|
330
|
+
'stack': self.stack,
|
|
331
|
+
'phases': self.phases,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
def _init_phase(self, name: str, display_name: str) -> Dict:
|
|
335
|
+
"""Initialize a phase result structure."""
|
|
336
|
+
phase = {
|
|
337
|
+
'name': display_name,
|
|
338
|
+
'score': 100,
|
|
339
|
+
'checks': [],
|
|
340
|
+
'critical': 0,
|
|
341
|
+
'warnings': 0,
|
|
342
|
+
'passed': 0,
|
|
343
|
+
}
|
|
344
|
+
self.phases[name] = phase
|
|
345
|
+
if self.verbose:
|
|
346
|
+
print(f"\n{'─' * 40}")
|
|
347
|
+
print(f"📋 Phase: {display_name}")
|
|
348
|
+
print(f"{'─' * 40}")
|
|
349
|
+
return phase
|
|
350
|
+
|
|
351
|
+
def _check(self, phase: Dict, name: str, passed: bool, severity: str = 'warning', penalty: int = 5):
|
|
352
|
+
"""Record a check result."""
|
|
353
|
+
phase['checks'].append({
|
|
354
|
+
'name': name,
|
|
355
|
+
'passed': passed,
|
|
356
|
+
'severity': severity,
|
|
357
|
+
})
|
|
358
|
+
if passed:
|
|
359
|
+
phase['passed'] += 1
|
|
360
|
+
if self.verbose:
|
|
361
|
+
print(f" ✅ {name}")
|
|
362
|
+
else:
|
|
363
|
+
if severity == 'critical':
|
|
364
|
+
phase['critical'] += 1
|
|
365
|
+
phase['score'] -= max(penalty, 15)
|
|
366
|
+
else:
|
|
367
|
+
phase['warnings'] += 1
|
|
368
|
+
phase['score'] -= penalty
|
|
369
|
+
phase['score'] = max(0, phase['score'])
|
|
370
|
+
icon = '🔴' if severity == 'critical' else '🟡'
|
|
371
|
+
if self.verbose:
|
|
372
|
+
print(f" {icon} {name}")
|
|
373
|
+
|
|
374
|
+
def _file_exists(self, *paths: str) -> bool:
|
|
375
|
+
"""Check if any of the given file paths exist."""
|
|
376
|
+
return any((self.target_path / p).exists() for p in paths)
|
|
377
|
+
|
|
378
|
+
def _dir_exists(self, *paths: str) -> bool:
|
|
379
|
+
"""Check if any of the given directory paths exist."""
|
|
380
|
+
return any((self.target_path / p).is_dir() for p in paths)
|
|
381
|
+
|
|
382
|
+
def _file_contains(self, filepath: str, pattern: str) -> bool:
|
|
383
|
+
"""Check if a file contains a pattern."""
|
|
384
|
+
fp = self.target_path / filepath
|
|
385
|
+
if not fp.exists():
|
|
386
|
+
return False
|
|
387
|
+
try:
|
|
388
|
+
return pattern in fp.read_text(errors='ignore')
|
|
389
|
+
except (IOError, OSError):
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
def _grep_project(self, pattern: str, extensions: List[str] = None) -> List[str]:
|
|
393
|
+
"""Search for a pattern in project files."""
|
|
394
|
+
matches = []
|
|
395
|
+
exts = extensions or ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']
|
|
396
|
+
for ext in exts:
|
|
397
|
+
for f in self.target_path.rglob(f'*{ext}'):
|
|
398
|
+
if any(x in str(f) for x in ['node_modules', '.next', 'dist', '.git', 'playwright-report', 'test-results']):
|
|
399
|
+
continue
|
|
400
|
+
try:
|
|
401
|
+
content = f.read_text(errors='ignore')
|
|
402
|
+
if re.search(pattern, content):
|
|
403
|
+
matches.append(str(f.relative_to(self.target_path)))
|
|
404
|
+
except (IOError, OSError):
|
|
405
|
+
continue
|
|
406
|
+
return matches
|
|
407
|
+
|
|
408
|
+
# ─── PHASE 1: Architecture & Code Quality ───
|
|
409
|
+
|
|
410
|
+
def _phase_1_architecture(self):
|
|
411
|
+
phase = self._init_phase('architecture', 'Architecture & Code Quality')
|
|
412
|
+
|
|
413
|
+
# Structure
|
|
414
|
+
self._check(phase, 'Has organized source directory (src/ or app/)',
|
|
415
|
+
self._dir_exists('src', 'app'))
|
|
416
|
+
|
|
417
|
+
# TS Setup
|
|
418
|
+
has_tsconfig = self._file_exists('tsconfig.json')
|
|
419
|
+
strict_ts = self._file_contains('tsconfig.json', '"strict": true') or self._file_contains('tsconfig.json', '"strict":true')
|
|
420
|
+
|
|
421
|
+
# Also check tsconfig.app.json etc
|
|
422
|
+
if not strict_ts and self._file_exists('tsconfig.app.json'):
|
|
423
|
+
strict_ts = self._file_contains('tsconfig.app.json', '"strict": true') or self._file_contains('tsconfig.app.json', '"strict":true')
|
|
424
|
+
|
|
425
|
+
self._check(phase, 'TypeScript strict mode enabled', strict_ts or not self.stack['typescript'])
|
|
426
|
+
self._check(phase, 'package.json exists', self._file_exists('package.json'))
|
|
427
|
+
|
|
428
|
+
# Any types
|
|
429
|
+
any_matches = self._grep_project(r': any\b|as any\b', ['.ts', '.tsx'])
|
|
430
|
+
self._check(phase, 'No TypeScript `any` types found',
|
|
431
|
+
len(any_matches) == 0, penalty=3)
|
|
432
|
+
|
|
433
|
+
self._check(phase, '.env.example/env.example documents required config variables',
|
|
434
|
+
self._file_exists('.env.example', 'env.example', '.env.template'))
|
|
435
|
+
|
|
436
|
+
# Input validation
|
|
437
|
+
has_val = self.stack['react'] and self._file_exists('package.json')
|
|
438
|
+
if has_val:
|
|
439
|
+
try:
|
|
440
|
+
pkg = json.loads((self.target_path / 'package.json').read_text(encoding='utf-8', errors='ignore'))
|
|
441
|
+
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
|
442
|
+
has_val = any(v in deps for v in ['zod', 'joi', 'yup', 'class-validator'])
|
|
443
|
+
except Exception:
|
|
444
|
+
has_val = False
|
|
445
|
+
self._check(phase, 'Input validation library used (Zod/Joi/Yup)', has_val)
|
|
446
|
+
|
|
447
|
+
# TypeScript Compiler Check
|
|
448
|
+
tsc_ok = len(self.parsed_tsc_errors) == 0
|
|
449
|
+
penalty_val = min(5 * len(self.parsed_tsc_errors), 45)
|
|
450
|
+
self._check(phase, f'TypeScript compiles without errors ({len(self.parsed_tsc_errors)} errors)',
|
|
451
|
+
tsc_ok, severity='warning', penalty=penalty_val)
|
|
452
|
+
|
|
453
|
+
# ─── PHASE 2: Frontend Hardening ───
|
|
454
|
+
|
|
455
|
+
def _phase_2_frontend(self):
|
|
456
|
+
phase = self._init_phase('frontend', 'Frontend Hardening')
|
|
457
|
+
|
|
458
|
+
has_fe = self.stack['react'] or self._file_exists('vite.config.ts', 'next.config.js')
|
|
459
|
+
self._check(phase, 'Frontend framework detected', has_fe)
|
|
460
|
+
|
|
461
|
+
if not has_fe:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Error Boundary
|
|
465
|
+
error_files = self._grep_project(r'error\.(tsx?|jsx?)$|ErrorBoundary|react-error-boundary', ['.ts', '.tsx', '.js', '.jsx'])
|
|
466
|
+
self._check(phase, 'Error boundaries implemented', len(error_files) > 0)
|
|
467
|
+
|
|
468
|
+
# Suspense / Loading
|
|
469
|
+
loading_files = self._grep_project(r'loading\.(tsx?|jsx?)$|Suspense|Skeleton', ['.ts', '.tsx', '.js', '.jsx'])
|
|
470
|
+
self._check(phase, 'Loading skeletons or Suspense implemented', len(loading_files) > 0)
|
|
471
|
+
|
|
472
|
+
# React 19 rules / ESLint Problems
|
|
473
|
+
eslint_ok = len(self.parsed_eslint_errors) == 0
|
|
474
|
+
penalty_val = min(4 * len(self.parsed_eslint_errors), 30)
|
|
475
|
+
self._check(phase, f'ESLint passes with zero errors ({len(self.parsed_eslint_errors)} warnings/errors)',
|
|
476
|
+
eslint_ok, severity='warning', penalty=penalty_val)
|
|
477
|
+
|
|
478
|
+
# Check React Hook calls (hook violations)
|
|
479
|
+
hook_violations = [e for e in self.parsed_eslint_errors if 'rules-of-hooks' in e.get('rule', '')]
|
|
480
|
+
self._check(phase, 'No React Hook rule violations detected',
|
|
481
|
+
len(hook_violations) == 0, severity='critical', penalty=15)
|
|
482
|
+
|
|
483
|
+
# ─── PHASE 3: Backend Hardening ───
|
|
484
|
+
|
|
485
|
+
def _phase_3_backend(self):
|
|
486
|
+
phase = self._init_phase('backend', 'Backend Hardening')
|
|
487
|
+
|
|
488
|
+
# Database migrations
|
|
489
|
+
has_mig = self._dir_exists('supabase/migrations', 'migrations', 'prisma/migrations', 'drizzle')
|
|
490
|
+
self._check(phase, 'Database migrations directory setup', has_mig)
|
|
491
|
+
|
|
492
|
+
# Health endpoint / check
|
|
493
|
+
health_check = self._file_exists('public/robots.txt') # placeholder or real
|
|
494
|
+
self._check(phase, 'Public routing endpoints ready', health_check)
|
|
495
|
+
|
|
496
|
+
# Supabase specific checks
|
|
497
|
+
if self.stack['supabase']:
|
|
498
|
+
has_sb_types = self._file_exists('src/types/supabase.ts') or self._file_exists('src/lib/supabase.ts')
|
|
499
|
+
self._check(phase, 'Supabase TypeScript schema types generated (supabase.ts)', has_sb_types)
|
|
500
|
+
|
|
501
|
+
# Check local supabase config
|
|
502
|
+
has_sb_config = self._file_exists('supabase/config.toml')
|
|
503
|
+
self._check(phase, 'Supabase config.toml set up', has_sb_config, penalty=3)
|
|
504
|
+
|
|
505
|
+
# ─── PHASE 4: Security Hardening ───
|
|
506
|
+
|
|
507
|
+
def _phase_4_security(self):
|
|
508
|
+
phase = self._init_phase('security', 'Security Hardening')
|
|
509
|
+
|
|
510
|
+
# .gitignore env
|
|
511
|
+
gi = self.target_path / '.gitignore'
|
|
512
|
+
env_ig = False
|
|
513
|
+
if gi.exists():
|
|
514
|
+
content = gi.read_text(encoding='utf-8', errors='ignore')
|
|
515
|
+
env_ig = '.env' in content
|
|
516
|
+
self._check(phase, '.env included in .gitignore', env_ig, severity='critical', penalty=20)
|
|
517
|
+
|
|
518
|
+
# Check raw env files for secrets
|
|
519
|
+
secrets_exposed = False
|
|
520
|
+
env_files = ['.env', '.env.local', '.env.production']
|
|
521
|
+
for ef in env_files:
|
|
522
|
+
ef_path = self.target_path / ef
|
|
523
|
+
if ef_path.exists():
|
|
524
|
+
# Just flag if .env is tracked in git (which gitignore audit checks)
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
# Check for hardcoded private keys/secrets in files
|
|
528
|
+
matches = self._grep_project(r'sk_live_[a-zA-Z0-9]{20,}|sk_test_[a-zA-Z0-9]{20,}|service_role.*["\'][a-zA-Z0-9_]{30,}', ['.ts', '.tsx', '.js', '.py'])
|
|
529
|
+
self._check(phase, 'No hardcoded private API keys/secrets in project files', len(matches) == 0, severity='critical', penalty=25)
|
|
530
|
+
|
|
531
|
+
# SQL Injection / parameterized queries check
|
|
532
|
+
raw_sqli = self._grep_project(r'\.query\(\s*`[^\$]*\$\{', ['.ts', '.js'])
|
|
533
|
+
self._check(phase, 'No vulnerable string-interpolated SQL queries found', len(raw_sqli) == 0, severity='critical', penalty=20)
|
|
534
|
+
|
|
535
|
+
# CORS Check
|
|
536
|
+
cors_wildcard = self._grep_project(r'cors:\s*["\']\*["\']|Access-Control-Allow-Origin.*\*', ['.ts', '.js'])
|
|
537
|
+
self._check(phase, 'No unrestricted CORS wildcard access allowed in source', len(cors_wildcard) == 0, severity='warning', penalty=10)
|
|
538
|
+
|
|
539
|
+
# ─── PHASE 5: Testing & QA ───
|
|
540
|
+
|
|
541
|
+
def _phase_5_testing(self):
|
|
542
|
+
phase = self._init_phase('testing', 'Testing & Quality Assurance')
|
|
543
|
+
|
|
544
|
+
# Unit tests
|
|
545
|
+
has_ut = self._file_exists('vitest.config.ts', 'jest.config.js') or self._dir_exists('tests', '__tests__')
|
|
546
|
+
self._check(phase, 'Unit test suite configured (Vitest/Jest)', has_ut)
|
|
547
|
+
|
|
548
|
+
# E2E test
|
|
549
|
+
has_e2e = self._file_exists('playwright.config.ts') or self._dir_exists('e2e')
|
|
550
|
+
self._check(phase, 'E2E test suite configured (Playwright)', has_e2e)
|
|
551
|
+
|
|
552
|
+
# Eslint config file
|
|
553
|
+
has_eslint_cfg = self._file_exists('eslint.config.js', '.eslintrc.js', '.eslintrc.json')
|
|
554
|
+
self._check(phase, 'Linter configuration exists (eslint.config.js)', has_eslint_cfg)
|
|
555
|
+
|
|
556
|
+
# Pre-commit config
|
|
557
|
+
has_hooks = self._file_exists('.husky/pre-commit', '.pre-commit-config.yaml')
|
|
558
|
+
self._check(phase, 'Git hooks config present', has_hooks, penalty=3)
|
|
559
|
+
|
|
560
|
+
# ─── PHASE 6: Performance & SEO ───
|
|
561
|
+
|
|
562
|
+
def _phase_6_performance(self):
|
|
563
|
+
phase = self._init_phase('performance', 'Performance & SEO')
|
|
564
|
+
|
|
565
|
+
# Sitemap
|
|
566
|
+
has_sitemap = self._file_exists('public/sitemap.xml') or self._file_exists('src/sitemap.ts')
|
|
567
|
+
self._check(phase, 'Sitemap exists or generated', has_sitemap, penalty=3)
|
|
568
|
+
|
|
569
|
+
# robots.txt
|
|
570
|
+
self._check(phase, 'robots.txt exists in public directory', self._file_exists('public/robots.txt'))
|
|
571
|
+
|
|
572
|
+
# Analytics
|
|
573
|
+
self._check(phase, 'User speed & web vitals tracking integrated', self.stack['analytics'], penalty=3)
|
|
574
|
+
|
|
575
|
+
# PWA
|
|
576
|
+
self._check(phase, 'Progressive Web App (PWA) configuration present', self.stack['pwa'], penalty=2)
|
|
577
|
+
|
|
578
|
+
# ─── PHASE 7: DevOps & Deployment ───
|
|
579
|
+
|
|
580
|
+
def _phase_7_devops(self):
|
|
581
|
+
phase = self._init_phase('devops', 'DevOps & Deployment')
|
|
582
|
+
|
|
583
|
+
# GitHub Workflows
|
|
584
|
+
has_ci = self._dir_exists('.github/workflows')
|
|
585
|
+
self._check(phase, 'GitHub Actions CI/CD workflows setup', has_ci)
|
|
586
|
+
|
|
587
|
+
# Vercel or hosting configuration
|
|
588
|
+
has_deploy_config = self._file_exists('vercel.json', 'netlify.toml', 'Dockerfile')
|
|
589
|
+
self._check(phase, 'Deployment configuration file exists', has_deploy_config)
|
|
590
|
+
|
|
591
|
+
# Documentation completeness
|
|
592
|
+
self._check(phase, 'README.md contains instructions', self._file_exists('README.md'))
|
|
593
|
+
self._check(phase, 'CHANGELOG.md updated for release', self._file_exists('CHANGELOG.md'), penalty=2)
|
|
594
|
+
|
|
595
|
+
# Build errors penalty
|
|
596
|
+
has_build_errs = len(self.build_errors) > 0
|
|
597
|
+
self._check(phase, 'Vite/Compiler build executes successfully (No build_errors.txt)',
|
|
598
|
+
not has_build_errs, severity='critical', penalty=50)
|
|
599
|
+
|
|
600
|
+
# ─── Scoring & Reporting ───
|
|
601
|
+
|
|
602
|
+
def _calculate_overall_score(self):
|
|
603
|
+
"""Calculate weighted overall score."""
|
|
604
|
+
total = 0
|
|
605
|
+
for phase_key, weight in self.PHASE_WEIGHTS.items():
|
|
606
|
+
if phase_key in self.phases:
|
|
607
|
+
total += self.phases[phase_key]['score'] * weight
|
|
608
|
+
self.overall_score = round(total)
|
|
609
|
+
|
|
610
|
+
def _get_grade(self, score: int) -> str:
|
|
611
|
+
"""Convert score to letter grade."""
|
|
612
|
+
if score >= 95: return 'A+'
|
|
613
|
+
elif score >= 90: return 'A'
|
|
614
|
+
elif score >= 85: return 'B+'
|
|
615
|
+
elif score >= 80: return 'B'
|
|
616
|
+
elif score >= 70: return 'C'
|
|
617
|
+
elif score >= 60: return 'D'
|
|
618
|
+
else: return 'F'
|
|
619
|
+
|
|
620
|
+
def _generate_report(self):
|
|
621
|
+
"""Print the final production readiness report to console."""
|
|
622
|
+
grade = self._get_grade(self.overall_score)
|
|
623
|
+
total_critical = sum(p['critical'] for p in self.phases.values())
|
|
624
|
+
total_warnings = sum(p['warnings'] for p in self.phases.values())
|
|
625
|
+
total_passed = sum(p['passed'] for p in self.phases.values())
|
|
626
|
+
|
|
627
|
+
print(f"\n{'=' * 60}")
|
|
628
|
+
print("🛡️ PRODUCTION READINESS REPORT")
|
|
629
|
+
print(f"{'=' * 60}")
|
|
630
|
+
print(f"\n Overall Score: {self.overall_score}/100 ({grade})")
|
|
631
|
+
print(f" Passed: {total_passed} | Warnings: {total_warnings} | Critical: {total_critical}")
|
|
632
|
+
|
|
633
|
+
if self.overall_score >= 95:
|
|
634
|
+
print(" Status: ✅ PRODUCTION READY")
|
|
635
|
+
elif self.overall_score >= 80:
|
|
636
|
+
print(" Status: ⚠️ NEEDS MINOR FIXES")
|
|
637
|
+
elif self.overall_score >= 60:
|
|
638
|
+
print(" Status: 🟡 SIGNIFICANT ISSUES")
|
|
639
|
+
else:
|
|
640
|
+
print(" Status: 🔴 NOT PRODUCTION READY")
|
|
641
|
+
|
|
642
|
+
print(f"\n{'─' * 60}")
|
|
643
|
+
print(" Phase Breakdown:")
|
|
644
|
+
print(f"{'─' * 60}")
|
|
645
|
+
print(f" {'Phase':<35} {'Score':>6} {'Critical':>9} {'Warnings':>9}")
|
|
646
|
+
print(f" {'─' * 59}")
|
|
647
|
+
|
|
648
|
+
for phase_key in self.PHASE_WEIGHTS:
|
|
649
|
+
if phase_key in self.phases:
|
|
650
|
+
p = self.phases[phase_key]
|
|
651
|
+
icon = '✅' if p['score'] >= 90 else ('🟡' if p['score'] >= 70 else '🔴')
|
|
652
|
+
print(f" {icon} {p['name']:<32} {p['score']:>4}/100 {p['critical']:>6} {p['warnings']:>6}")
|
|
653
|
+
|
|
654
|
+
# List critical issues
|
|
655
|
+
critical_issues = []
|
|
656
|
+
for phase_key, phase in self.phases.items():
|
|
657
|
+
for check in phase['checks']:
|
|
658
|
+
if not check['passed'] and check['severity'] == 'critical':
|
|
659
|
+
critical_issues.append((phase['name'], check['name']))
|
|
660
|
+
|
|
661
|
+
if critical_issues:
|
|
662
|
+
print(f"\n{'─' * 60}")
|
|
663
|
+
print(" 🔴 CRITICAL ISSUES (Must Fix Before Production):")
|
|
664
|
+
print(f"{'─' * 60}")
|
|
665
|
+
for phase_name, issue in critical_issues:
|
|
666
|
+
print(f" [{phase_name}] {issue}")
|
|
667
|
+
|
|
668
|
+
print(f"\n{'=' * 60}\n")
|
|
669
|
+
|
|
670
|
+
def _write_markdown_report(self):
|
|
671
|
+
"""Generate and save PRODUCTION_READINESS_REPORT.md in target project."""
|
|
672
|
+
report_path = self.target_path / 'PRODUCTION_READINESS_REPORT.md'
|
|
673
|
+
grade = self._get_grade(self.overall_score)
|
|
674
|
+
|
|
675
|
+
status_text = "✅ PRODUCTION READY" if self.overall_score >= 95 else \
|
|
676
|
+
("⚠️ NEEDS MINOR FIXES" if self.overall_score >= 80 else \
|
|
677
|
+
("🟡 SIGNIFICANT ISSUES" if self.overall_score >= 60 else "🔴 NOT PRODUCTION READY"))
|
|
678
|
+
|
|
679
|
+
md = []
|
|
680
|
+
md.append(f"# Production Readiness Report")
|
|
681
|
+
md.append(f"**Target:** `{self.target_path}` ")
|
|
682
|
+
md.append(f"**Date Check:** {os.environ.get('DATE_STRING', 'Current Date')} ")
|
|
683
|
+
md.append(f"**Overall Score:** `{self.overall_score}/100` (`{grade}`) ")
|
|
684
|
+
md.append(f"**Status:** {status_text}\n")
|
|
685
|
+
|
|
686
|
+
md.append("## Executive Summary")
|
|
687
|
+
md.append("This report lists potential security vulnerabilities, code architecture errors, test coverage status, and build checks before release. Critical errors must be addressed immediately.")
|
|
688
|
+
|
|
689
|
+
# Phase score table
|
|
690
|
+
md.append("## Phase Breakdown\n")
|
|
691
|
+
md.append("| Phase | Score | Critical | Warnings |")
|
|
692
|
+
md.append("|:---|:---:|:---:|:---:|")
|
|
693
|
+
for phase_key in self.PHASE_WEIGHTS:
|
|
694
|
+
if phase_key in self.phases:
|
|
695
|
+
p = self.phases[phase_key]
|
|
696
|
+
md.append(f"| {p['name']} | `{p['score']}/100` | {p['critical']} | {p['warnings']} |")
|
|
697
|
+
md.append("")
|
|
698
|
+
|
|
699
|
+
# Helper to format clickable links
|
|
700
|
+
def make_link(filepath: str, line: int = None) -> str:
|
|
701
|
+
# Normalize path
|
|
702
|
+
fp_norm = filepath.replace('\\', '/')
|
|
703
|
+
full_abs = (self.target_path / fp_norm).resolve().as_posix()
|
|
704
|
+
link_text = f"{filepath}:{line}" if line else filepath
|
|
705
|
+
href = f"file:///{full_abs}"
|
|
706
|
+
if line:
|
|
707
|
+
href += f"#L{line}"
|
|
708
|
+
return f"[{link_text}]({href})"
|
|
709
|
+
|
|
710
|
+
# Dynamic Log results
|
|
711
|
+
if self.parsed_tsc_errors:
|
|
712
|
+
md.append("## 🔴 TypeScript Compiler Errors")
|
|
713
|
+
md.append(f"The following **{len(self.parsed_tsc_errors)} TypeScript errors** were parsed from logs or dynamic execution. Resolving these is highly recommended to prevent production runtime errors.\n")
|
|
714
|
+
md.append("| File & Line | Code | Message |")
|
|
715
|
+
md.append("|:---|:---:|:---|")
|
|
716
|
+
for err in self.parsed_tsc_errors[:30]: # Limit top 30
|
|
717
|
+
md.append(f"| {make_link(err['file'], err['line'])} | `TS{err['code']}` | `{err['message'].replace('|', 'I')}` |")
|
|
718
|
+
if len(self.parsed_tsc_errors) > 30:
|
|
719
|
+
md.append(f"| *and {len(self.parsed_tsc_errors) - 30} more errors...* | | |")
|
|
720
|
+
md.append("\n")
|
|
721
|
+
|
|
722
|
+
if self.parsed_eslint_errors:
|
|
723
|
+
md.append("## 🟡 ESLint Warnings & Errors")
|
|
724
|
+
md.append(f"Detected **{len(self.parsed_eslint_errors)} problems** in coding rules and hook order requirements.\n")
|
|
725
|
+
md.append("| File & Line | Severity | Message | Rule |")
|
|
726
|
+
md.append("|:---|:---:|:---|:---:|")
|
|
727
|
+
for err in self.parsed_eslint_errors[:30]:
|
|
728
|
+
sev_icon = "🔴 error" if err['severity'].lower() == 'error' else "🟡 warning"
|
|
729
|
+
md.append(f"| {make_link(err['file'], err['line'])} | {sev_icon} | `{err['message'].replace('|', 'I')}` | `{err['rule']}` |")
|
|
730
|
+
if len(self.parsed_eslint_errors) > 30:
|
|
731
|
+
md.append(f"| *and {len(self.parsed_eslint_errors) - 30} more warnings...* | | | |")
|
|
732
|
+
md.append("\n")
|
|
733
|
+
|
|
734
|
+
if self.build_errors:
|
|
735
|
+
md.append("## 🔴 Vite / Compiler Build Failures")
|
|
736
|
+
md.append("Application bundle builds have failed. You must repair these prior to release.\n")
|
|
737
|
+
for err in self.build_errors:
|
|
738
|
+
md.append(f"### Log Source: {err['source_file']}")
|
|
739
|
+
md.append("```text")
|
|
740
|
+
md.append(err['content'])
|
|
741
|
+
md.append("```\n")
|
|
742
|
+
|
|
743
|
+
# Specific checklist results
|
|
744
|
+
md.append("## Detailed Checklist Items\n")
|
|
745
|
+
for phase_key, p in self.phases.items():
|
|
746
|
+
md.append(f"### {p['name']}")
|
|
747
|
+
for check in p['checks']:
|
|
748
|
+
status = "✅ Passed" if check['passed'] else f"❌ Failed ({check['severity'].upper()})"
|
|
749
|
+
md.append(f"- **{check['name']}**: {status}")
|
|
750
|
+
md.append("")
|
|
751
|
+
|
|
752
|
+
# Remediation section
|
|
753
|
+
md.append("## 🚀 Quick Remediation Checklist")
|
|
754
|
+
md.append("Below are instructions to address the most urgent failures detected:\n")
|
|
755
|
+
|
|
756
|
+
remediation_items = 0
|
|
757
|
+
|
|
758
|
+
# Check hooks called conditionally
|
|
759
|
+
hook_errs = [e for e in self.parsed_eslint_errors if 'rules-of-hooks' in e.get('rule', '')]
|
|
760
|
+
if hook_errs:
|
|
761
|
+
remediation_items += 1
|
|
762
|
+
md.append(f"### 1. Fix React Hook Rules in {make_link(hook_errs[0]['file'], hook_errs[0]['line'])}")
|
|
763
|
+
md.append("React Hooks cannot be called conditionally. Ensure `usePWAInstall` or `useState` calls are moved to the top level of component rendering.")
|
|
764
|
+
md.append("```typescript")
|
|
765
|
+
md.append("// Move inside component but BEFORE any conditional returns or if-blocks:")
|
|
766
|
+
md.append("const installState = usePWAInstall();")
|
|
767
|
+
md.append("```\n")
|
|
768
|
+
|
|
769
|
+
# Check motion variants assignment
|
|
770
|
+
motion_errs = [e for e in self.parsed_tsc_errors if 'Variants' in e['message'] and 'BusinessAnalysisTab' in e['file']]
|
|
771
|
+
if motion_errs:
|
|
772
|
+
remediation_items += 1
|
|
773
|
+
md.append(f"### 2. Fix Framer Motion Types in {make_link(motion_errs[0]['file'], motion_errs[0]['line'])}")
|
|
774
|
+
md.append("In Framer Motion variants, setting `type` as generic string causes type assignment failures on type `Variants`. Set it as a string literal or cast it:")
|
|
775
|
+
md.append("```typescript")
|
|
776
|
+
md.append("// Change from:")
|
|
777
|
+
md.append("transition: { type: 'spring', ... }")
|
|
778
|
+
md.append("// To literal type casting or let variants behave automatically:")
|
|
779
|
+
md.append("transition: { type: 'spring' as const, ... }")
|
|
780
|
+
md.append("```\n")
|
|
781
|
+
|
|
782
|
+
if self.stack['supabase'] and not self._file_exists('src/types/supabase.ts'):
|
|
783
|
+
remediation_items += 1
|
|
784
|
+
md.append("### 3. Generate Supabase Schema Types")
|
|
785
|
+
md.append("Execute the supabase schema type exporter script:")
|
|
786
|
+
md.append("```bash")
|
|
787
|
+
md.append("npm run supabase:types")
|
|
788
|
+
md.append("```\n")
|
|
789
|
+
|
|
790
|
+
if not remediation_items:
|
|
791
|
+
md.append("- No immediate high-priority remediation templates needed. Fix individual warnings listed above.")
|
|
792
|
+
|
|
793
|
+
# Save MD
|
|
794
|
+
try:
|
|
795
|
+
report_path.write_text("\n".join(md), encoding='utf-8')
|
|
796
|
+
print(f"📝 Markdown report generated and saved: {report_path}")
|
|
797
|
+
except Exception as e:
|
|
798
|
+
print(f"❌ Failed to write markdown report: {str(e)}")
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def main():
|
|
802
|
+
"""Main entry point."""
|
|
803
|
+
if sys.platform.startswith('win'):
|
|
804
|
+
try:
|
|
805
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
806
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
807
|
+
except Exception:
|
|
808
|
+
pass
|
|
809
|
+
parser = argparse.ArgumentParser(
|
|
810
|
+
description="Production Readiness Scanner — comprehensive pre-production audit"
|
|
811
|
+
)
|
|
812
|
+
parser.add_argument(
|
|
813
|
+
'target',
|
|
814
|
+
help='Target project path to scan'
|
|
815
|
+
)
|
|
816
|
+
parser.add_argument(
|
|
817
|
+
'--verbose', '-v',
|
|
818
|
+
action='store_true',
|
|
819
|
+
help='Enable verbose output (show all checks)'
|
|
820
|
+
)
|
|
821
|
+
parser.add_argument(
|
|
822
|
+
'--json',
|
|
823
|
+
action='store_true',
|
|
824
|
+
help='Output results as JSON'
|
|
825
|
+
)
|
|
826
|
+
parser.add_argument(
|
|
827
|
+
'--output', '-o',
|
|
828
|
+
help='Output file path'
|
|
829
|
+
)
|
|
830
|
+
parser.add_argument(
|
|
831
|
+
'--run-tsc',
|
|
832
|
+
action='store_true',
|
|
833
|
+
help='Execute typescript compiler typechecking'
|
|
834
|
+
)
|
|
835
|
+
parser.add_argument(
|
|
836
|
+
'--run-lint',
|
|
837
|
+
action='store_true',
|
|
838
|
+
help='Execute eslint check'
|
|
839
|
+
)
|
|
840
|
+
parser.add_argument(
|
|
841
|
+
'--run-tests',
|
|
842
|
+
action='store_true',
|
|
843
|
+
help='Execute unit tests'
|
|
844
|
+
)
|
|
845
|
+
parser.add_argument(
|
|
846
|
+
'--run-build',
|
|
847
|
+
action='store_true',
|
|
848
|
+
help='Execute vite build compilation test'
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
args = parser.parse_args()
|
|
852
|
+
|
|
853
|
+
scanner = ProductionReadinessScanner(
|
|
854
|
+
args.target,
|
|
855
|
+
verbose=args.verbose,
|
|
856
|
+
run_tsc=args.run_tsc,
|
|
857
|
+
run_lint=args.run_lint,
|
|
858
|
+
run_tests=args.run_tests,
|
|
859
|
+
run_build=args.run_build
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
results = scanner.run()
|
|
863
|
+
|
|
864
|
+
if args.json:
|
|
865
|
+
output = json.dumps(results, indent=2, default=str)
|
|
866
|
+
if args.output:
|
|
867
|
+
with open(args.output, 'w', encoding='utf-8') as f:
|
|
868
|
+
f.write(output)
|
|
869
|
+
print(f"Results written to {args.output}")
|
|
870
|
+
else:
|
|
871
|
+
print(output)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
if __name__ == '__main__':
|
|
875
|
+
main()
|