@vibecheckai/cli 3.1.2 → 3.1.4

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 (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -0,0 +1,2452 @@
1
+ /**
2
+ * vibecheck polish - Production Polish Analyzer
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * Finds all the small detailed things you forgot - the polish that makes
6
+ * projects production-ready.
7
+ * ═══════════════════════════════════════════════════════════════════════════════
8
+ *
9
+ * Categories:
10
+ * - Frontend: Error boundaries, 404 pages, loading states, empty states
11
+ * - Backend: Health endpoints, graceful shutdown, rate limiting, validation
12
+ * - Security: HTTPS, CORS, CSP, secrets management, auth headers
13
+ * - Performance: Caching, compression, lazy loading, bundle optimization
14
+ * - Accessibility: ARIA labels, focus management, color contrast, keyboard nav
15
+ * - SEO: Meta tags, sitemap, robots.txt, Open Graph, structured data
16
+ * - Configuration: Environment variables, feature flags, logging
17
+ * - Documentation: README, API docs, CHANGELOG, contributing guide
18
+ * - Infrastructure: Docker, CI/CD, monitoring, backups
19
+ */
20
+
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+
24
+ // ═══════════════════════════════════════════════════════════════════════════════
25
+ // TERMINAL STYLING
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+
28
+ const c = {
29
+ reset: '\x1b[0m',
30
+ bold: '\x1b[1m',
31
+ dim: '\x1b[2m',
32
+ italic: '\x1b[3m',
33
+ underline: '\x1b[4m',
34
+ red: '\x1b[31m',
35
+ green: '\x1b[32m',
36
+ yellow: '\x1b[33m',
37
+ blue: '\x1b[34m',
38
+ magenta: '\x1b[35m',
39
+ cyan: '\x1b[36m',
40
+ white: '\x1b[37m',
41
+ bgRed: '\x1b[41m',
42
+ bgGreen: '\x1b[42m',
43
+ bgYellow: '\x1b[43m',
44
+ bgBlue: '\x1b[44m',
45
+ };
46
+
47
+ const icons = {
48
+ critical: '🔴',
49
+ high: '🟠',
50
+ medium: '🟡',
51
+ low: '🔵',
52
+ check: '✓',
53
+ cross: '✗',
54
+ arrow: '→',
55
+ star: '★',
56
+ sparkle: '✨',
57
+ wrench: '🔧',
58
+ rocket: '🚀',
59
+ shield: '🛡️',
60
+ lightning: '⚡',
61
+ eye: '👁️',
62
+ search: '🔍',
63
+ book: '📖',
64
+ gear: '⚙️',
65
+ server: '🖥️',
66
+ lock: '🔒',
67
+ };
68
+
69
+ // ═══════════════════════════════════════════════════════════════════════════════
70
+ // UTILITY FUNCTIONS
71
+ // ═══════════════════════════════════════════════════════════════════════════════
72
+
73
+ async function pathExists(filePath) {
74
+ try {
75
+ await fs.promises.access(filePath);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ async function findFile(dir, pattern, maxDepth = 5, currentDepth = 0) {
83
+ if (currentDepth >= maxDepth) return null;
84
+
85
+ try {
86
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
87
+
88
+ for (const entry of entries) {
89
+ // Skip node_modules, .git, dist, etc.
90
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') {
91
+ continue;
92
+ }
93
+
94
+ const fullPath = path.join(dir, entry.name);
95
+
96
+ if (entry.isDirectory()) {
97
+ const found = await findFile(fullPath, pattern, maxDepth, currentDepth + 1);
98
+ if (found) return found;
99
+ } else if (pattern.test(entry.name)) {
100
+ return fullPath;
101
+ }
102
+ }
103
+ return null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ async function readFileSafe(filePath) {
110
+ try {
111
+ return await fs.promises.readFile(filePath, 'utf8');
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ async function fileContains(filePath, pattern) {
118
+ const content = await readFileSafe(filePath);
119
+ if (!content) return false;
120
+ return pattern.test(content);
121
+ }
122
+
123
+ async function findAllFiles(dir, pattern, maxDepth = 5, currentDepth = 0) {
124
+ if (currentDepth >= maxDepth) return [];
125
+ const results = [];
126
+
127
+ try {
128
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
129
+
130
+ for (const entry of entries) {
131
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') {
132
+ continue;
133
+ }
134
+
135
+ const fullPath = path.join(dir, entry.name);
136
+
137
+ if (entry.isDirectory()) {
138
+ const found = await findAllFiles(fullPath, pattern, maxDepth, currentDepth + 1);
139
+ results.push(...found);
140
+ } else if (pattern.test(entry.name)) {
141
+ results.push(fullPath);
142
+ }
143
+ }
144
+ } catch {
145
+ // Ignore errors
146
+ }
147
+
148
+ return results;
149
+ }
150
+
151
+ // ═══════════════════════════════════════════════════════════════════════════════
152
+ // POLISH CHECKERS
153
+ // ═══════════════════════════════════════════════════════════════════════════════
154
+
155
+ const checkers = {
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // FRONTEND CHECKER - Comprehensive UX/UI Polish Analysis
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+ async frontend(projectPath) {
160
+ const issues = [];
161
+ const srcPath = path.join(projectPath, 'src');
162
+ const appPath = path.join(projectPath, 'app'); // Next.js app router
163
+ const pagesPath = path.join(projectPath, 'pages'); // Next.js pages router
164
+ const componentsPath = path.join(srcPath, 'components');
165
+
166
+ const hasSrc = await pathExists(srcPath);
167
+ const hasApp = await pathExists(appPath);
168
+ const hasPages = await pathExists(pagesPath);
169
+
170
+ if (!hasSrc && !hasApp && !hasPages) return issues;
171
+
172
+ const searchPath = hasSrc ? srcPath : (hasApp ? appPath : pagesPath);
173
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+ // ERROR HANDLING & BOUNDARIES
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+
179
+ const hasErrorBoundary = await findFile(searchPath, /ErrorBoundary|error\.(tsx?|jsx?)$/i);
180
+ if (!hasErrorBoundary) {
181
+ issues.push({
182
+ id: 'missing-error-boundary',
183
+ category: 'Frontend',
184
+ severity: 'critical',
185
+ title: 'Missing Error Boundary',
186
+ description: 'No error boundary component found. React errors will crash the entire app.',
187
+ suggestion: 'Add ErrorBoundary component to catch and handle React errors gracefully.',
188
+ autoFixable: true,
189
+ aiPrompt: `Create a reusable ErrorBoundary component for React/Next.js that:
190
+ 1. Catches JavaScript errors anywhere in the child component tree
191
+ 2. Logs errors to console and optionally to an error tracking service
192
+ 3. Displays a user-friendly fallback UI with:
193
+ - A friendly error message
194
+ - A "Try Again" button that resets the error state
195
+ - Option to report the error
196
+ 4. Supports custom fallback UI via props
197
+ 5. Uses TypeScript with proper typing
198
+ 6. Includes a useErrorBoundary hook for functional components
199
+ 7. Has smooth fade-in animation for the fallback UI
200
+
201
+ Place it in src/components/ErrorBoundary.tsx`,
202
+ });
203
+ }
204
+
205
+ const has404 = await findFile(searchPath, /not-found|NotFound|404/i);
206
+ if (!has404) {
207
+ issues.push({
208
+ id: 'missing-404',
209
+ category: 'Frontend',
210
+ severity: 'medium',
211
+ title: 'Missing 404 Page',
212
+ description: 'No custom 404/NotFound page found. Users will see default browser error.',
213
+ suggestion: 'Add a custom 404 page with navigation options.',
214
+ autoFixable: true,
215
+ aiPrompt: `Create a beautiful, user-friendly 404 Not Found page that:
216
+ 1. Has a visually appealing design with subtle animations
217
+ 2. Shows a clear "Page Not Found" message
218
+ 3. Includes helpful navigation options:
219
+ - Go back to previous page button
220
+ - Go to homepage button
221
+ - Search functionality (if applicable)
222
+ - Popular/suggested links
223
+ 4. Has a fun illustration or animation (CSS-based, no external images)
224
+ 5. Is fully responsive for mobile
225
+ 6. Maintains the site's design language
226
+ 7. Includes proper meta tags for SEO
227
+
228
+ For Next.js App Router: Create app/not-found.tsx
229
+ For Pages Router: Create pages/404.tsx`,
230
+ });
231
+ }
232
+
233
+ // ═══════════════════════════════════════════════════════════════════════════
234
+ // LOADING STATES & SKELETONS
235
+ // ═══════════════════════════════════════════════════════════════════════════
236
+
237
+ const hasLoadingSpinner = await findFile(searchPath, /Spinner|LoadingSpinner/i);
238
+ if (!hasLoadingSpinner) {
239
+ issues.push({
240
+ id: 'missing-spinner',
241
+ category: 'Frontend',
242
+ severity: 'high',
243
+ title: 'Missing Loading Spinner',
244
+ description: 'No spinner component found. Users need visual feedback during loading.',
245
+ suggestion: 'Add a reusable Spinner component with multiple sizes and variants.',
246
+ autoFixable: true,
247
+ aiPrompt: `Create a modern, reusable Spinner/Loading component that:
248
+ 1. Has multiple size variants: sm, md, lg, xl
249
+ 2. Supports custom colors via props or CSS variables
250
+ 3. Has multiple styles: circular spinner, dots, pulse, bars
251
+ 4. Uses pure CSS animations (no external dependencies)
252
+ 5. Is accessible with proper ARIA attributes (role="status", aria-label)
253
+ 6. Supports dark/light mode automatically
254
+ 7. Can be used inline or as an overlay
255
+ 8. Has smooth entrance/exit animations
256
+ 9. TypeScript with proper prop types
257
+
258
+ Create: src/components/ui/Spinner.tsx and src/components/ui/Spinner.css`,
259
+ });
260
+ }
261
+
262
+ const hasSkeleton = await findFile(searchPath, /Skeleton|Shimmer|Placeholder/i);
263
+ if (!hasSkeleton) {
264
+ issues.push({
265
+ id: 'missing-skeleton',
266
+ category: 'Frontend',
267
+ severity: 'high',
268
+ title: 'Missing Skeleton Loaders',
269
+ description: 'No skeleton/shimmer components found. Skeleton loaders provide better perceived performance than spinners.',
270
+ suggestion: 'Add skeleton components for content placeholders.',
271
+ autoFixable: true,
272
+ aiPrompt: `Create a comprehensive Skeleton loading system with:
273
+
274
+ 1. Base Skeleton component with:
275
+ - Shimmer animation effect
276
+ - Multiple variants: text, circular, rectangular, rounded
277
+ - Customizable width, height, border-radius
278
+ - Animation that flows left-to-right with gradient
279
+
280
+ 2. Pre-built skeleton patterns:
281
+ - SkeletonText (paragraph lines with varying widths)
282
+ - SkeletonAvatar (circular profile picture placeholder)
283
+ - SkeletonCard (card layout placeholder)
284
+ - SkeletonTable (table rows placeholder)
285
+ - SkeletonList (list items placeholder)
286
+ - SkeletonImage (image placeholder with aspect ratio)
287
+
288
+ 3. Features:
289
+ - CSS-only animations (performant)
290
+ - Dark/light mode support
291
+ - Customizable animation speed
292
+ - Can disable animation for reduced motion preference
293
+ - TypeScript with proper types
294
+
295
+ Create: src/components/ui/Skeleton.tsx with all variants`,
296
+ });
297
+ }
298
+
299
+ const hasLoadingPage = await findFile(searchPath, /loading\.(tsx?|jsx?)$/i);
300
+ if (!hasLoadingPage && hasApp) {
301
+ issues.push({
302
+ id: 'missing-loading-page',
303
+ category: 'Frontend',
304
+ severity: 'medium',
305
+ title: 'Missing Next.js Loading State',
306
+ description: 'No loading.tsx found. Next.js App Router supports automatic loading states.',
307
+ suggestion: 'Add loading.tsx files for route segments.',
308
+ autoFixable: true,
309
+ aiPrompt: `Create a loading.tsx file for Next.js App Router that:
310
+ 1. Shows a full-page skeleton loader matching your layout
311
+ 2. Uses your Skeleton components for consistency
312
+ 3. Includes a subtle progress indicator
313
+ 4. Maintains layout structure to prevent layout shift
314
+ 5. Has smooth fade-out when content loads
315
+
316
+ Create: app/loading.tsx (root level) and consider adding to major route segments`,
317
+ });
318
+ }
319
+
320
+ // ═══════════════════════════════════════════════════════════════════════════
321
+ // EMPTY STATES & FEEDBACK
322
+ // ═══════════════════════════════════════════════════════════════════════════
323
+
324
+ const hasEmptyState = await findFile(searchPath, /EmptyState|Empty|NoData|NoResults/i);
325
+ if (!hasEmptyState) {
326
+ issues.push({
327
+ id: 'missing-empty-states',
328
+ category: 'Frontend',
329
+ severity: 'medium',
330
+ title: 'Missing Empty States',
331
+ description: 'No empty state components found. Users need helpful messages when data is empty.',
332
+ suggestion: 'Add EmptyState components for lists and data displays.',
333
+ autoFixable: true,
334
+ aiPrompt: `Create a versatile EmptyState component system with:
335
+
336
+ 1. Base EmptyState component with:
337
+ - Icon slot (customizable)
338
+ - Title and description
339
+ - Action button(s)
340
+ - Illustration option
341
+
342
+ 2. Pre-built variants:
343
+ - EmptySearch: "No results found" with search tips
344
+ - EmptyList: "No items yet" with add item CTA
345
+ - EmptyInbox: "All caught up!" positive empty state
346
+ - EmptyError: "Something went wrong" with retry
347
+ - EmptyPermission: "Access denied" with request access
348
+ - EmptyOffline: "You're offline" with retry when online
349
+
350
+ 3. Features:
351
+ - Subtle entrance animation
352
+ - Responsive design
353
+ - Supports custom illustrations
354
+ - Action buttons with loading states
355
+ - TypeScript with proper props
356
+
357
+ Create: src/components/ui/EmptyState.tsx with all variants`,
358
+ });
359
+ }
360
+
361
+ const hasToast = await findFile(searchPath, /Toast|Notification|Snackbar/i);
362
+ if (!hasToast) {
363
+ issues.push({
364
+ id: 'missing-toast',
365
+ category: 'Frontend',
366
+ severity: 'high',
367
+ title: 'Missing Toast/Notification System',
368
+ description: 'No toast or notification component found for user feedback.',
369
+ suggestion: 'Add a toast/notification system for user feedback on actions.',
370
+ autoFixable: true,
371
+ aiPrompt: `Create a complete Toast notification system with:
372
+
373
+ 1. Toast component with:
374
+ - Variants: success, error, warning, info, loading
375
+ - Auto-dismiss with configurable duration
376
+ - Manual dismiss button
377
+ - Progress bar showing time remaining
378
+ - Stack multiple toasts
379
+ - Position options: top-right, top-center, bottom-right, etc.
380
+
381
+ 2. Toast provider/context:
382
+ - useToast() hook for easy triggering
383
+ - toast.success(), toast.error(), etc. methods
384
+ - toast.promise() for async operations
385
+ - Global configuration
386
+
387
+ 3. Features:
388
+ - Smooth slide-in/out animations
389
+ - Swipe to dismiss on mobile
390
+ - Pause on hover
391
+ - Accessible with ARIA live regions
392
+ - Queue management
393
+ - TypeScript with proper types
394
+
395
+ Create: src/components/ui/Toast.tsx and src/hooks/useToast.ts`,
396
+ });
397
+ }
398
+
399
+ // ═══════════════════════════════════════════════════════════════════════════
400
+ // FORMS & INPUT FEEDBACK
401
+ // ═══════════════════════════════════════════════════════════════════════════
402
+
403
+ const hasFormValidation = await findFile(searchPath, /FormError|FieldError|ValidationMessage/i);
404
+ const hasReactHookForm = packageJson && /react-hook-form/i.test(packageJson);
405
+ if (!hasFormValidation && !hasReactHookForm) {
406
+ issues.push({
407
+ id: 'missing-form-validation-ui',
408
+ category: 'Frontend',
409
+ severity: 'high',
410
+ title: 'Missing Form Validation UI',
411
+ description: 'No form validation components found. Users need clear feedback on form errors.',
412
+ suggestion: 'Add form validation components with clear error states.',
413
+ autoFixable: true,
414
+ aiPrompt: `Create a comprehensive form validation UI system with:
415
+
416
+ 1. FormField wrapper component with:
417
+ - Label with required indicator
418
+ - Help text / description
419
+ - Error message display
420
+ - Success state indicator
421
+ - Character counter for text inputs
422
+
423
+ 2. Input components with built-in validation states:
424
+ - TextInput with error/success borders
425
+ - SelectInput with validation
426
+ - Checkbox/Radio with error states
427
+ - TextArea with validation
428
+
429
+ 3. Validation feedback:
430
+ - Inline error messages with icons
431
+ - Shake animation on error
432
+ - Real-time validation feedback
433
+ - Form-level error summary
434
+ - Success checkmark animation
435
+
436
+ 4. Features:
437
+ - Works with react-hook-form or standalone
438
+ - Accessible error announcements
439
+ - Touch-friendly error targets
440
+ - TypeScript with proper types
441
+
442
+ Create: src/components/ui/FormField.tsx and related input components`,
443
+ });
444
+ }
445
+
446
+ // ═══════════════════════════════════════════════════════════════════════════
447
+ // MICRO-INTERACTIONS & ANIMATIONS
448
+ // ═══════════════════════════════════════════════════════════════════════════
449
+
450
+ const hasAnimationLib = packageJson && /framer-motion|react-spring|@react-spring|gsap|animejs/i.test(packageJson);
451
+ const hasAnimations = await findFile(searchPath, /animate|motion|transition|useSpring/i);
452
+ if (!hasAnimationLib && !hasAnimations) {
453
+ issues.push({
454
+ id: 'missing-animations',
455
+ category: 'Frontend',
456
+ severity: 'medium',
457
+ title: 'Missing Animation System',
458
+ description: 'No animation library or utilities found. Animations make UX feel polished.',
459
+ suggestion: 'Add framer-motion or CSS animations for micro-interactions.',
460
+ autoFixable: true,
461
+ aiPrompt: `Create a lightweight animation system with:
462
+
463
+ 1. CSS animation utilities (no library needed):
464
+ - Fade in/out
465
+ - Slide in from directions
466
+ - Scale up/down
467
+ - Bounce, pulse, shake
468
+ - Stagger children animations
469
+
470
+ 2. Animation wrapper components:
471
+ - <FadeIn> - fade in on mount
472
+ - <SlideIn direction="up|down|left|right">
473
+ - <ScaleIn> - scale from 0 to 1
474
+ - <AnimatePresence> - animate on unmount
475
+ - <StaggerChildren> - stagger child animations
476
+
477
+ 3. CSS utilities:
478
+ - .animate-fade-in, .animate-slide-up, etc.
479
+ - Animation delay utilities
480
+ - Reduced motion media query support
481
+ - Duration and easing variables
482
+
483
+ 4. Hooks:
484
+ - useInView() - trigger animation when element is visible
485
+ - useAnimation() - programmatic control
486
+
487
+ Create: src/styles/animations.css and src/components/ui/Animate.tsx
488
+
489
+ Or install framer-motion: npm install framer-motion`,
490
+ });
491
+ }
492
+
493
+ const hasHoverEffects = await findAllFiles(searchPath, /\.(css|scss|tsx?|jsx?)$/);
494
+ let foundHoverStates = false;
495
+ for (const file of hasHoverEffects.slice(0, 20)) {
496
+ const content = await readFileSafe(file);
497
+ if (content && /:hover|onMouseEnter|hover:/i.test(content)) {
498
+ foundHoverStates = true;
499
+ break;
500
+ }
501
+ }
502
+ if (!foundHoverStates) {
503
+ issues.push({
504
+ id: 'missing-hover-states',
505
+ category: 'Frontend',
506
+ severity: 'medium',
507
+ title: 'Missing Hover States',
508
+ description: 'No hover effects detected. Interactive elements need visual feedback on hover.',
509
+ suggestion: 'Add hover states to buttons, links, and interactive elements.',
510
+ autoFixable: true,
511
+ aiPrompt: `Add comprehensive hover states to your components:
512
+
513
+ 1. Button hover effects:
514
+ - Subtle background color shift
515
+ - Slight scale transform (1.02)
516
+ - Shadow elevation change
517
+ - Underline animation for text buttons
518
+
519
+ 2. Card hover effects:
520
+ - Lift effect with shadow
521
+ - Border color change
522
+ - Subtle background shift
523
+ - Scale transform
524
+
525
+ 3. Link hover effects:
526
+ - Underline animation (slide in)
527
+ - Color transition
528
+ - Arrow/icon movement
529
+
530
+ 4. Interactive elements:
531
+ - Focus-visible states (keyboard users)
532
+ - Active/pressed states
533
+ - Disabled state styling
534
+
535
+ 5. CSS utilities to add:
536
+ \`\`\`css
537
+ .hover-lift:hover {
538
+ transform: translateY(-2px);
539
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
540
+ }
541
+
542
+ .hover-scale:hover {
543
+ transform: scale(1.02);
544
+ }
545
+
546
+ .hover-glow:hover {
547
+ box-shadow: 0 0 20px rgba(var(--primary-rgb), 0.3);
548
+ }
549
+ \`\`\`
550
+
551
+ Add to: src/styles/interactions.css or your global styles`,
552
+ });
553
+ }
554
+
555
+ // ═══════════════════════════════════════════════════════════════════════════
556
+ // HAPTIC FEEDBACK & MOBILE UX
557
+ // ═══════════════════════════════════════════════════════════════════════════
558
+
559
+ const hasHapticFeedback = await findFile(searchPath, /haptic|vibrate|navigator\.vibrate/i);
560
+ if (!hasHapticFeedback) {
561
+ issues.push({
562
+ id: 'missing-haptic-feedback',
563
+ category: 'Frontend',
564
+ severity: 'low',
565
+ title: 'Missing Haptic Feedback',
566
+ description: 'No haptic feedback found. Mobile users benefit from tactile feedback on interactions.',
567
+ suggestion: 'Add haptic feedback for key interactions on mobile.',
568
+ autoFixable: true,
569
+ aiPrompt: `Create a haptic feedback utility system:
570
+
571
+ 1. useHaptic() hook:
572
+ \`\`\`typescript
573
+ const useHaptic = () => {
574
+ const trigger = (type: 'light' | 'medium' | 'heavy' | 'success' | 'error') => {
575
+ if (!navigator.vibrate) return;
576
+
577
+ const patterns = {
578
+ light: [10],
579
+ medium: [20],
580
+ heavy: [30],
581
+ success: [10, 50, 10],
582
+ error: [50, 30, 50, 30, 50],
583
+ };
584
+
585
+ navigator.vibrate(patterns[type]);
586
+ };
587
+
588
+ return { trigger };
589
+ };
590
+ \`\`\`
591
+
592
+ 2. Integration points:
593
+ - Button clicks (light)
594
+ - Form submission success (success)
595
+ - Form validation error (error)
596
+ - Toggle switches (light)
597
+ - Pull-to-refresh complete (medium)
598
+ - Delete/destructive actions (heavy)
599
+
600
+ 3. Best practices:
601
+ - Always check navigator.vibrate support
602
+ - Respect user preferences (reduced motion)
603
+ - Don't overuse - only key interactions
604
+ - Different patterns for different actions
605
+
606
+ Create: src/hooks/useHaptic.ts`,
607
+ });
608
+ }
609
+
610
+ const hasPullToRefresh = await findFile(searchPath, /PullToRefresh|pull-to-refresh|usePullRefresh/i);
611
+ if (!hasPullToRefresh && packageJson && /react-native|mobile|pwa/i.test(packageJson)) {
612
+ issues.push({
613
+ id: 'missing-pull-refresh',
614
+ category: 'Frontend',
615
+ severity: 'low',
616
+ title: 'Missing Pull-to-Refresh',
617
+ description: 'No pull-to-refresh found. Mobile/PWA apps benefit from this gesture.',
618
+ suggestion: 'Add pull-to-refresh for mobile list views.',
619
+ autoFixable: true,
620
+ aiPrompt: `Create a pull-to-refresh component for mobile:
621
+
622
+ 1. PullToRefresh wrapper component:
623
+ - Touch gesture detection
624
+ - Visual pull indicator
625
+ - Loading spinner during refresh
626
+ - Haptic feedback on trigger
627
+ - Smooth animations
628
+
629
+ 2. Features:
630
+ - Configurable pull threshold
631
+ - Custom pull indicator
632
+ - Callback on refresh
633
+ - Disable on desktop
634
+
635
+ Create: src/components/ui/PullToRefresh.tsx`,
636
+ });
637
+ }
638
+
639
+ // ═══════════════════════════════════════════════════════════════════════════
640
+ // ACCESSIBILITY (ARIA & A11Y)
641
+ // ═══════════════════════════════════════════════════════════════════════════
642
+
643
+ const hasAriaLive = await findFile(searchPath, /aria-live|role="alert"|role="status"/i);
644
+ if (!hasAriaLive) {
645
+ issues.push({
646
+ id: 'missing-aria-live',
647
+ category: 'Frontend',
648
+ severity: 'high',
649
+ title: 'Missing ARIA Live Regions',
650
+ description: 'No ARIA live regions found. Screen readers won\'t announce dynamic content changes.',
651
+ suggestion: 'Add aria-live regions for toasts, alerts, and dynamic updates.',
652
+ autoFixable: true,
653
+ aiPrompt: `Add ARIA live regions for accessibility:
654
+
655
+ 1. Create an Announcer component for screen readers:
656
+ \`\`\`tsx
657
+ const Announcer = () => {
658
+ const [message, setMessage] = useState('');
659
+
660
+ // Expose via context or global
661
+ useEffect(() => {
662
+ window.announce = (msg: string) => setMessage(msg);
663
+ return () => delete window.announce;
664
+ }, []);
665
+
666
+ return (
667
+ <div
668
+ role="status"
669
+ aria-live="polite"
670
+ aria-atomic="true"
671
+ className="sr-only"
672
+ >
673
+ {message}
674
+ </div>
675
+ );
676
+ };
677
+ \`\`\`
678
+
679
+ 2. Places to add aria-live:
680
+ - Toast notifications: aria-live="polite"
681
+ - Error alerts: aria-live="assertive"
682
+ - Loading states: aria-live="polite" aria-busy="true"
683
+ - Search results count
684
+ - Form validation errors
685
+
686
+ 3. Screen reader only utility:
687
+ \`\`\`css
688
+ .sr-only {
689
+ position: absolute;
690
+ width: 1px;
691
+ height: 1px;
692
+ padding: 0;
693
+ margin: -1px;
694
+ overflow: hidden;
695
+ clip: rect(0, 0, 0, 0);
696
+ white-space: nowrap;
697
+ border: 0;
698
+ }
699
+ \`\`\`
700
+
701
+ Create: src/components/ui/Announcer.tsx`,
702
+ });
703
+ }
704
+
705
+ const hasFocusTrap = await findFile(searchPath, /FocusTrap|focus-trap|useFocusTrap/i);
706
+ const hasModal = await findFile(searchPath, /Modal|Dialog|Drawer/i);
707
+ if (hasModal && !hasFocusTrap) {
708
+ issues.push({
709
+ id: 'missing-focus-trap',
710
+ category: 'Frontend',
711
+ severity: 'high',
712
+ title: 'Missing Focus Trap for Modals',
713
+ description: 'Modal/Dialog found but no focus trap. Keyboard users can tab outside the modal.',
714
+ suggestion: 'Add focus trap to modals and dialogs.',
715
+ autoFixable: true,
716
+ aiPrompt: `Add focus trapping to modals and dialogs:
717
+
718
+ 1. Create useFocusTrap hook:
719
+ \`\`\`typescript
720
+ const useFocusTrap = (isOpen: boolean) => {
721
+ const ref = useRef<HTMLElement>(null);
722
+
723
+ useEffect(() => {
724
+ if (!isOpen || !ref.current) return;
725
+
726
+ const focusableElements = ref.current.querySelectorAll(
727
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
728
+ );
729
+ const firstElement = focusableElements[0] as HTMLElement;
730
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
731
+
732
+ // Focus first element on open
733
+ firstElement?.focus();
734
+
735
+ const handleKeyDown = (e: KeyboardEvent) => {
736
+ if (e.key !== 'Tab') return;
737
+
738
+ if (e.shiftKey && document.activeElement === firstElement) {
739
+ e.preventDefault();
740
+ lastElement?.focus();
741
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
742
+ e.preventDefault();
743
+ firstElement?.focus();
744
+ }
745
+ };
746
+
747
+ document.addEventListener('keydown', handleKeyDown);
748
+ return () => document.removeEventListener('keydown', handleKeyDown);
749
+ }, [isOpen]);
750
+
751
+ return ref;
752
+ };
753
+ \`\`\`
754
+
755
+ 2. Additional requirements:
756
+ - Return focus to trigger element on close
757
+ - Handle Escape key to close
758
+ - Prevent body scroll when open
759
+ - aria-modal="true" on modal
760
+
761
+ Create: src/hooks/useFocusTrap.ts`,
762
+ });
763
+ }
764
+
765
+ const hasSkipLink = await findFile(searchPath, /SkipLink|skip-to-content|skip-nav/i);
766
+ if (!hasSkipLink) {
767
+ issues.push({
768
+ id: 'missing-skip-link',
769
+ category: 'Frontend',
770
+ severity: 'medium',
771
+ title: 'Missing Skip Link',
772
+ description: 'No skip-to-content link found. Keyboard users can\'t skip navigation.',
773
+ suggestion: 'Add a skip-to-main-content link at the top of pages.',
774
+ autoFixable: true,
775
+ aiPrompt: `Add a skip link for keyboard navigation:
776
+
777
+ \`\`\`tsx
778
+ // components/SkipLink.tsx
779
+ export const SkipLink = () => (
780
+ <a
781
+ href="#main-content"
782
+ className="skip-link"
783
+ >
784
+ Skip to main content
785
+ </a>
786
+ );
787
+
788
+ // styles
789
+ .skip-link {
790
+ position: absolute;
791
+ top: -40px;
792
+ left: 0;
793
+ background: var(--primary);
794
+ color: white;
795
+ padding: 8px 16px;
796
+ z-index: 100;
797
+ transition: top 0.3s;
798
+ }
799
+
800
+ .skip-link:focus {
801
+ top: 0;
802
+ }
803
+
804
+ // Add id to main content
805
+ <main id="main-content">
806
+ \`\`\`
807
+
808
+ Place at the very top of your layout, before the header.`,
809
+ });
810
+ }
811
+
812
+ // ═══════════════════════════════════════════════════════════════════════════
813
+ // CACHING & PERFORMANCE
814
+ // ═══════════════════════════════════════════════════════════════════════════
815
+
816
+ const hasSWR = packageJson && /swr|@tanstack\/react-query|react-query/i.test(packageJson);
817
+ if (!hasSWR) {
818
+ issues.push({
819
+ id: 'missing-data-caching',
820
+ category: 'Frontend',
821
+ severity: 'high',
822
+ title: 'Missing Data Caching Library',
823
+ description: 'No SWR or React Query found. Data fetching lacks caching, revalidation, and deduplication.',
824
+ suggestion: 'Add SWR or TanStack Query for data caching and synchronization.',
825
+ autoFixable: false,
826
+ aiPrompt: `Add a data fetching and caching solution:
827
+
828
+ Option 1: SWR (lighter weight)
829
+ \`\`\`bash
830
+ npm install swr
831
+ \`\`\`
832
+
833
+ \`\`\`typescript
834
+ // hooks/useApi.ts
835
+ import useSWR from 'swr';
836
+
837
+ const fetcher = (url: string) => fetch(url).then(r => r.json());
838
+
839
+ export const useUser = (id: string) => {
840
+ const { data, error, isLoading, mutate } = useSWR(
841
+ \`/api/users/\${id}\`,
842
+ fetcher,
843
+ {
844
+ revalidateOnFocus: true,
845
+ revalidateOnReconnect: true,
846
+ dedupingInterval: 2000,
847
+ }
848
+ );
849
+
850
+ return { user: data, error, isLoading, refresh: mutate };
851
+ };
852
+ \`\`\`
853
+
854
+ Option 2: TanStack Query (more features)
855
+ \`\`\`bash
856
+ npm install @tanstack/react-query
857
+ \`\`\`
858
+
859
+ Benefits you'll get:
860
+ - Automatic caching
861
+ - Background revalidation
862
+ - Request deduplication
863
+ - Optimistic updates
864
+ - Offline support
865
+ - Retry logic`,
866
+ });
867
+ }
868
+
869
+ const hasMemoization = await findFile(searchPath, /useMemo|useCallback|React\.memo/i);
870
+ if (!hasMemoization) {
871
+ issues.push({
872
+ id: 'missing-memoization',
873
+ category: 'Frontend',
874
+ severity: 'low',
875
+ title: 'No Memoization Detected',
876
+ description: 'No useMemo/useCallback/React.memo usage found. May have unnecessary re-renders.',
877
+ suggestion: 'Review components for memoization opportunities.',
878
+ autoFixable: false,
879
+ aiPrompt: `Review your components for memoization opportunities:
880
+
881
+ 1. useMemo - for expensive calculations:
882
+ \`\`\`typescript
883
+ const sortedItems = useMemo(() =>
884
+ items.sort((a, b) => a.name.localeCompare(b.name)),
885
+ [items]
886
+ );
887
+ \`\`\`
888
+
889
+ 2. useCallback - for functions passed as props:
890
+ \`\`\`typescript
891
+ const handleClick = useCallback((id: string) => {
892
+ setSelected(id);
893
+ }, []);
894
+ \`\`\`
895
+
896
+ 3. React.memo - for components that receive same props:
897
+ \`\`\`typescript
898
+ const ExpensiveList = React.memo(({ items }) => {
899
+ return items.map(item => <Item key={item.id} {...item} />);
900
+ });
901
+ \`\`\`
902
+
903
+ Where to apply:
904
+ - List item components
905
+ - Charts and visualizations
906
+ - Components receiving objects/arrays as props
907
+ - Expensive filter/sort operations
908
+ - Event handlers passed to children`,
909
+ });
910
+ }
911
+
912
+ // ═══════════════════════════════════════════════════════════════════════════
913
+ // LOGGING & DEBUGGING
914
+ // ═══════════════════════════════════════════════════════════════════════════
915
+
916
+ const hasLogger = await findFile(searchPath, /logger|logging|winston|pino|loglevel/i);
917
+ const hasConsoleRemoval = packageJson && /babel-plugin-transform-remove-console|terser/i.test(packageJson);
918
+ if (!hasLogger) {
919
+ issues.push({
920
+ id: 'missing-frontend-logger',
921
+ category: 'Frontend',
922
+ severity: 'medium',
923
+ title: 'Missing Logging System',
924
+ description: 'No structured logging found. console.log statements are not suitable for production.',
925
+ suggestion: 'Add a logging utility that can be disabled in production.',
926
+ autoFixable: true,
927
+ aiPrompt: `Create a frontend logging utility:
928
+
929
+ \`\`\`typescript
930
+ // lib/logger.ts
931
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
932
+
933
+ const isDev = process.env.NODE_ENV === 'development';
934
+
935
+ const logger = {
936
+ debug: (...args: any[]) => isDev && console.debug('[DEBUG]', ...args),
937
+ info: (...args: any[]) => isDev && console.info('[INFO]', ...args),
938
+ warn: (...args: any[]) => console.warn('[WARN]', ...args),
939
+ error: (...args: any[]) => {
940
+ console.error('[ERROR]', ...args);
941
+ // Send to error tracking service in production
942
+ if (!isDev && typeof window !== 'undefined') {
943
+ // Sentry.captureException(args[0]);
944
+ }
945
+ },
946
+
947
+ // Performance logging
948
+ time: (label: string) => isDev && console.time(label),
949
+ timeEnd: (label: string) => isDev && console.timeEnd(label),
950
+
951
+ // Group logs
952
+ group: (label: string) => isDev && console.group(label),
953
+ groupEnd: () => isDev && console.groupEnd(),
954
+ };
955
+
956
+ export default logger;
957
+ \`\`\`
958
+
959
+ Usage:
960
+ \`\`\`typescript
961
+ import logger from '@/lib/logger';
962
+
963
+ logger.debug('User data:', userData);
964
+ logger.error('Failed to fetch', error);
965
+
966
+ logger.time('render');
967
+ // ... expensive operation
968
+ logger.timeEnd('render');
969
+ \`\`\``,
970
+ });
971
+ }
972
+
973
+ const hasErrorTracking = packageJson && /sentry|bugsnag|rollbar|logrocket|@sentry/i.test(packageJson);
974
+ if (!hasErrorTracking) {
975
+ issues.push({
976
+ id: 'missing-error-tracking',
977
+ category: 'Frontend',
978
+ severity: 'high',
979
+ title: 'Missing Error Tracking',
980
+ description: 'No error tracking service found. Production errors will go unnoticed.',
981
+ suggestion: 'Add Sentry, Bugsnag, or similar for production error monitoring.',
982
+ autoFixable: false,
983
+ aiPrompt: `Set up error tracking with Sentry:
984
+
985
+ 1. Install Sentry:
986
+ \`\`\`bash
987
+ npm install @sentry/nextjs
988
+ npx @sentry/wizard@latest -i nextjs
989
+ \`\`\`
990
+
991
+ 2. This will create:
992
+ - sentry.client.config.ts
993
+ - sentry.server.config.ts
994
+ - sentry.edge.config.ts
995
+
996
+ 3. Configure environment:
997
+ \`\`\`env
998
+ SENTRY_DSN=your-dsn-here
999
+ SENTRY_AUTH_TOKEN=your-auth-token
1000
+ \`\`\`
1001
+
1002
+ 4. Custom error boundary integration:
1003
+ \`\`\`typescript
1004
+ import * as Sentry from '@sentry/nextjs';
1005
+
1006
+ // In your error boundary
1007
+ componentDidCatch(error, errorInfo) {
1008
+ Sentry.captureException(error, { extra: errorInfo });
1009
+ }
1010
+ \`\`\`
1011
+
1012
+ 5. User context:
1013
+ \`\`\`typescript
1014
+ Sentry.setUser({ id: user.id, email: user.email });
1015
+ \`\`\``,
1016
+ });
1017
+ }
1018
+
1019
+ // ═══════════════════════════════════════════════════════════════════════════
1020
+ // IMAGE & MEDIA OPTIMIZATION
1021
+ // ═══════════════════════════════════════════════════════════════════════════
1022
+
1023
+ const hasLazyImages = await findFile(searchPath, /loading="lazy"|LazyImage|Image.*priority/i);
1024
+ if (!hasLazyImages) {
1025
+ issues.push({
1026
+ id: 'missing-lazy-images',
1027
+ category: 'Frontend',
1028
+ severity: 'medium',
1029
+ title: 'Missing Lazy Loading for Images',
1030
+ description: 'No lazy loading for images detected. All images load immediately.',
1031
+ suggestion: 'Add lazy loading to below-the-fold images.',
1032
+ autoFixable: true,
1033
+ aiPrompt: `Implement lazy loading for images:
1034
+
1035
+ 1. For Next.js - use next/image:
1036
+ \`\`\`tsx
1037
+ import Image from 'next/image';
1038
+
1039
+ // Lazy load by default, add priority for above-fold
1040
+ <Image
1041
+ src="/hero.jpg"
1042
+ alt="Hero"
1043
+ width={1200}
1044
+ height={600}
1045
+ priority // Only for above-the-fold images
1046
+ />
1047
+
1048
+ // Below the fold - lazy loads automatically
1049
+ <Image
1050
+ src="/feature.jpg"
1051
+ alt="Feature"
1052
+ width={600}
1053
+ height={400}
1054
+ // No priority = lazy loaded
1055
+ />
1056
+ \`\`\`
1057
+
1058
+ 2. For regular img tags:
1059
+ \`\`\`tsx
1060
+ <img
1061
+ src="/image.jpg"
1062
+ alt="Description"
1063
+ loading="lazy"
1064
+ decoding="async"
1065
+ />
1066
+ \`\`\`
1067
+
1068
+ 3. Create a LazyImage component:
1069
+ \`\`\`tsx
1070
+ export const LazyImage = ({ src, alt, ...props }) => (
1071
+ <img
1072
+ src={src}
1073
+ alt={alt}
1074
+ loading="lazy"
1075
+ decoding="async"
1076
+ {...props}
1077
+ />
1078
+ );
1079
+ \`\`\``,
1080
+ });
1081
+ }
1082
+
1083
+ // ═══════════════════════════════════════════════════════════════════════════
1084
+ // THEME & DARK MODE
1085
+ // ═══════════════════════════════════════════════════════════════════════════
1086
+
1087
+ const hasDarkMode = await findFile(searchPath, /dark-mode|darkMode|theme-toggle|ThemeProvider|useTheme/i);
1088
+ if (!hasDarkMode) {
1089
+ issues.push({
1090
+ id: 'missing-dark-mode',
1091
+ category: 'Frontend',
1092
+ severity: 'low',
1093
+ title: 'Missing Dark Mode Support',
1094
+ description: 'No dark mode implementation found. Many users prefer dark mode.',
1095
+ suggestion: 'Add dark mode toggle with system preference detection.',
1096
+ autoFixable: true,
1097
+ aiPrompt: `Implement dark mode with system preference detection:
1098
+
1099
+ 1. Create ThemeProvider and useTheme hook:
1100
+ \`\`\`typescript
1101
+ // contexts/ThemeContext.tsx
1102
+ type Theme = 'light' | 'dark' | 'system';
1103
+
1104
+ const ThemeContext = createContext<{
1105
+ theme: Theme;
1106
+ setTheme: (theme: Theme) => void;
1107
+ }>({ theme: 'system', setTheme: () => {} });
1108
+
1109
+ export const ThemeProvider = ({ children }) => {
1110
+ const [theme, setTheme] = useState<Theme>('system');
1111
+
1112
+ useEffect(() => {
1113
+ const saved = localStorage.getItem('theme') as Theme;
1114
+ if (saved) setTheme(saved);
1115
+ }, []);
1116
+
1117
+ useEffect(() => {
1118
+ const root = document.documentElement;
1119
+ const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1120
+ const isDark = theme === 'dark' || (theme === 'system' && systemDark);
1121
+
1122
+ root.classList.toggle('dark', isDark);
1123
+ localStorage.setItem('theme', theme);
1124
+ }, [theme]);
1125
+
1126
+ return (
1127
+ <ThemeContext.Provider value={{ theme, setTheme }}>
1128
+ {children}
1129
+ </ThemeContext.Provider>
1130
+ );
1131
+ };
1132
+
1133
+ export const useTheme = () => useContext(ThemeContext);
1134
+ \`\`\`
1135
+
1136
+ 2. CSS variables approach:
1137
+ \`\`\`css
1138
+ :root {
1139
+ --bg: #ffffff;
1140
+ --text: #1a1a1a;
1141
+ --primary: #0066cc;
1142
+ }
1143
+
1144
+ .dark {
1145
+ --bg: #1a1a1a;
1146
+ --text: #ffffff;
1147
+ --primary: #66b3ff;
1148
+ }
1149
+ \`\`\`
1150
+
1151
+ 3. Theme toggle component:
1152
+ \`\`\`tsx
1153
+ export const ThemeToggle = () => {
1154
+ const { theme, setTheme } = useTheme();
1155
+
1156
+ return (
1157
+ <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
1158
+ {theme === 'dark' ? '☀️' : '🌙'}
1159
+ </button>
1160
+ );
1161
+ };
1162
+ \`\`\``,
1163
+ });
1164
+ }
1165
+
1166
+ // ═══════════════════════════════════════════════════════════════════════════
1167
+ // CONFIRMATION & DESTRUCTIVE ACTIONS
1168
+ // ═══════════════════════════════════════════════════════════════════════════
1169
+
1170
+ const hasConfirmDialog = await findFile(searchPath, /ConfirmDialog|Confirm|AlertDialog|useConfirm/i);
1171
+ if (!hasConfirmDialog) {
1172
+ issues.push({
1173
+ id: 'missing-confirm-dialog',
1174
+ category: 'Frontend',
1175
+ severity: 'high',
1176
+ title: 'Missing Confirmation Dialog',
1177
+ description: 'No confirmation dialog found. Destructive actions should require confirmation.',
1178
+ suggestion: 'Add a confirmation dialog for delete, logout, and irreversible actions.',
1179
+ autoFixable: true,
1180
+ aiPrompt: `Create a confirmation dialog system:
1181
+
1182
+ 1. ConfirmDialog component:
1183
+ \`\`\`tsx
1184
+ interface ConfirmDialogProps {
1185
+ isOpen: boolean;
1186
+ onClose: () => void;
1187
+ onConfirm: () => void;
1188
+ title: string;
1189
+ message: string;
1190
+ confirmText?: string;
1191
+ cancelText?: string;
1192
+ variant?: 'danger' | 'warning' | 'info';
1193
+ isLoading?: boolean;
1194
+ }
1195
+
1196
+ export const ConfirmDialog = ({
1197
+ isOpen, onClose, onConfirm, title, message,
1198
+ confirmText = 'Confirm',
1199
+ cancelText = 'Cancel',
1200
+ variant = 'danger',
1201
+ isLoading = false,
1202
+ }) => {
1203
+ return (
1204
+ <Dialog open={isOpen} onClose={onClose}>
1205
+ <div className="confirm-dialog">
1206
+ <h3>{title}</h3>
1207
+ <p>{message}</p>
1208
+ <div className="actions">
1209
+ <Button variant="ghost" onClick={onClose} disabled={isLoading}>
1210
+ {cancelText}
1211
+ </Button>
1212
+ <Button
1213
+ variant={variant}
1214
+ onClick={onConfirm}
1215
+ isLoading={isLoading}
1216
+ >
1217
+ {confirmText}
1218
+ </Button>
1219
+ </div>
1220
+ </div>
1221
+ </Dialog>
1222
+ );
1223
+ };
1224
+ \`\`\`
1225
+
1226
+ 2. useConfirm hook for easy usage:
1227
+ \`\`\`typescript
1228
+ const useConfirm = () => {
1229
+ const [state, setState] = useState({ isOpen: false, config: null });
1230
+
1231
+ const confirm = (config) => new Promise((resolve) => {
1232
+ setState({
1233
+ isOpen: true,
1234
+ config: { ...config, resolve }
1235
+ });
1236
+ });
1237
+
1238
+ const handleConfirm = () => {
1239
+ state.config?.resolve(true);
1240
+ setState({ isOpen: false, config: null });
1241
+ };
1242
+
1243
+ const handleCancel = () => {
1244
+ state.config?.resolve(false);
1245
+ setState({ isOpen: false, config: null });
1246
+ };
1247
+
1248
+ return { confirm, ConfirmDialog: /* render dialog */ };
1249
+ };
1250
+
1251
+ // Usage:
1252
+ const { confirm } = useConfirm();
1253
+ const shouldDelete = await confirm({
1254
+ title: 'Delete item?',
1255
+ message: 'This action cannot be undone.',
1256
+ });
1257
+ if (shouldDelete) deleteItem();
1258
+ \`\`\``,
1259
+ });
1260
+ }
1261
+
1262
+ // ═══════════════════════════════════════════════════════════════════════════
1263
+ // OPTIMISTIC UPDATES
1264
+ // ═══════════════════════════════════════════════════════════════════════════
1265
+
1266
+ const hasOptimisticUpdates = await findFile(searchPath, /optimistic|mutate.*onMutate|useMutation/i);
1267
+ if (!hasOptimisticUpdates && hasSWR) {
1268
+ issues.push({
1269
+ id: 'missing-optimistic-updates',
1270
+ category: 'Frontend',
1271
+ severity: 'low',
1272
+ title: 'No Optimistic Updates',
1273
+ description: 'Data mutations may feel slow without optimistic updates.',
1274
+ suggestion: 'Add optimistic updates for better perceived performance.',
1275
+ autoFixable: false,
1276
+ aiPrompt: `Implement optimistic updates for mutations:
1277
+
1278
+ With SWR:
1279
+ \`\`\`typescript
1280
+ import useSWR, { useSWRConfig } from 'swr';
1281
+
1282
+ const useUpdateTodo = () => {
1283
+ const { mutate } = useSWRConfig();
1284
+
1285
+ const updateTodo = async (id: string, data: Partial<Todo>) => {
1286
+ // Optimistic update
1287
+ mutate(
1288
+ '/api/todos',
1289
+ (todos: Todo[]) => todos.map(t =>
1290
+ t.id === id ? { ...t, ...data } : t
1291
+ ),
1292
+ false // Don't revalidate yet
1293
+ );
1294
+
1295
+ try {
1296
+ await fetch(\`/api/todos/\${id}\`, {
1297
+ method: 'PATCH',
1298
+ body: JSON.stringify(data),
1299
+ });
1300
+ // Revalidate after success
1301
+ mutate('/api/todos');
1302
+ } catch (error) {
1303
+ // Revert on error
1304
+ mutate('/api/todos');
1305
+ throw error;
1306
+ }
1307
+ };
1308
+
1309
+ return { updateTodo };
1310
+ };
1311
+ \`\`\`
1312
+
1313
+ With TanStack Query:
1314
+ \`\`\`typescript
1315
+ const useUpdateTodo = () => {
1316
+ const queryClient = useQueryClient();
1317
+
1318
+ return useMutation({
1319
+ mutationFn: updateTodoApi,
1320
+ onMutate: async (newTodo) => {
1321
+ await queryClient.cancelQueries({ queryKey: ['todos'] });
1322
+ const previous = queryClient.getQueryData(['todos']);
1323
+
1324
+ queryClient.setQueryData(['todos'], (old) =>
1325
+ old.map(t => t.id === newTodo.id ? { ...t, ...newTodo } : t)
1326
+ );
1327
+
1328
+ return { previous };
1329
+ },
1330
+ onError: (err, newTodo, context) => {
1331
+ queryClient.setQueryData(['todos'], context.previous);
1332
+ },
1333
+ onSettled: () => {
1334
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
1335
+ },
1336
+ });
1337
+ };
1338
+ \`\`\``,
1339
+ });
1340
+ }
1341
+
1342
+ // ═══════════════════════════════════════════════════════════════════════════
1343
+ // KEYBOARD SHORTCUTS
1344
+ // ═══════════════════════════════════════════════════════════════════════════
1345
+
1346
+ const hasKeyboardShortcuts = await findFile(searchPath, /useHotkeys|Keyboard|shortcut|Cmd\+|Ctrl\+/i);
1347
+ if (!hasKeyboardShortcuts) {
1348
+ issues.push({
1349
+ id: 'missing-keyboard-shortcuts',
1350
+ category: 'Frontend',
1351
+ severity: 'low',
1352
+ title: 'Missing Keyboard Shortcuts',
1353
+ description: 'No keyboard shortcuts detected. Power users benefit from keyboard navigation.',
1354
+ suggestion: 'Add keyboard shortcuts for common actions.',
1355
+ autoFixable: true,
1356
+ aiPrompt: `Implement keyboard shortcuts for power users:
1357
+
1358
+ 1. Create useHotkey hook:
1359
+ \`\`\`typescript
1360
+ const useHotkey = (key: string, callback: () => void, deps: any[] = []) => {
1361
+ useEffect(() => {
1362
+ const handler = (e: KeyboardEvent) => {
1363
+ const isMac = navigator.platform.includes('Mac');
1364
+ const modifier = isMac ? e.metaKey : e.ctrlKey;
1365
+
1366
+ // Parse key combo like "mod+k" or "shift+enter"
1367
+ const parts = key.toLowerCase().split('+');
1368
+ const mainKey = parts[parts.length - 1];
1369
+ const needsModifier = parts.includes('mod');
1370
+ const needsShift = parts.includes('shift');
1371
+
1372
+ if (needsModifier && !modifier) return;
1373
+ if (needsShift && !e.shiftKey) return;
1374
+ if (e.key.toLowerCase() !== mainKey) return;
1375
+
1376
+ e.preventDefault();
1377
+ callback();
1378
+ };
1379
+
1380
+ window.addEventListener('keydown', handler);
1381
+ return () => window.removeEventListener('keydown', handler);
1382
+ }, deps);
1383
+ };
1384
+ \`\`\`
1385
+
1386
+ 2. Common shortcuts to implement:
1387
+ - Cmd/Ctrl + K: Search / Command palette
1388
+ - Cmd/Ctrl + S: Save
1389
+ - Cmd/Ctrl + Enter: Submit form
1390
+ - Escape: Close modal/dialog
1391
+ - ?: Show keyboard shortcuts help
1392
+
1393
+ 3. Display shortcut hints in UI:
1394
+ \`\`\`tsx
1395
+ <Button>
1396
+ Save
1397
+ <kbd className="ml-2 opacity-50">⌘S</kbd>
1398
+ </Button>
1399
+ \`\`\``,
1400
+ });
1401
+ }
1402
+
1403
+ // ═══════════════════════════════════════════════════════════════════════════
1404
+ // SCROLL BEHAVIOR
1405
+ // ═══════════════════════════════════════════════════════════════════════════
1406
+
1407
+ const hasScrollToTop = await findFile(searchPath, /ScrollToTop|scroll-to-top|BackToTop/i);
1408
+ if (!hasScrollToTop) {
1409
+ issues.push({
1410
+ id: 'missing-scroll-to-top',
1411
+ category: 'Frontend',
1412
+ severity: 'low',
1413
+ title: 'Missing Scroll-to-Top Button',
1414
+ description: 'No scroll-to-top button found. Long pages need easy navigation back to top.',
1415
+ suggestion: 'Add a scroll-to-top button for long pages.',
1416
+ autoFixable: true,
1417
+ aiPrompt: `Create a scroll-to-top button:
1418
+
1419
+ \`\`\`tsx
1420
+ export const ScrollToTop = () => {
1421
+ const [isVisible, setIsVisible] = useState(false);
1422
+
1423
+ useEffect(() => {
1424
+ const toggleVisibility = () => {
1425
+ setIsVisible(window.scrollY > 500);
1426
+ };
1427
+
1428
+ window.addEventListener('scroll', toggleVisibility);
1429
+ return () => window.removeEventListener('scroll', toggleVisibility);
1430
+ }, []);
1431
+
1432
+ const scrollToTop = () => {
1433
+ window.scrollTo({
1434
+ top: 0,
1435
+ behavior: 'smooth'
1436
+ });
1437
+ };
1438
+
1439
+ if (!isVisible) return null;
1440
+
1441
+ return (
1442
+ <button
1443
+ onClick={scrollToTop}
1444
+ className="scroll-to-top"
1445
+ aria-label="Scroll to top"
1446
+ >
1447
+
1448
+ </button>
1449
+ );
1450
+ };
1451
+ \`\`\`
1452
+
1453
+ CSS:
1454
+ \`\`\`css
1455
+ .scroll-to-top {
1456
+ position: fixed;
1457
+ bottom: 2rem;
1458
+ right: 2rem;
1459
+ width: 48px;
1460
+ height: 48px;
1461
+ border-radius: 50%;
1462
+ background: var(--primary);
1463
+ color: white;
1464
+ border: none;
1465
+ cursor: pointer;
1466
+ opacity: 0.9;
1467
+ transition: opacity 0.2s, transform 0.2s;
1468
+ z-index: 50;
1469
+ }
1470
+
1471
+ .scroll-to-top:hover {
1472
+ opacity: 1;
1473
+ transform: translateY(-2px);
1474
+ }
1475
+ \`\`\``,
1476
+ });
1477
+ }
1478
+
1479
+ // ═══════════════════════════════════════════════════════════════════════════
1480
+ // PROGRESS INDICATORS
1481
+ // ═══════════════════════════════════════════════════════════════════════════
1482
+
1483
+ const hasProgressBar = await findFile(searchPath, /Progress|ProgressBar|NProgress|nprogress/i);
1484
+ if (!hasProgressBar) {
1485
+ issues.push({
1486
+ id: 'missing-progress-bar',
1487
+ category: 'Frontend',
1488
+ severity: 'medium',
1489
+ title: 'Missing Progress Indicators',
1490
+ description: 'No progress bar found. Users need visual feedback for page transitions and uploads.',
1491
+ suggestion: 'Add progress bar for navigation and long operations.',
1492
+ autoFixable: true,
1493
+ aiPrompt: `Add progress indicators:
1494
+
1495
+ 1. Page transition progress (NProgress style):
1496
+ \`\`\`bash
1497
+ npm install nprogress
1498
+ npm install @types/nprogress -D
1499
+ \`\`\`
1500
+
1501
+ \`\`\`tsx
1502
+ // app/providers.tsx (Next.js App Router)
1503
+ 'use client';
1504
+ import { useEffect } from 'react';
1505
+ import { usePathname, useSearchParams } from 'next/navigation';
1506
+ import NProgress from 'nprogress';
1507
+ import 'nprogress/nprogress.css';
1508
+
1509
+ NProgress.configure({ showSpinner: false });
1510
+
1511
+ export function NavigationProgress() {
1512
+ const pathname = usePathname();
1513
+ const searchParams = useSearchParams();
1514
+
1515
+ useEffect(() => {
1516
+ NProgress.done();
1517
+ }, [pathname, searchParams]);
1518
+
1519
+ return null;
1520
+ }
1521
+ \`\`\`
1522
+
1523
+ 2. Custom progress bar component:
1524
+ \`\`\`tsx
1525
+ export const ProgressBar = ({ value, max = 100 }) => (
1526
+ <div className="progress-bar" role="progressbar" aria-valuenow={value}>
1527
+ <div
1528
+ className="progress-bar-fill"
1529
+ style={{ width: \`\${(value / max) * 100}%\` }}
1530
+ />
1531
+ </div>
1532
+ );
1533
+ \`\`\`
1534
+
1535
+ 3. File upload progress:
1536
+ \`\`\`tsx
1537
+ export const UploadProgress = ({ progress, fileName }) => (
1538
+ <div className="upload-progress">
1539
+ <span>{fileName}</span>
1540
+ <ProgressBar value={progress} />
1541
+ <span>{progress}%</span>
1542
+ </div>
1543
+ );
1544
+ \`\`\``,
1545
+ });
1546
+ }
1547
+
1548
+ return issues;
1549
+ },
1550
+
1551
+ // ─────────────────────────────────────────────────────────────────────────────
1552
+ // BACKEND CHECKER
1553
+ // ─────────────────────────────────────────────────────────────────────────────
1554
+ async backend(projectPath) {
1555
+ const issues = [];
1556
+ const apiPath = path.join(projectPath, 'src', 'server');
1557
+ const apiAltPath = path.join(projectPath, 'api');
1558
+ const pagesApiPath = path.join(projectPath, 'pages', 'api');
1559
+ const appApiPath = path.join(projectPath, 'app', 'api');
1560
+
1561
+ const hasApi = await pathExists(apiPath) || await pathExists(apiAltPath) ||
1562
+ await pathExists(pagesApiPath) || await pathExists(appApiPath);
1563
+
1564
+ if (!hasApi) return issues;
1565
+
1566
+ // Check for health endpoint
1567
+ const hasHealth = await findFile(projectPath, /health|healthcheck|status/i);
1568
+ if (!hasHealth) {
1569
+ issues.push({
1570
+ id: 'missing-health-endpoint',
1571
+ category: 'Backend',
1572
+ severity: 'high',
1573
+ title: 'Missing Health Endpoint',
1574
+ description: 'No health check endpoint found. Load balancers and monitoring can\'t verify service status.',
1575
+ suggestion: 'Add a /health or /api/health endpoint that returns service status.',
1576
+ autoFixable: true,
1577
+ });
1578
+ }
1579
+
1580
+ // Check for validation library
1581
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
1582
+ if (packageJson) {
1583
+ const hasValidation = /zod|yup|joi|class-validator|ajv/i.test(packageJson);
1584
+ if (!hasValidation) {
1585
+ issues.push({
1586
+ id: 'missing-validation',
1587
+ category: 'Backend',
1588
+ severity: 'high',
1589
+ title: 'Missing Input Validation',
1590
+ description: 'No validation library found. API inputs may not be properly validated.',
1591
+ suggestion: 'Add zod, yup, joi, or similar for input validation.',
1592
+ autoFixable: false,
1593
+ });
1594
+ }
1595
+ }
1596
+
1597
+ // Check for rate limiting
1598
+ const hasRateLimiting = packageJson && /rate-limit|ratelimit|express-rate-limit|@upstash\/ratelimit/i.test(packageJson);
1599
+ if (!hasRateLimiting) {
1600
+ issues.push({
1601
+ id: 'missing-rate-limiting',
1602
+ category: 'Backend',
1603
+ severity: 'high',
1604
+ title: 'Missing Rate Limiting',
1605
+ description: 'No rate limiting found. API is vulnerable to abuse and DDoS.',
1606
+ suggestion: 'Add rate limiting middleware to protect your API.',
1607
+ autoFixable: false,
1608
+ });
1609
+ }
1610
+
1611
+ // Check for error handling middleware
1612
+ const hasErrorMiddleware = await findFile(projectPath, /errorHandler|error-handler|errorMiddleware/i);
1613
+ if (!hasErrorMiddleware) {
1614
+ issues.push({
1615
+ id: 'missing-error-handler',
1616
+ category: 'Backend',
1617
+ severity: 'medium',
1618
+ title: 'Missing Global Error Handler',
1619
+ description: 'No global error handler found. Unhandled errors may leak stack traces.',
1620
+ suggestion: 'Add a global error handler middleware that catches and formats errors.',
1621
+ autoFixable: true,
1622
+ });
1623
+ }
1624
+
1625
+ return issues;
1626
+ },
1627
+
1628
+ // ─────────────────────────────────────────────────────────────────────────────
1629
+ // SECURITY CHECKER
1630
+ // ─────────────────────────────────────────────────────────────────────────────
1631
+ async security(projectPath) {
1632
+ const issues = [];
1633
+
1634
+ // Check for .env.example
1635
+ const hasEnvExample = await pathExists(path.join(projectPath, '.env.example')) ||
1636
+ await pathExists(path.join(projectPath, '.env.sample'));
1637
+ if (!hasEnvExample) {
1638
+ issues.push({
1639
+ id: 'missing-env-example',
1640
+ category: 'Security',
1641
+ severity: 'medium',
1642
+ title: 'Missing .env.example',
1643
+ description: 'No .env.example file found. Team members won\'t know what env vars are needed.',
1644
+ suggestion: 'Create .env.example with all required environment variables (without values).',
1645
+ autoFixable: true,
1646
+ });
1647
+ }
1648
+
1649
+ // Check for .gitignore with .env
1650
+ const gitignore = await readFileSafe(path.join(projectPath, '.gitignore'));
1651
+ if (gitignore && !gitignore.includes('.env')) {
1652
+ issues.push({
1653
+ id: 'env-not-gitignored',
1654
+ category: 'Security',
1655
+ severity: 'critical',
1656
+ title: '.env Not in .gitignore',
1657
+ description: 'Environment files may be committed to git, exposing secrets.',
1658
+ suggestion: 'Add .env* to .gitignore immediately.',
1659
+ autoFixable: true,
1660
+ });
1661
+ }
1662
+
1663
+ // Check for security headers
1664
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
1665
+ const hasHelmet = packageJson && /helmet|next-secure-headers/i.test(packageJson);
1666
+ if (!hasHelmet) {
1667
+ issues.push({
1668
+ id: 'missing-security-headers',
1669
+ category: 'Security',
1670
+ severity: 'high',
1671
+ title: 'Missing Security Headers',
1672
+ description: 'No security headers library found (helmet, etc.).',
1673
+ suggestion: 'Add helmet or security headers middleware for CSP, HSTS, etc.',
1674
+ autoFixable: false,
1675
+ });
1676
+ }
1677
+
1678
+ // Check for CORS configuration
1679
+ const hasCors = packageJson && /cors|@fastify\/cors/i.test(packageJson);
1680
+ const nextConfig = await readFileSafe(path.join(projectPath, 'next.config.js')) ||
1681
+ await readFileSafe(path.join(projectPath, 'next.config.mjs'));
1682
+ const hasNextCors = nextConfig && /headers|Access-Control/i.test(nextConfig);
1683
+
1684
+ if (!hasCors && !hasNextCors) {
1685
+ issues.push({
1686
+ id: 'missing-cors',
1687
+ category: 'Security',
1688
+ severity: 'medium',
1689
+ title: 'No CORS Configuration',
1690
+ description: 'No CORS configuration found. May have issues with cross-origin requests.',
1691
+ suggestion: 'Add CORS configuration to control which origins can access your API.',
1692
+ autoFixable: false,
1693
+ });
1694
+ }
1695
+
1696
+ return issues;
1697
+ },
1698
+
1699
+ // ─────────────────────────────────────────────────────────────────────────────
1700
+ // PERFORMANCE CHECKER
1701
+ // ─────────────────────────────────────────────────────────────────────────────
1702
+ async performance(projectPath) {
1703
+ const issues = [];
1704
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
1705
+
1706
+ // Check for image optimization
1707
+ const nextConfig = await readFileSafe(path.join(projectPath, 'next.config.js')) ||
1708
+ await readFileSafe(path.join(projectPath, 'next.config.mjs'));
1709
+ const hasImageOptimization = (packageJson && /sharp|next\/image|@next\/image/i.test(packageJson)) ||
1710
+ (nextConfig && /images:/i.test(nextConfig));
1711
+
1712
+ if (!hasImageOptimization) {
1713
+ issues.push({
1714
+ id: 'missing-image-optimization',
1715
+ category: 'Performance',
1716
+ severity: 'medium',
1717
+ title: 'Missing Image Optimization',
1718
+ description: 'No image optimization setup found. Images may be served unoptimized.',
1719
+ suggestion: 'Use Next.js Image component or sharp for image optimization.',
1720
+ autoFixable: false,
1721
+ });
1722
+ }
1723
+
1724
+ // Check for caching headers
1725
+ const hasCaching = await findFile(projectPath, /cache|stale-while-revalidate/i);
1726
+ if (!hasCaching) {
1727
+ issues.push({
1728
+ id: 'missing-caching',
1729
+ category: 'Performance',
1730
+ severity: 'medium',
1731
+ title: 'No Caching Strategy',
1732
+ description: 'No caching configuration found. API responses may not be cached.',
1733
+ suggestion: 'Add Cache-Control headers or use SWR/React Query for client-side caching.',
1734
+ autoFixable: false,
1735
+ });
1736
+ }
1737
+
1738
+ // Check for bundle analyzer
1739
+ const hasBundleAnalyzer = packageJson && /@next\/bundle-analyzer|webpack-bundle-analyzer/i.test(packageJson);
1740
+ if (!hasBundleAnalyzer) {
1741
+ issues.push({
1742
+ id: 'missing-bundle-analyzer',
1743
+ category: 'Performance',
1744
+ severity: 'low',
1745
+ title: 'No Bundle Analyzer',
1746
+ description: 'No bundle analyzer installed. Can\'t visualize bundle size.',
1747
+ suggestion: 'Add @next/bundle-analyzer to track and optimize bundle size.',
1748
+ autoFixable: false,
1749
+ });
1750
+ }
1751
+
1752
+ return issues;
1753
+ },
1754
+
1755
+ // ─────────────────────────────────────────────────────────────────────────────
1756
+ // ACCESSIBILITY CHECKER
1757
+ // ─────────────────────────────────────────────────────────────────────────────
1758
+ async accessibility(projectPath) {
1759
+ const issues = [];
1760
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
1761
+
1762
+ // Check for accessibility testing
1763
+ const hasA11yTesting = packageJson && /axe-core|@axe-core|jest-axe|cypress-axe|pa11y/i.test(packageJson);
1764
+ if (!hasA11yTesting) {
1765
+ issues.push({
1766
+ id: 'missing-a11y-testing',
1767
+ category: 'Accessibility',
1768
+ severity: 'medium',
1769
+ title: 'No Accessibility Testing',
1770
+ description: 'No accessibility testing library found.',
1771
+ suggestion: 'Add jest-axe, cypress-axe, or pa11y for automated a11y testing.',
1772
+ autoFixable: false,
1773
+ });
1774
+ }
1775
+
1776
+ // Check for skip link
1777
+ const hasSkipLink = await findFile(projectPath, /skip-link|skiplink|SkipNav/i);
1778
+ if (!hasSkipLink) {
1779
+ issues.push({
1780
+ id: 'missing-skip-link',
1781
+ category: 'Accessibility',
1782
+ severity: 'medium',
1783
+ title: 'Missing Skip Link',
1784
+ description: 'No skip-to-content link found. Keyboard users can\'t skip navigation.',
1785
+ suggestion: 'Add a skip-to-main-content link at the top of your pages.',
1786
+ autoFixable: true,
1787
+ });
1788
+ }
1789
+
1790
+ // Check for focus-visible
1791
+ const globalCss = await readFileSafe(path.join(projectPath, 'src', 'styles', 'globals.css')) ||
1792
+ await readFileSafe(path.join(projectPath, 'app', 'globals.css')) ||
1793
+ await readFileSafe(path.join(projectPath, 'styles', 'globals.css'));
1794
+
1795
+ if (globalCss && /outline:\s*none|outline:\s*0/i.test(globalCss) && !/focus-visible/i.test(globalCss)) {
1796
+ issues.push({
1797
+ id: 'focus-removed',
1798
+ category: 'Accessibility',
1799
+ severity: 'high',
1800
+ title: 'Focus Outline Removed',
1801
+ description: 'Focus outlines are removed without alternative. Keyboard users can\'t see focus.',
1802
+ suggestion: 'Use :focus-visible instead of removing outlines entirely.',
1803
+ autoFixable: false,
1804
+ });
1805
+ }
1806
+
1807
+ return issues;
1808
+ },
1809
+
1810
+ // ─────────────────────────────────────────────────────────────────────────────
1811
+ // SEO CHECKER
1812
+ // ─────────────────────────────────────────────────────────────────────────────
1813
+ async seo(projectPath) {
1814
+ const issues = [];
1815
+
1816
+ // Check for robots.txt
1817
+ const hasRobots = await pathExists(path.join(projectPath, 'public', 'robots.txt'));
1818
+ if (!hasRobots) {
1819
+ issues.push({
1820
+ id: 'missing-robots',
1821
+ category: 'SEO',
1822
+ severity: 'medium',
1823
+ title: 'Missing robots.txt',
1824
+ description: 'No robots.txt file found. Search engines may not crawl correctly.',
1825
+ suggestion: 'Add robots.txt to public folder to guide search engine crawlers.',
1826
+ autoFixable: true,
1827
+ });
1828
+ }
1829
+
1830
+ // Check for sitemap
1831
+ const hasSitemap = await pathExists(path.join(projectPath, 'public', 'sitemap.xml')) ||
1832
+ await findFile(projectPath, /sitemap/i);
1833
+ if (!hasSitemap) {
1834
+ issues.push({
1835
+ id: 'missing-sitemap',
1836
+ category: 'SEO',
1837
+ severity: 'medium',
1838
+ title: 'Missing Sitemap',
1839
+ description: 'No sitemap found. Search engines may miss some pages.',
1840
+ suggestion: 'Generate sitemap.xml for better search engine indexing.',
1841
+ autoFixable: false,
1842
+ });
1843
+ }
1844
+
1845
+ // Check for meta description component or setup
1846
+ const hasMetaTags = await findFile(projectPath, /MetaTags|Seo|Head|metadata/i);
1847
+ if (!hasMetaTags) {
1848
+ issues.push({
1849
+ id: 'missing-meta-setup',
1850
+ category: 'SEO',
1851
+ severity: 'high',
1852
+ title: 'Missing Meta Tag Setup',
1853
+ description: 'No meta tag component found. Pages may lack proper SEO metadata.',
1854
+ suggestion: 'Add a SEO/MetaTags component for consistent meta descriptions.',
1855
+ autoFixable: true,
1856
+ });
1857
+ }
1858
+
1859
+ // Check for Open Graph images
1860
+ const hasOgImage = await pathExists(path.join(projectPath, 'public', 'og-image.png')) ||
1861
+ await pathExists(path.join(projectPath, 'public', 'og.png')) ||
1862
+ await findFile(path.join(projectPath, 'public'), /og[-_]?image/i);
1863
+ if (!hasOgImage) {
1864
+ issues.push({
1865
+ id: 'missing-og-image',
1866
+ category: 'SEO',
1867
+ severity: 'low',
1868
+ title: 'Missing Open Graph Image',
1869
+ description: 'No OG image found. Social media shares will look plain.',
1870
+ suggestion: 'Add og-image.png to public folder for social media previews.',
1871
+ autoFixable: false,
1872
+ });
1873
+ }
1874
+
1875
+ return issues;
1876
+ },
1877
+
1878
+ // ─────────────────────────────────────────────────────────────────────────────
1879
+ // CONFIGURATION CHECKER
1880
+ // ─────────────────────────────────────────────────────────────────────────────
1881
+ async configuration(projectPath) {
1882
+ const issues = [];
1883
+
1884
+ // Check for TypeScript strict mode
1885
+ const tsConfig = await readFileSafe(path.join(projectPath, 'tsconfig.json'));
1886
+ if (tsConfig) {
1887
+ const parsed = JSON.parse(tsConfig);
1888
+ if (!parsed.compilerOptions?.strict) {
1889
+ issues.push({
1890
+ id: 'ts-not-strict',
1891
+ category: 'Configuration',
1892
+ severity: 'medium',
1893
+ title: 'TypeScript Strict Mode Disabled',
1894
+ description: 'TypeScript strict mode is not enabled. Type safety is reduced.',
1895
+ suggestion: 'Enable "strict": true in tsconfig.json for better type safety.',
1896
+ autoFixable: true,
1897
+ });
1898
+ }
1899
+ }
1900
+
1901
+ // Check for ESLint config
1902
+ const hasEslint = await pathExists(path.join(projectPath, '.eslintrc.json')) ||
1903
+ await pathExists(path.join(projectPath, '.eslintrc.js')) ||
1904
+ await pathExists(path.join(projectPath, 'eslint.config.js'));
1905
+ if (!hasEslint) {
1906
+ issues.push({
1907
+ id: 'missing-eslint',
1908
+ category: 'Configuration',
1909
+ severity: 'medium',
1910
+ title: 'Missing ESLint Configuration',
1911
+ description: 'No ESLint config found. Code quality may not be enforced.',
1912
+ suggestion: 'Add ESLint configuration for code quality enforcement.',
1913
+ autoFixable: true,
1914
+ });
1915
+ }
1916
+
1917
+ // Check for Prettier
1918
+ const hasPrettier = await pathExists(path.join(projectPath, '.prettierrc')) ||
1919
+ await pathExists(path.join(projectPath, '.prettierrc.json')) ||
1920
+ await pathExists(path.join(projectPath, 'prettier.config.js'));
1921
+ if (!hasPrettier) {
1922
+ issues.push({
1923
+ id: 'missing-prettier',
1924
+ category: 'Configuration',
1925
+ severity: 'low',
1926
+ title: 'Missing Prettier Configuration',
1927
+ description: 'No Prettier config found. Code formatting may be inconsistent.',
1928
+ suggestion: 'Add Prettier configuration for consistent code formatting.',
1929
+ autoFixable: true,
1930
+ });
1931
+ }
1932
+
1933
+ // Check for editor config
1934
+ const hasEditorConfig = await pathExists(path.join(projectPath, '.editorconfig'));
1935
+ if (!hasEditorConfig) {
1936
+ issues.push({
1937
+ id: 'missing-editorconfig',
1938
+ category: 'Configuration',
1939
+ severity: 'low',
1940
+ title: 'Missing .editorconfig',
1941
+ description: 'No .editorconfig found. Different editors may format differently.',
1942
+ suggestion: 'Add .editorconfig for consistent editor settings across team.',
1943
+ autoFixable: true,
1944
+ });
1945
+ }
1946
+
1947
+ return issues;
1948
+ },
1949
+
1950
+ // ─────────────────────────────────────────────────────────────────────────────
1951
+ // DOCUMENTATION CHECKER
1952
+ // ─────────────────────────────────────────────────────────────────────────────
1953
+ async documentation(projectPath) {
1954
+ const issues = [];
1955
+
1956
+ // Check for README
1957
+ const hasReadme = await pathExists(path.join(projectPath, 'README.md'));
1958
+ if (!hasReadme) {
1959
+ issues.push({
1960
+ id: 'missing-readme',
1961
+ category: 'Documentation',
1962
+ severity: 'high',
1963
+ title: 'Missing README',
1964
+ description: 'No README.md found. New developers won\'t know how to start.',
1965
+ suggestion: 'Add README.md with project overview, setup, and usage instructions.',
1966
+ autoFixable: true,
1967
+ });
1968
+ } else {
1969
+ // Check README quality
1970
+ const readme = await readFileSafe(path.join(projectPath, 'README.md'));
1971
+ if (readme && readme.length < 500) {
1972
+ issues.push({
1973
+ id: 'insufficient-readme',
1974
+ category: 'Documentation',
1975
+ severity: 'medium',
1976
+ title: 'Insufficient README',
1977
+ description: 'README is very short. May be missing important sections.',
1978
+ suggestion: 'Expand README with installation, usage, API docs, and contributing guide.',
1979
+ autoFixable: false,
1980
+ });
1981
+ }
1982
+ }
1983
+
1984
+ // Check for CHANGELOG
1985
+ const hasChangelog = await pathExists(path.join(projectPath, 'CHANGELOG.md'));
1986
+ if (!hasChangelog) {
1987
+ issues.push({
1988
+ id: 'missing-changelog',
1989
+ category: 'Documentation',
1990
+ severity: 'low',
1991
+ title: 'Missing CHANGELOG',
1992
+ description: 'No CHANGELOG.md found. Version history is not documented.',
1993
+ suggestion: 'Add CHANGELOG.md to track version history and changes.',
1994
+ autoFixable: true,
1995
+ });
1996
+ }
1997
+
1998
+ // Check for CONTRIBUTING guide
1999
+ const hasContributing = await pathExists(path.join(projectPath, 'CONTRIBUTING.md'));
2000
+ if (!hasContributing) {
2001
+ issues.push({
2002
+ id: 'missing-contributing',
2003
+ category: 'Documentation',
2004
+ severity: 'low',
2005
+ title: 'Missing CONTRIBUTING Guide',
2006
+ description: 'No CONTRIBUTING.md found. Contributors won\'t know the process.',
2007
+ suggestion: 'Add CONTRIBUTING.md with contribution guidelines.',
2008
+ autoFixable: true,
2009
+ });
2010
+ }
2011
+
2012
+ // Check for LICENSE
2013
+ const hasLicense = await pathExists(path.join(projectPath, 'LICENSE')) ||
2014
+ await pathExists(path.join(projectPath, 'LICENSE.md'));
2015
+ if (!hasLicense) {
2016
+ issues.push({
2017
+ id: 'missing-license',
2018
+ category: 'Documentation',
2019
+ severity: 'medium',
2020
+ title: 'Missing LICENSE',
2021
+ description: 'No LICENSE file found. Legal usage of code is unclear.',
2022
+ suggestion: 'Add a LICENSE file to clarify code usage rights.',
2023
+ autoFixable: false,
2024
+ });
2025
+ }
2026
+
2027
+ return issues;
2028
+ },
2029
+
2030
+ // ─────────────────────────────────────────────────────────────────────────────
2031
+ // INFRASTRUCTURE CHECKER
2032
+ // ─────────────────────────────────────────────────────────────────────────────
2033
+ async infrastructure(projectPath) {
2034
+ const issues = [];
2035
+
2036
+ // Check for Dockerfile
2037
+ const hasDocker = await pathExists(path.join(projectPath, 'Dockerfile')) ||
2038
+ await pathExists(path.join(projectPath, 'docker-compose.yml'));
2039
+ if (!hasDocker) {
2040
+ issues.push({
2041
+ id: 'missing-docker',
2042
+ category: 'Infrastructure',
2043
+ severity: 'low',
2044
+ title: 'Missing Docker Configuration',
2045
+ description: 'No Docker setup found. Deployment may be inconsistent.',
2046
+ suggestion: 'Add Dockerfile for consistent deployment environments.',
2047
+ autoFixable: true,
2048
+ });
2049
+ }
2050
+
2051
+ // Check for CI/CD
2052
+ const hasGitHubActions = await pathExists(path.join(projectPath, '.github', 'workflows'));
2053
+ const hasGitLab = await pathExists(path.join(projectPath, '.gitlab-ci.yml'));
2054
+ const hasCircleCI = await pathExists(path.join(projectPath, '.circleci'));
2055
+
2056
+ if (!hasGitHubActions && !hasGitLab && !hasCircleCI) {
2057
+ issues.push({
2058
+ id: 'missing-ci',
2059
+ category: 'Infrastructure',
2060
+ severity: 'high',
2061
+ title: 'Missing CI/CD Configuration',
2062
+ description: 'No CI/CD pipeline found. Code changes are not automatically tested.',
2063
+ suggestion: 'Add GitHub Actions, GitLab CI, or CircleCI for automated testing.',
2064
+ autoFixable: true,
2065
+ });
2066
+ }
2067
+
2068
+ // Check for deployment config
2069
+ const hasVercel = await pathExists(path.join(projectPath, 'vercel.json'));
2070
+ const hasNetlify = await pathExists(path.join(projectPath, 'netlify.toml'));
2071
+ const hasRailway = await pathExists(path.join(projectPath, 'railway.json'));
2072
+
2073
+ if (!hasVercel && !hasNetlify && !hasRailway && !hasDocker) {
2074
+ issues.push({
2075
+ id: 'missing-deployment-config',
2076
+ category: 'Infrastructure',
2077
+ severity: 'medium',
2078
+ title: 'No Deployment Configuration',
2079
+ description: 'No deployment platform configuration found.',
2080
+ suggestion: 'Add vercel.json, netlify.toml, or similar for deployment settings.',
2081
+ autoFixable: false,
2082
+ });
2083
+ }
2084
+
2085
+ // Check for environment validation
2086
+ const packageJson = await readFileSafe(path.join(projectPath, 'package.json'));
2087
+ const hasEnvValidation = packageJson && /@t3-oss\/env|envalid|dotenv-safe/i.test(packageJson);
2088
+ if (!hasEnvValidation) {
2089
+ issues.push({
2090
+ id: 'missing-env-validation',
2091
+ category: 'Infrastructure',
2092
+ severity: 'medium',
2093
+ title: 'No Environment Validation',
2094
+ description: 'Environment variables are not validated at startup.',
2095
+ suggestion: 'Add @t3-oss/env-nextjs or envalid to validate env vars at startup.',
2096
+ autoFixable: false,
2097
+ });
2098
+ }
2099
+
2100
+ return issues;
2101
+ },
2102
+ };
2103
+
2104
+ // ═══════════════════════════════════════════════════════════════════════════════
2105
+ // POLISH SERVICE
2106
+ // ═══════════════════════════════════════════════════════════════════════════════
2107
+
2108
+ async function analyzeProject(projectPath, options = {}) {
2109
+ const allIssues = [];
2110
+ const categories = Object.keys(checkers);
2111
+ const selectedCategories = options.category
2112
+ ? [options.category.toLowerCase()]
2113
+ : categories;
2114
+
2115
+ for (const category of selectedCategories) {
2116
+ if (checkers[category]) {
2117
+ try {
2118
+ const issues = await checkers[category](projectPath);
2119
+ allIssues.push(...issues);
2120
+ } catch (error) {
2121
+ console.error(`${c.red}Error in ${category} checker:${c.reset}`, error.message);
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ // Calculate score
2127
+ let score = 100;
2128
+ for (const issue of allIssues) {
2129
+ switch (issue.severity) {
2130
+ case 'critical': score -= 10; break;
2131
+ case 'high': score -= 5; break;
2132
+ case 'medium': score -= 2; break;
2133
+ case 'low': score -= 1; break;
2134
+ }
2135
+ }
2136
+ score = Math.max(0, Math.min(100, score));
2137
+
2138
+ // Generate recommendations
2139
+ const recommendations = [];
2140
+ const criticalCount = allIssues.filter(i => i.severity === 'critical').length;
2141
+ const highCount = allIssues.filter(i => i.severity === 'high').length;
2142
+ const autoFixable = allIssues.filter(i => i.autoFixable).length;
2143
+
2144
+ if (criticalCount > 0) {
2145
+ recommendations.push(`Fix ${criticalCount} critical issue(s) immediately - these affect security or functionality.`);
2146
+ }
2147
+ if (highCount > 0) {
2148
+ recommendations.push(`Address ${highCount} high-priority issue(s) - these impact user experience or production readiness.`);
2149
+ }
2150
+ if (autoFixable > 0) {
2151
+ recommendations.push(`${autoFixable} issue(s) can be auto-fixed. Run 'vibecheck polish --fix' to apply fixes.`);
2152
+ }
2153
+ if (recommendations.length === 0) {
2154
+ recommendations.push('Your project looks polished! Great job! 🎉');
2155
+ }
2156
+
2157
+ return {
2158
+ projectPath,
2159
+ totalIssues: allIssues.length,
2160
+ critical: criticalCount,
2161
+ high: highCount,
2162
+ medium: allIssues.filter(i => i.severity === 'medium').length,
2163
+ low: allIssues.filter(i => i.severity === 'low').length,
2164
+ issues: allIssues,
2165
+ score,
2166
+ recommendations,
2167
+ };
2168
+ }
2169
+
2170
+ // ═══════════════════════════════════════════════════════════════════════════════
2171
+ // CLI INTERFACE
2172
+ // ═══════════════════════════════════════════════════════════════════════════════
2173
+
2174
+ function parseArgs(args) {
2175
+ const opts = {
2176
+ help: false,
2177
+ fix: false,
2178
+ json: false,
2179
+ prompts: false,
2180
+ verbose: false,
2181
+ category: null,
2182
+ path: process.cwd(),
2183
+ };
2184
+
2185
+ for (let i = 0; i < args.length; i++) {
2186
+ const arg = args[i];
2187
+ if (arg === '--help' || arg === '-h') opts.help = true;
2188
+ else if (arg === '--fix' || arg === '-f') opts.fix = true;
2189
+ else if (arg === '--json') opts.json = true;
2190
+ else if (arg === '--prompts' || arg === '--ai') opts.prompts = true;
2191
+ else if (arg === '--verbose' || arg === '-v') opts.verbose = true;
2192
+ else if (arg === '--category' || arg === '-c') opts.category = args[++i];
2193
+ else if (arg === '--path' || arg === '-p') opts.path = args[++i];
2194
+ else if (!arg.startsWith('-')) opts.path = arg;
2195
+ }
2196
+
2197
+ return opts;
2198
+ }
2199
+
2200
+ function printHelp() {
2201
+ console.log(`
2202
+ ${c.bold}${icons.sparkle} vibecheck polish${c.reset} - Production Polish Analyzer
2203
+
2204
+ ${c.dim}Finds all the small detailed things you forgot - the polish that makes
2205
+ projects production-ready.${c.reset}
2206
+
2207
+ ${c.bold}USAGE${c.reset}
2208
+ vibecheck polish [options] [path]
2209
+
2210
+ ${c.bold}OPTIONS${c.reset}
2211
+ -h, --help Show this help message
2212
+ -f, --fix Auto-fix issues where possible
2213
+ --prompts, --ai Show AI prompts for each issue (copy to AI assistant)
2214
+ -v, --verbose Show detailed output including AI prompts inline
2215
+ -c, --category <name> Check only specific category
2216
+ -p, --path <path> Project path (defaults to current directory)
2217
+ --json Output as JSON (includes AI prompts)
2218
+
2219
+ ${c.bold}CATEGORIES${c.reset}
2220
+ frontend Error boundaries, skeletons, ARIA, haptics, animations, caching
2221
+ backend Health endpoints, validation, rate limiting, error handling
2222
+ security .env files, security headers, CORS, secrets management
2223
+ performance Image optimization, caching, bundle analysis
2224
+ accessibility A11y testing, skip links, focus management
2225
+ seo Meta tags, sitemap, robots.txt, Open Graph
2226
+ configuration TypeScript, ESLint, Prettier, EditorConfig
2227
+ documentation README, CHANGELOG, CONTRIBUTING, LICENSE
2228
+ infrastructure Docker, CI/CD, deployment config, env validation
2229
+
2230
+ ${c.bold}EXAMPLES${c.reset}
2231
+ vibecheck polish # Analyze current project
2232
+ vibecheck polish --prompts # Show AI prompts for all issues
2233
+ vibecheck polish -c frontend --ai # Frontend issues with AI prompts
2234
+ vibecheck polish --fix # Auto-fix issues
2235
+ vibecheck polish --json # Output JSON for CI
2236
+
2237
+ ${c.bold}AI PROMPTS${c.reset}
2238
+ Each issue includes an AI-ready prompt you can copy to Cursor, Copilot, or Claude.
2239
+ Use --prompts to see all prompts, or --json for machine-readable output.
2240
+
2241
+ ${c.bold}TIER${c.reset}
2242
+ ${c.cyan}Free${c.reset} - Available on all tiers
2243
+ `);
2244
+ }
2245
+
2246
+ function formatScore(score) {
2247
+ if (score >= 90) return `${c.green}${c.bold}${score}/100${c.reset} ${c.green}(Excellent)${c.reset}`;
2248
+ if (score >= 70) return `${c.yellow}${c.bold}${score}/100${c.reset} ${c.yellow}(Good)${c.reset}`;
2249
+ if (score >= 50) return `${c.yellow}${c.bold}${score}/100${c.reset} ${c.yellow}(Needs Work)${c.reset}`;
2250
+ return `${c.red}${c.bold}${score}/100${c.reset} ${c.red}(Critical Issues)${c.reset}`;
2251
+ }
2252
+
2253
+ function formatSeverity(severity) {
2254
+ const styles = {
2255
+ critical: `${icons.critical} ${c.red}${c.bold}CRITICAL${c.reset}`,
2256
+ high: `${icons.high} ${c.yellow}${c.bold}HIGH${c.reset}`,
2257
+ medium: `${icons.medium} ${c.yellow}MEDIUM${c.reset}`,
2258
+ low: `${icons.low} ${c.blue}LOW${c.reset}`,
2259
+ };
2260
+ return styles[severity] || severity;
2261
+ }
2262
+
2263
+ function getCategoryIcon(category) {
2264
+ const categoryIcons = {
2265
+ Frontend: icons.sparkle,
2266
+ Backend: icons.server,
2267
+ Security: icons.lock,
2268
+ Performance: icons.lightning,
2269
+ Accessibility: icons.eye,
2270
+ SEO: icons.search,
2271
+ Configuration: icons.gear,
2272
+ Documentation: icons.book,
2273
+ Infrastructure: icons.rocket,
2274
+ };
2275
+ return categoryIcons[category] || icons.star;
2276
+ }
2277
+
2278
+ async function runPolish(args) {
2279
+ const opts = parseArgs(args);
2280
+
2281
+ if (opts.help) {
2282
+ printHelp();
2283
+ return 0;
2284
+ }
2285
+
2286
+ const projectPath = path.resolve(opts.path);
2287
+
2288
+ // Verify project exists
2289
+ if (!await pathExists(projectPath)) {
2290
+ console.error(`${c.red}${icons.cross} Project path does not exist: ${projectPath}${c.reset}`);
2291
+ return 1;
2292
+ }
2293
+
2294
+ // JSON mode
2295
+ if (opts.json) {
2296
+ const report = await analyzeProject(projectPath, opts);
2297
+ console.log(JSON.stringify(report, null, 2));
2298
+ return report.critical > 0 ? 1 : 0;
2299
+ }
2300
+
2301
+ // Interactive mode
2302
+ console.log(`
2303
+ ${c.bold}╔══════════════════════════════════════════════════════════════════════════════╗
2304
+ ║ ║
2305
+ ║ ${icons.sparkle} ${c.cyan}VIBECHECK POLISH${c.reset}${c.bold} - Production Readiness Analyzer ║
2306
+ ║ ║
2307
+ ║ ${c.dim}Finds all the small detailed things you forgot - the polish that${c.reset}${c.bold} ║
2308
+ ║ ${c.dim}makes projects production-ready.${c.reset}${c.bold} ║
2309
+ ║ ║
2310
+ ╚══════════════════════════════════════════════════════════════════════════════╝${c.reset}
2311
+ `);
2312
+
2313
+ console.log(`${c.dim}Project:${c.reset} ${projectPath}\n`);
2314
+
2315
+ // Show spinner
2316
+ const isTTY = process.stdout.isTTY;
2317
+ if (isTTY) {
2318
+ process.stdout.write(`${icons.search} Scanning project for polish issues...`);
2319
+ } else {
2320
+ console.log(`${icons.search} Scanning project for polish issues...`);
2321
+ }
2322
+
2323
+ const report = await analyzeProject(projectPath, opts);
2324
+
2325
+ // Clear spinner line
2326
+ if (isTTY && process.stdout.clearLine) {
2327
+ process.stdout.clearLine(0);
2328
+ process.stdout.cursorTo(0);
2329
+ }
2330
+
2331
+ // Summary
2332
+ console.log(`${c.bold}${icons.star} POLISH REPORT SUMMARY${c.reset}\n`);
2333
+ console.log(` ${c.bold}Score:${c.reset} ${formatScore(report.score)}`);
2334
+ console.log(` ${c.bold}Issues:${c.reset} ${report.totalIssues} total`);
2335
+ console.log(` ${icons.critical} ${report.critical} critical ${icons.high} ${report.high} high ${icons.medium} ${report.medium} medium ${icons.low} ${report.low} low\n`);
2336
+
2337
+ if (report.issues.length === 0) {
2338
+ console.log(`\n${c.green}${c.bold}${icons.check} Perfect!${c.reset} No polish issues found. Your project is production-ready! ${icons.rocket}\n`);
2339
+ return 0;
2340
+ }
2341
+
2342
+ // Group by category
2343
+ const byCategory = {};
2344
+ for (const issue of report.issues) {
2345
+ if (!byCategory[issue.category]) {
2346
+ byCategory[issue.category] = [];
2347
+ }
2348
+ byCategory[issue.category].push(issue);
2349
+ }
2350
+
2351
+ // Show issues by category
2352
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
2353
+ console.log(`${c.bold}${icons.wrench} ISSUES BY CATEGORY${c.reset}`);
2354
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
2355
+
2356
+ const issuesWithPrompts = [];
2357
+
2358
+ for (const [category, issues] of Object.entries(byCategory)) {
2359
+ console.log(`${c.bold}${getCategoryIcon(category)} ${category}${c.reset} ${c.dim}(${issues.length} issues)${c.reset}`);
2360
+ console.log(`${'─'.repeat(60)}`);
2361
+
2362
+ for (const issue of issues) {
2363
+ console.log(`\n ${formatSeverity(issue.severity)}`);
2364
+ console.log(` ${c.bold}${issue.title}${c.reset}`);
2365
+ console.log(` ${c.dim}${issue.description}${c.reset}`);
2366
+ if (issue.file) {
2367
+ console.log(` ${c.dim}File: ${path.relative(projectPath, issue.file)}${c.reset}`);
2368
+ }
2369
+ console.log(` ${c.cyan}${icons.arrow} ${issue.suggestion}${c.reset}`);
2370
+ if (issue.autoFixable) {
2371
+ console.log(` ${c.green}${icons.check} Auto-fixable${c.reset}`);
2372
+ }
2373
+ if (issue.aiPrompt) {
2374
+ issuesWithPrompts.push(issue);
2375
+ if (opts.verbose) {
2376
+ console.log(`\n ${c.magenta}${c.bold}🤖 AI PROMPT:${c.reset}`);
2377
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
2378
+ const promptLines = issue.aiPrompt.split('\n');
2379
+ for (const line of promptLines) {
2380
+ console.log(` ${c.dim}${line}${c.reset}`);
2381
+ }
2382
+ console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
2383
+ } else {
2384
+ console.log(` ${c.magenta}🤖 Has AI prompt${c.reset} ${c.dim}(use --prompts to see)${c.reset}`);
2385
+ }
2386
+ }
2387
+ }
2388
+ console.log();
2389
+ }
2390
+
2391
+ // Recommendations
2392
+ if (report.recommendations.length > 0) {
2393
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
2394
+ console.log(`${c.bold}💡 RECOMMENDATIONS${c.reset}`);
2395
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
2396
+ report.recommendations.forEach((rec, i) => {
2397
+ console.log(` ${i + 1}. ${rec}`);
2398
+ });
2399
+ console.log();
2400
+ }
2401
+
2402
+ // Auto-fix prompt
2403
+ const autoFixable = report.issues.filter(i => i.autoFixable);
2404
+ if (autoFixable.length > 0 && opts.fix) {
2405
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
2406
+ console.log(`${c.bold}${icons.wrench} AUTO-FIX${c.reset}`);
2407
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
2408
+ console.log(` ${c.yellow}Auto-fix for ${autoFixable.length} issue(s) is coming soon!${c.reset}`);
2409
+ console.log(` ${c.dim}For now, please follow the suggestions above to fix issues manually.${c.reset}\n`);
2410
+ } else if (autoFixable.length > 0) {
2411
+ console.log(`${c.dim}Tip: Run 'vibecheck polish --fix' to auto-fix ${autoFixable.length} issue(s)${c.reset}\n`);
2412
+ }
2413
+
2414
+ // AI Prompts section (when --prompts flag is used)
2415
+ if (opts.prompts && issuesWithPrompts.length > 0) {
2416
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
2417
+ console.log(`${c.bold}🤖 AI PROMPTS - Copy to your AI assistant${c.reset}`);
2418
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
2419
+ console.log(`${c.dim}Copy these prompts to Cursor, Copilot, Claude, or your preferred AI assistant.${c.reset}\n`);
2420
+
2421
+ for (let i = 0; i < issuesWithPrompts.length; i++) {
2422
+ const issue = issuesWithPrompts[i];
2423
+ console.log(`${c.bold}╭───────────────────────────────────────────────────────────────────────────────╮${c.reset}`);
2424
+ console.log(`${c.bold}│${c.reset} ${c.cyan}[${i + 1}/${issuesWithPrompts.length}]${c.reset} ${c.bold}${issue.title}${c.reset}`);
2425
+ console.log(`${c.bold}│${c.reset} ${c.dim}Severity: ${issue.severity} | Category: ${issue.category}${c.reset}`);
2426
+ console.log(`${c.bold}╰───────────────────────────────────────────────────────────────────────────────╯${c.reset}`);
2427
+ console.log();
2428
+ console.log(`${c.magenta}${issue.aiPrompt}${c.reset}`);
2429
+ console.log();
2430
+ console.log(`${c.dim}${'─'.repeat(79)}${c.reset}\n`);
2431
+ }
2432
+
2433
+ console.log(`${c.green}${c.bold}💡 Tip:${c.reset} Copy one prompt at a time and paste into your AI coding assistant.`);
2434
+ console.log(`${c.dim}The prompts are designed to give AI complete context to generate the right code.${c.reset}\n`);
2435
+ } else if (issuesWithPrompts.length > 0 && !opts.verbose) {
2436
+ console.log(`${c.dim}${icons.star} ${issuesWithPrompts.length} issue(s) have AI prompts. Run with ${c.cyan}--prompts${c.reset}${c.dim} to see them.${c.reset}\n`);
2437
+ }
2438
+
2439
+ // Next steps
2440
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
2441
+ console.log(`${c.bold}${icons.rocket} NEXT STEPS${c.reset}`);
2442
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
2443
+ console.log(` 1. Fix ${c.red}critical${c.reset} and ${c.yellow}high${c.reset} severity issues first`);
2444
+ console.log(` 2. Use ${c.cyan}vibecheck polish --prompts${c.reset} to get AI-ready fixes`);
2445
+ console.log(` 3. Copy prompts to your AI assistant (Cursor, Copilot, Claude)`);
2446
+ console.log(` 4. Re-run ${c.cyan}vibecheck polish${c.reset} to verify fixes`);
2447
+ console.log(` 5. Run ${c.cyan}vibecheck ship${c.reset} when score is 80+\n`);
2448
+
2449
+ return report.critical > 0 ? 1 : 0;
2450
+ }
2451
+
2452
+ module.exports = { runPolish };