kctl-react 0.6.2__py3-none-any.whl

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 (102) hide show
  1. kctl_react/__init__.py +3 -0
  2. kctl_react/__main__.py +5 -0
  3. kctl_react/cli.py +201 -0
  4. kctl_react/commands/__init__.py +0 -0
  5. kctl_react/commands/a11y.py +78 -0
  6. kctl_react/commands/affected.py +170 -0
  7. kctl_react/commands/apps.py +353 -0
  8. kctl_react/commands/build.py +376 -0
  9. kctl_react/commands/bundle_cmd.py +217 -0
  10. kctl_react/commands/cap.py +1465 -0
  11. kctl_react/commands/clean.py +76 -0
  12. kctl_react/commands/codegen.py +491 -0
  13. kctl_react/commands/compliance.py +587 -0
  14. kctl_react/commands/config_cmd.py +368 -0
  15. kctl_react/commands/dashboard.py +163 -0
  16. kctl_react/commands/deploy.py +318 -0
  17. kctl_react/commands/deps.py +792 -0
  18. kctl_react/commands/dev.py +96 -0
  19. kctl_react/commands/docker_cmd.py +73 -0
  20. kctl_react/commands/doctor.py +170 -0
  21. kctl_react/commands/e2e.py +343 -0
  22. kctl_react/commands/env.py +155 -0
  23. kctl_react/commands/i18n.py +310 -0
  24. kctl_react/commands/lint.py +306 -0
  25. kctl_react/commands/maintenance.py +308 -0
  26. kctl_react/commands/monitor_cmd.py +50 -0
  27. kctl_react/commands/observe.py +34 -0
  28. kctl_react/commands/packages.py +129 -0
  29. kctl_react/commands/perf.py +762 -0
  30. kctl_react/commands/pipeline.py +289 -0
  31. kctl_react/commands/pwa.py +193 -0
  32. kctl_react/commands/scaffold.py +323 -0
  33. kctl_react/commands/security.py +660 -0
  34. kctl_react/commands/skill_cmd.py +54 -0
  35. kctl_react/commands/state.py +254 -0
  36. kctl_react/commands/test_cmd.py +418 -0
  37. kctl_react/commands/ui_audit.py +889 -0
  38. kctl_react/core/__init__.py +0 -0
  39. kctl_react/core/analyzers.py +200 -0
  40. kctl_react/core/callbacks.py +70 -0
  41. kctl_react/core/compliance/__init__.py +3 -0
  42. kctl_react/core/compliance/api_check/__init__.py +3 -0
  43. kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
  44. kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
  45. kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
  46. kctl_react/core/compliance/api_check/checks/naming.py +60 -0
  47. kctl_react/core/compliance/api_check/checks/params.py +44 -0
  48. kctl_react/core/compliance/api_check/checks/requests.py +57 -0
  49. kctl_react/core/compliance/api_check/checks/types.py +55 -0
  50. kctl_react/core/compliance/api_check/hooks.py +133 -0
  51. kctl_react/core/compliance/api_check/matcher.py +55 -0
  52. kctl_react/core/compliance/api_check/schema.py +151 -0
  53. kctl_react/core/compliance/api_health/__init__.py +35 -0
  54. kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
  55. kctl_react/core/compliance/api_health/checks/auth.py +72 -0
  56. kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
  57. kctl_react/core/compliance/api_health/checks/response.py +55 -0
  58. kctl_react/core/compliance/api_health/checks/timing.py +38 -0
  59. kctl_react/core/compliance/api_health/client.py +99 -0
  60. kctl_react/core/compliance/api_health/sampler.py +16 -0
  61. kctl_react/core/compliance/checks/__init__.py +47 -0
  62. kctl_react/core/compliance/checks/api.py +101 -0
  63. kctl_react/core/compliance/checks/codegen.py +94 -0
  64. kctl_react/core/compliance/checks/darkmode.py +57 -0
  65. kctl_react/core/compliance/checks/errors.py +68 -0
  66. kctl_react/core/compliance/checks/features.py +66 -0
  67. kctl_react/core/compliance/checks/i18n_check.py +105 -0
  68. kctl_react/core/compliance/checks/imports.py +86 -0
  69. kctl_react/core/compliance/checks/navigation.py +62 -0
  70. kctl_react/core/compliance/checks/practices.py +122 -0
  71. kctl_react/core/compliance/checks/providers.py +85 -0
  72. kctl_react/core/compliance/checks/pwa.py +101 -0
  73. kctl_react/core/compliance/checks/responsive.py +47 -0
  74. kctl_react/core/compliance/checks/scripts.py +85 -0
  75. kctl_react/core/compliance/checks/shadcn.py +51 -0
  76. kctl_react/core/compliance/checks/structure.py +76 -0
  77. kctl_react/core/compliance/checks/testing.py +83 -0
  78. kctl_react/core/compliance/checks/theme.py +92 -0
  79. kctl_react/core/compliance/checks/ui_standard.py +185 -0
  80. kctl_react/core/compliance/checks/vite.py +83 -0
  81. kctl_react/core/compliance/engine.py +87 -0
  82. kctl_react/core/compliance/exceptions_map.py +15 -0
  83. kctl_react/core/compliance/fixes/__init__.py +33 -0
  84. kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
  85. kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
  86. kctl_react/core/compliance/fixes/imports_fix.py +36 -0
  87. kctl_react/core/compliance/fixes/structure_fix.py +20 -0
  88. kctl_react/core/compliance/fixes/theme_fix.py +29 -0
  89. kctl_react/core/compliance/models.py +106 -0
  90. kctl_react/core/config.py +201 -0
  91. kctl_react/core/discovery.py +185 -0
  92. kctl_react/core/exceptions.py +17 -0
  93. kctl_react/core/git.py +146 -0
  94. kctl_react/core/history.py +121 -0
  95. kctl_react/core/output.py +5 -0
  96. kctl_react/core/plugins.py +13 -0
  97. kctl_react/core/runner.py +34 -0
  98. kctl_react/py.typed +0 -0
  99. kctl_react-0.6.2.dist-info/METADATA +17 -0
  100. kctl_react-0.6.2.dist-info/RECORD +102 -0
  101. kctl_react-0.6.2.dist-info/WHEEL +4 -0
  102. kctl_react-0.6.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,86 @@
