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