1
+ """Checker: import conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+ _SKIP_PARTS = {"node_modules", "generated", "__tests__"}
11
+
12
+
13
+ def _should_skip(file_path: Path) -> bool:
14
+ return bool(_SKIP_PARTS & set(file_path.parts))
15
+
16
+
17
+ class ImportsChecker:
18
+ name = "imports"
19
+ label = "Import Conventions"
20
+ max_points = 6
21
+
22
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
23
+ violations: list[Violation] = []
24
+ src = app_path / "src"
25
+ if not src.is_dir():
26
+ return CategoryResult(
27
+ name=self.name,
28
+ label=self.label,
29
+ max_points=self.max_points,
30
+ violations=violations,
31
+ )
32
+
33
+ for ext in ("*.ts", "*.tsx"):
34
+ for f in src.rglob(ext):
35
+ if _should_skip(f):
36
+ continue
37
+ rel = str(f.relative_to(app_path))
38
+ try:
39
+ lines = f.read_text().splitlines()
40
+ except OSError:
41
+ continue
42
+
43
+ for i, line in enumerate(lines, 1):
44
+ # "use client" directive
45
+ if re.search(r'"use client"', line) or re.search(r"'use client'", line):
46
+ violations.append(
47
+ Violation(
48
+ file=rel,
49
+ line=i,
50
+ message='"use client" directive not needed in Vite apps',
51
+ fix_hint="Remove the directive",
52
+ auto_fixable=True,
53
+ )
54
+ )
55
+
56
+ # Deep relative imports ../../
57
+ if re.search(r'from\s+["\']\.\.\/\.\.', line):
58
+ violations.append(
59
+ Violation(
60
+ file=rel,
61
+ line=i,
62
+ message="Deep relative import (../../) — use @/ alias",
63
+ fix_hint="Replace with @/ path alias",
64
+ auto_fixable=True,
65
+ )
66
+ )
67
+
68
+ # Direct @/generated/ imports outside types/api.ts
69
+ if re.search(r'from\s+["\']@/generated/', line):
70
+ if not rel.endswith("types/api.ts"):
71
+ violations.append(
72
+ Violation(
73
+ file=rel,
74
+ line=i,
75
+ message="Direct @/generated/ import — use @/types/api instead",
76
+ fix_hint="Import from @/types/api",
77
+ auto_fixable=True,
78
+ )
79
+ )
80
+
81
+ return CategoryResult(
82
+ name=self.name,
83
+ label=self.label,
84
+ max_points=self.max_points,
85
+ violations=violations,
86
+ )
@@ -0,0 +1,62 @@
1
+ """Checker: navigation config and lazy loading."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class NavigationChecker:
12
+ name = "navigation"
13
+ label = "Navigation"
14
+ max_points = 4
15
+
16
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
17
+ violations: list[Violation] = []
18
+ src = app_path / "src"
19
+
20
+ # navConfig.tsx must exist with expected exports
21
+ nav = src / "config" / "navConfig.tsx"
22
+ if nav.is_file():
23
+ content = nav.read_text()
24
+ for fn_name in ("useDesktopNavGroups", "useMobileNavItems"):
25
+ if fn_name not in content:
26
+ violations.append(
27
+ Violation(
28
+ file="src/config/navConfig.tsx",
29
+ message=f"Missing {fn_name} in navConfig",
30
+ fix_hint=f"Export {fn_name} from navConfig.tsx",
31
+ )
32
+ )
33
+ else:
34
+ violations.append(
35
+ Violation(
36
+ file="src/config/navConfig.tsx",
37
+ message="Missing navConfig.tsx",
38
+ )
39
+ )
40
+
41
+ # App.tsx should use React.lazy() for pages
42
+ app_tsx = src / "App.tsx"
43
+ if app_tsx.is_file():
44
+ content = app_tsx.read_text()
45
+ # Check for non-lazy page imports (direct named imports from pages/)
46
+ direct_imports = re.findall(r'import\s+\{[^}]+\}\s+from\s+["\']@/pages/', content)
47
+ lazy_count = len(re.findall(r"React\.lazy\(|lazy\(\s*\(\)\s*=>", content))
48
+ if direct_imports and lazy_count == 0:
49
+ violations.append(
50
+ Violation(
51
+ file="src/App.tsx",
52
+ message="Pages imported directly without React.lazy()",
53
+ fix_hint="Use React.lazy(() => import('@/pages/...')) for code splitting",
54
+ )
55
+ )
56
+
57
+ return CategoryResult(
58
+ name=self.name,
59
+ label=self.label,
60
+ max_points=self.max_points,
61
+ violations=violations,
62
+ )
@@ -0,0 +1,122 @@
1
+ """Checker: coding best practices."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+ _SKIP_PARTS = {"node_modules", "__tests__", "generated"}
11
+
12
+
13
+ def _should_skip(file_path: Path) -> bool:
14
+ return bool(_SKIP_PARTS & set(file_path.parts))
15
+
16
+
17
+ class PracticesChecker:
18
+ name = "practices"
19
+ label = "Best Practices"
20
+ max_points = 4
21
+
22
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
23
+ violations: list[Violation] = []
24
+ src = app_path / "src"
25
+ if not src.is_dir():
26
+ return CategoryResult(
27
+ name=self.name,
28
+ label=self.label,
29
+ max_points=self.max_points,
30
+ violations=violations,
31
+ )
32
+
33
+ for ext in ("*.ts", "*.tsx"):
34
+ for f in src.rglob(ext):
35
+ if _should_skip(f):
36
+ continue
37
+ try:
38
+ lines = f.read_text().splitlines()
39
+ except OSError:
40
+ continue
41
+ rel = str(f.relative_to(app_path))
42
+
43
+ is_user_code = "pages" in f.parts or "components" in f.parts
44
+ for i, line in enumerate(lines, 1):
45
+ stripped = line.lstrip()
46
+ if stripped.startswith("//") or stripped.startswith("*"):
47
+ continue
48
+
49
+ # console.log in production code (pages/components only)
50
+ if is_user_code and re.search(r"\bconsole\.log\s*\(", line):
51
+ violations.append(
52
+ Violation(
53
+ file=rel,
54
+ line=i,
55
+ message="console.log() in production code",
56
+ fix_hint="Remove or replace with Sentry.captureMessage",
57
+ )
58
+ )
59
+
60
+ # : any type annotation (pages/components only)
61
+ if is_user_code and re.search(r":\s*any\b", line):
62
+ violations.append(
63
+ Violation(
64
+ file=rel,
65
+ line=i,
66
+ message=": any type annotation — use proper types",
67
+ fix_hint="Replace with specific type",
68
+ )
69
+ )
70
+
71
+ # process.env instead of import.meta.env (Vite apps)
72
+ if re.search(r"\bprocess\.env\b", line):
73
+ violations.append(
74
+ Violation(
75
+ file=rel,
76
+ line=i,
77
+ message="process.env — use import.meta.env in Vite apps",
78
+ fix_hint="Replace process.env.X with import.meta.env.VITE_X",
79
+ auto_fixable=False,
80
+ )
81
+ )
82
+
83
+ # Hardcoded localhost URLs in non-config files
84
+ if is_user_code and re.search(r"https?://localhost[:\d/]", line):
85
+ violations.append(
86
+ Violation(
87
+ file=rel,
88
+ line=i,
89
+ message="Hardcoded localhost URL — use env variable",
90
+ fix_hint="Use import.meta.env.VITE_API_BASE_URL",
91
+ )
92
+ )
93
+
94
+ # Whole library imports (lodash, date-fns, etc.)
95
+ if re.search(r'import\s+\w+\s+from\s+["\'](?:lodash|moment|date-fns)["\']', line):
96
+ violations.append(
97
+ Violation(
98
+ file=rel,
99
+ line=i,
100
+ message="Whole library import — use named imports for tree-shaking",
101
+ fix_hint='Use import { fn } from "library" instead',
102
+ )
103
+ )
104
+
105
+ # export default check for page files
106
+ if "pages" in f.parts:
107
+ text = "\n".join(lines)
108
+ if re.search(r"\bexport\s+default\b", text):
109
+ violations.append(
110
+ Violation(
111
+ file=rel,
112
+ message="export default in page — use named export",
113
+ fix_hint="Use named export instead of export default",
114
+ )
115
+ )
116
+
117
+ return CategoryResult(
118
+ name=self.name,
119
+ label=self.label,
120
+ max_points=self.max_points,
121
+ violations=violations,
122
+ )
@@ -0,0 +1,85 @@
1
+ """Checker: provider stack order and completeness in main.tsx."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.exceptions_map import get_skip_providers
9
+ from kctl_react.core.compliance.models import CategoryResult, Violation
10
+
11
+ EXPECTED_ORDER = [
12
+ "ErrorBoundary",
13
+ "PwaUpdatePrompt",
14
+ "QueryClientProvider",
15
+ "BrowserRouter",
16
+ "AuthProvider",
17
+ "GPSProvider",
18
+ "TooltipProvider",
19
+ "OfflineProvider",
20
+ "AiProvider",
21
+ "App",
22
+ ]
23
+
24
+
25
+ class ProviderChecker:
26
+ name = "providers"
27
+ label = "Provider Stack"
28
+ max_points = 8
29
+
30
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
31
+ violations: list[Violation] = []
32
+ main_tsx = app_path / "src" / "main.tsx"
33
+
34
+ if not main_tsx.is_file():
35
+ violations.append(Violation(file="src/main.tsx", message="main.tsx not found"))
36
+ return CategoryResult(
37
+ name=self.name,
38
+ label=self.label,
39
+ max_points=self.max_points,
40
+ violations=violations,
41
+ )
42
+
43
+ content = main_tsx.read_text()
44
+ skip = get_skip_providers(app_name)
45
+ expected = [p for p in EXPECTED_ORDER if p not in skip]
46
+
47
+ # Find positions of each provider in the file
48
+ positions: dict[str, int] = {}
49
+ for provider in expected:
50
+ # Match <Provider or <Provider> or {Provider}
51
+ pattern = rf"<{provider}[\s>/]"
52
+ match = re.search(pattern, content)
53
+ if match:
54
+ positions[provider] = match.start()
55
+
56
+ # Check missing providers
57
+ for provider in expected:
58
+ if provider not in positions:
59
+ violations.append(
60
+ Violation(
61
+ file="src/main.tsx",
62
+ message=f"Missing provider: {provider}",
63
+ fix_hint=f"Add <{provider}> to the provider stack",
64
+ )
65
+ )
66
+
67
+ # Check order of found providers
68
+ found_ordered = sorted(positions.keys(), key=lambda p: positions[p])
69
+ expected_found = [p for p in expected if p in positions]
70
+
71
+ if found_ordered != expected_found:
72
+ violations.append(
73
+ Violation(
74
+ file="src/main.tsx",
75
+ message=f"Provider order incorrect. Expected: {' > '.join(expected_found)}, got: {' > '.join(found_ordered)}",
76
+ fix_hint="Reorder providers to match expected stack",
77
+ )
78
+ )
79
+
80
+ return CategoryResult(
81
+ name=self.name,
82
+ label=self.label,
83
+ max_points=self.max_points,
84
+ violations=violations,
85
+ )
@@ -0,0 +1,101 @@
1
+ """Checker: PWA configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.models import CategoryResult, Violation
8
+
9
+ PWA_META_TAGS = [
10
+ "mobile-web-app-capable",
11
+ "apple-mobile-web-app-capable",
12
+ "viewport-fit=cover",
13
+ ]
14
+
15
+
16
+ class PwaChecker:
17
+ name = "pwa"
18
+ label = "PWA Configuration"
19
+ max_points = 6
20
+
21
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
22
+ violations: list[Violation] = []
23
+
24
+ # index.html checks
25
+ index_html = app_path / "index.html"
26
+ if index_html.is_file():
27
+ content = index_html.read_text()
28
+
29
+ for tag in PWA_META_TAGS:
30
+ if tag not in content:
31
+ violations.append(
32
+ Violation(
33
+ file="index.html",
34
+ message=f"Missing PWA meta tag: {tag}",
35
+ fix_hint=f"Add {tag} meta tag to index.html",
36
+ )
37
+ )
38
+
39
+ if "manifest" not in content:
40
+ violations.append(
41
+ Violation(
42
+ file="index.html",
43
+ message="Missing manifest reference in index.html",
44
+ fix_hint="Add <link rel='manifest' href='manifest.webmanifest'>",
45
+ )
46
+ )
47
+ else:
48
+ violations.append(Violation(file="index.html", message="Missing index.html"))
49
+
50
+ # Public icons
51
+ public_dir = app_path / "public"
52
+ if public_dir.is_dir():
53
+ icon_files = (
54
+ list(public_dir.glob("favicon*"))
55
+ + list(public_dir.glob("icon*"))
56
+ + list(public_dir.glob("*.png"))
57
+ + list(public_dir.glob("*.svg"))
58
+ )
59
+ if not icon_files:
60
+ violations.append(
61
+ Violation(
62
+ file="public/",
63
+ message="No icon files found in public/",
64
+ fix_hint="Add favicon and PWA icons to public/",
65
+ )
66
+ )
67
+ else:
68
+ violations.append(
69
+ Violation(
70
+ file="public/",
71
+ message="Missing public/ directory",
72
+ )
73
+ )
74
+
75
+ # main.tsx PwaUpdatePrompt + useRegisterSW
76
+ main_tsx = app_path / "src" / "main.tsx"
77
+ if main_tsx.is_file():
78
+ content = main_tsx.read_text()
79
+ if "PwaUpdatePrompt" not in content:
80
+ violations.append(
81
+ Violation(
82
+ file="src/main.tsx",
83
+ message="Missing PwaUpdatePrompt in main.tsx",
84
+ fix_hint="Add PwaUpdatePrompt component",
85
+ )
86
+ )
87
+ if "useRegisterSW" not in content:
88
+ violations.append(
89
+ Violation(
90
+ file="src/main.tsx",
91
+ message="Missing useRegisterSW in main.tsx",
92
+ fix_hint="Add useRegisterSW from virtual:pwa-register/react",
93
+ )
94
+ )
95
+
96
+ return CategoryResult(
97
+ name=self.name,
98
+ label=self.label,
99
+ max_points=self.max_points,
100
+ violations=violations,
101
+ )
@@ -0,0 +1,47 @@
1
+ """Checker: responsive layout setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.compliance.models import CategoryResult, Violation
8
+
9
+
10
+ class ResponsiveChecker:
11
+ name = "responsive"
12
+ label = "Responsive Layout"
13
+ max_points = 6
14
+
15
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
16
+ violations: list[Violation] = []
17
+ src = app_path / "src"
18
+
19
+ # AppLayout.tsx checks
20
+ layout = src / "components" / "AppLayout.tsx"
21
+ if layout.is_file():
22
+ content = layout.read_text()
23
+ for token in ("useIsMobile", "DesktopLayout", "MobileLayout"):
24
+ if token not in content:
25
+ violations.append(
26
+ Violation(
27
+ file="src/components/AppLayout.tsx",
28
+ message=f"Missing {token} in AppLayout",
29
+ fix_hint=f"Add {token} for responsive switching",
30
+ )
31
+ )
32
+ else:
33
+ violations.append(
34
+ Violation(
35
+ file="src/components/AppLayout.tsx",
36
+ message="Missing AppLayout.tsx",
37
+ )
38
+ )
39
+
40
+ # navConfig.tsx checks are in navigation checker — skip here to avoid duplicates
41
+
42
+ return CategoryResult(
43
+ name=self.name,
44
+ label=self.label,
45
+ max_points=self.max_points,
46
+ violations=violations,
47
+ )
@@ -0,0 +1,85 @@
1
+ """Checker: package.json scripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+ REQUIRED_SCRIPTS = [
11
+ "dev",
12
+ "build",
13
+ "preview",
14
+ "lint",
15
+ "type-check",
16
+ "test",
17
+ "fetch:schema",
18
+ "generate:api",
19
+ ]
20
+
21
+ HOOK_SCRIPTS = ["predev", "prebuild"]
22
+
23
+
24
+ class ScriptsChecker:
25
+ name = "scripts"
26
+ label = "Package Scripts"
27
+ max_points = 4
28
+
29
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
30
+ violations: list[Violation] = []
31
+
32
+ pkg = app_path / "package.json"
33
+ if not pkg.is_file():
34
+ violations.append(Violation(file="package.json", message="Missing package.json"))
35
+ return CategoryResult(
36
+ name=self.name,
37
+ label=self.label,
38
+ max_points=self.max_points,
39
+ violations=violations,
40
+ )
41
+
42
+ try:
43
+ data = json.loads(pkg.read_text())
44
+ except (json.JSONDecodeError, OSError):
45
+ violations.append(
46
+ Violation(
47
+ file="package.json",
48
+ message="Failed to parse package.json",
49
+ )
50
+ )
51
+ return CategoryResult(
52
+ name=self.name,
53
+ label=self.label,
54
+ max_points=self.max_points,
55
+ violations=violations,
56
+ )
57
+
58
+ scripts = data.get("scripts", {})
59
+
60
+ for script in REQUIRED_SCRIPTS:
61
+ if script not in scripts:
62
+ violations.append(
63
+ Violation(
64
+ file="package.json",
65
+ message=f'Missing required script: "{script}"',
66
+ fix_hint=f"Add {script} script to package.json",
67
+ )
68
+ )
69
+
70
+ for hook in HOOK_SCRIPTS:
71
+ if hook not in scripts:
72
+ violations.append(
73
+ Violation(
74
+ file="package.json",
75
+ message=f'Missing hook script: "{hook}"',
76
+ fix_hint=f"Add {hook} hook to package.json",
77
+ )
78
+ )
79
+
80
+ return CategoryResult(
81
+ name=self.name,
82
+ label=self.label,
83
+ max_points=self.max_points,
84
+ violations=violations,
85
+ )
@@ -0,0 +1,51 @@
1
+ """Checker: shadcn/ui component usage (no raw HTML elements)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kctl_react.core.analyzers import find_raw_html_elements
8
+ from kctl_react.core.compliance.models import CategoryResult, Violation
9
+
10
+
11
+ class ShadcnChecker:
12
+ name = "shadcn"
13
+ label = "shadcn/ui Usage"
14
+ max_points = 8
15
+
16
+ def check(self, app_path: Path, app_name: str) -> CategoryResult:
17
+ violations: list[Violation] = []
18
+ src = app_path / "src"
19
+
20
+ for subdir in ("pages", "components"):
21
+ scan_dir = src / subdir
22
+ if not scan_dir.is_dir():
23
+ continue
24
+ for tsx_file in scan_dir.rglob("*.tsx"):
25
+ # Skip test files
26
+ if "__tests__" in tsx_file.parts:
27
+ continue
28
+ results = find_raw_html_elements(tsx_file)
29
+ for r in results:
30
+ # Skip title= prop on JSX components (title={...} is a
31
+ # component prop, not an HTML tooltip attribute)
32
+ if r["element"] == "title= attr":
33
+ code = str(r.get("code", ""))
34
+ if "title={" in code:
35
+ continue
36
+ rel = str(tsx_file.relative_to(app_path))
37
+ violations.append(
38
+ Violation(
39
+ file=rel,
40
+ line=int(r["line"]),
41
+ message=f"Raw <{r['element']}> — use {r['replacement']}",
42
+ fix_hint=f"Replace <{r['element']}> with {r['replacement']}",
43
+ )
44
+ )
45
+
46
+ return CategoryResult(
47
+ name=self.name,
48
+ label=self.label,
49
+ max_points=self.max_points,
50
+ violations=violations,
51
+ )