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.
- kctl_react/__init__.py +3 -0
- kctl_react/__main__.py +5 -0
- kctl_react/cli.py +201 -0
- kctl_react/commands/__init__.py +0 -0
- kctl_react/commands/a11y.py +78 -0
- kctl_react/commands/affected.py +170 -0
- kctl_react/commands/apps.py +353 -0
- kctl_react/commands/build.py +376 -0
- kctl_react/commands/bundle_cmd.py +217 -0
- kctl_react/commands/cap.py +1465 -0
- kctl_react/commands/clean.py +76 -0
- kctl_react/commands/codegen.py +491 -0
- kctl_react/commands/compliance.py +587 -0
- kctl_react/commands/config_cmd.py +368 -0
- kctl_react/commands/dashboard.py +163 -0
- kctl_react/commands/deploy.py +318 -0
- kctl_react/commands/deps.py +792 -0
- kctl_react/commands/dev.py +96 -0
- kctl_react/commands/docker_cmd.py +73 -0
- kctl_react/commands/doctor.py +170 -0
- kctl_react/commands/e2e.py +343 -0
- kctl_react/commands/env.py +155 -0
- kctl_react/commands/i18n.py +310 -0
- kctl_react/commands/lint.py +306 -0
- kctl_react/commands/maintenance.py +308 -0
- kctl_react/commands/monitor_cmd.py +50 -0
- kctl_react/commands/observe.py +34 -0
- kctl_react/commands/packages.py +129 -0
- kctl_react/commands/perf.py +762 -0
- kctl_react/commands/pipeline.py +289 -0
- kctl_react/commands/pwa.py +193 -0
- kctl_react/commands/scaffold.py +323 -0
- kctl_react/commands/security.py +660 -0
- kctl_react/commands/skill_cmd.py +54 -0
- kctl_react/commands/state.py +254 -0
- kctl_react/commands/test_cmd.py +418 -0
- kctl_react/commands/ui_audit.py +889 -0
- kctl_react/core/__init__.py +0 -0
- kctl_react/core/analyzers.py +200 -0
- kctl_react/core/callbacks.py +70 -0
- kctl_react/core/compliance/__init__.py +3 -0
- kctl_react/core/compliance/api_check/__init__.py +3 -0
- kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
- kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
- kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
- kctl_react/core/compliance/api_check/checks/naming.py +60 -0
- kctl_react/core/compliance/api_check/checks/params.py +44 -0
- kctl_react/core/compliance/api_check/checks/requests.py +57 -0
- kctl_react/core/compliance/api_check/checks/types.py +55 -0
- kctl_react/core/compliance/api_check/hooks.py +133 -0
- kctl_react/core/compliance/api_check/matcher.py +55 -0
- kctl_react/core/compliance/api_check/schema.py +151 -0
- kctl_react/core/compliance/api_health/__init__.py +35 -0
- kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
- kctl_react/core/compliance/api_health/checks/auth.py +72 -0
- kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
- kctl_react/core/compliance/api_health/checks/response.py +55 -0
- kctl_react/core/compliance/api_health/checks/timing.py +38 -0
- kctl_react/core/compliance/api_health/client.py +99 -0
- kctl_react/core/compliance/api_health/sampler.py +16 -0
- kctl_react/core/compliance/checks/__init__.py +47 -0
- kctl_react/core/compliance/checks/api.py +101 -0
- kctl_react/core/compliance/checks/codegen.py +94 -0
- kctl_react/core/compliance/checks/darkmode.py +57 -0
- kctl_react/core/compliance/checks/errors.py +68 -0
- kctl_react/core/compliance/checks/features.py +66 -0
- kctl_react/core/compliance/checks/i18n_check.py +105 -0
- kctl_react/core/compliance/checks/imports.py +86 -0
- kctl_react/core/compliance/checks/navigation.py +62 -0
- kctl_react/core/compliance/checks/practices.py +122 -0
- kctl_react/core/compliance/checks/providers.py +85 -0
- kctl_react/core/compliance/checks/pwa.py +101 -0
- kctl_react/core/compliance/checks/responsive.py +47 -0
- kctl_react/core/compliance/checks/scripts.py +85 -0
- kctl_react/core/compliance/checks/shadcn.py +51 -0
- kctl_react/core/compliance/checks/structure.py +76 -0
- kctl_react/core/compliance/checks/testing.py +83 -0
- kctl_react/core/compliance/checks/theme.py +92 -0
- kctl_react/core/compliance/checks/ui_standard.py +185 -0
- kctl_react/core/compliance/checks/vite.py +83 -0
- kctl_react/core/compliance/engine.py +87 -0
- kctl_react/core/compliance/exceptions_map.py +15 -0
- kctl_react/core/compliance/fixes/__init__.py +33 -0
- kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
- kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
- kctl_react/core/compliance/fixes/imports_fix.py +36 -0
- kctl_react/core/compliance/fixes/structure_fix.py +20 -0
- kctl_react/core/compliance/fixes/theme_fix.py +29 -0
- kctl_react/core/compliance/models.py +106 -0
- kctl_react/core/config.py +201 -0
- kctl_react/core/discovery.py +185 -0
- kctl_react/core/exceptions.py +17 -0
- kctl_react/core/git.py +146 -0
- kctl_react/core/history.py +121 -0
- kctl_react/core/output.py +5 -0
- kctl_react/core/plugins.py +13 -0
- kctl_react/core/runner.py +34 -0
- kctl_react/py.typed +0 -0
- kctl_react-0.6.2.dist-info/METADATA +17 -0
- kctl_react-0.6.2.dist-info/RECORD +102 -0
- kctl_react-0.6.2.dist-info/WHEEL +4 -0
- kctl_react-0.6.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Checker: project file/directory structure."""
|
|
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
|
+
REQUIRED_DIRS = [
|
|
10
|
+
"api",
|
|
11
|
+
"pages",
|
|
12
|
+
"components",
|
|
13
|
+
"config",
|
|
14
|
+
"hooks",
|
|
15
|
+
"contexts",
|
|
16
|
+
"constants",
|
|
17
|
+
"types",
|
|
18
|
+
"i18n",
|
|
19
|
+
"utils",
|
|
20
|
+
"__tests__",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
REQUIRED_FILES = [
|
|
24
|
+
"main.tsx",
|
|
25
|
+
"App.tsx",
|
|
26
|
+
"config/navConfig.tsx",
|
|
27
|
+
"components/AppLayout.tsx",
|
|
28
|
+
"api/client.ts",
|
|
29
|
+
"types/api.ts",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StructureChecker:
|
|
34
|
+
name = "structure"
|
|
35
|
+
label = "File Structure"
|
|
36
|
+
max_points = 8
|
|
37
|
+
|
|
38
|
+
def check(self, app_path: Path, app_name: str) -> CategoryResult:
|
|
39
|
+
violations: list[Violation] = []
|
|
40
|
+
src = app_path / "src"
|
|
41
|
+
|
|
42
|
+
for d in REQUIRED_DIRS:
|
|
43
|
+
if not (src / d).is_dir():
|
|
44
|
+
violations.append(
|
|
45
|
+
Violation(
|
|
46
|
+
file=f"src/{d}/",
|
|
47
|
+
message=f"Missing required directory: src/{d}/",
|
|
48
|
+
fix_hint=f"mkdir -p apps/{app_name}/src/{d}",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
for f in REQUIRED_FILES:
|
|
53
|
+
if not (src / f).is_file():
|
|
54
|
+
violations.append(
|
|
55
|
+
Violation(
|
|
56
|
+
file=f"src/{f}",
|
|
57
|
+
message=f"Missing required file: src/{f}",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# .env.example in app root
|
|
62
|
+
if not (app_path / ".env.example").is_file():
|
|
63
|
+
violations.append(
|
|
64
|
+
Violation(
|
|
65
|
+
file=".env.example",
|
|
66
|
+
message="Missing .env.example in app root",
|
|
67
|
+
fix_hint="Create .env.example with sanitized env vars",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return CategoryResult(
|
|
72
|
+
name=self.name,
|
|
73
|
+
label=self.label,
|
|
74
|
+
max_points=self.max_points,
|
|
75
|
+
violations=violations,
|
|
76
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Checker: testing setup."""
|
|
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
|
+
|
|
11
|
+
class TestingChecker:
|
|
12
|
+
name = "testing"
|
|
13
|
+
label = "Testing Setup"
|
|
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
|
+
# __tests__/ directory with test files
|
|
21
|
+
tests_dir = src / "__tests__"
|
|
22
|
+
if tests_dir.is_dir():
|
|
23
|
+
test_files = list(tests_dir.rglob("*.test.tsx")) + list(tests_dir.rglob("*.test.ts"))
|
|
24
|
+
if not test_files:
|
|
25
|
+
violations.append(
|
|
26
|
+
Violation(
|
|
27
|
+
file="src/__tests__/",
|
|
28
|
+
message="No test files found in __tests__/",
|
|
29
|
+
fix_hint="Add .test.tsx/.test.ts files",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
violations.append(
|
|
34
|
+
Violation(
|
|
35
|
+
file="src/__tests__/",
|
|
36
|
+
message="Missing __tests__/ directory",
|
|
37
|
+
fix_hint="Create src/__tests__/ with test files",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# package.json test scripts
|
|
42
|
+
pkg = app_path / "package.json"
|
|
43
|
+
if pkg.is_file():
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(pkg.read_text())
|
|
46
|
+
except (json.JSONDecodeError, OSError):
|
|
47
|
+
data = {}
|
|
48
|
+
scripts = data.get("scripts", {})
|
|
49
|
+
for s in ("test", "test:ci"):
|
|
50
|
+
if s not in scripts:
|
|
51
|
+
violations.append(
|
|
52
|
+
Violation(
|
|
53
|
+
file="package.json",
|
|
54
|
+
message=f'Missing script: "{s}"',
|
|
55
|
+
fix_hint=f"Add {s} script to package.json",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# vitest config
|
|
60
|
+
vitest_config = app_path / "vitest.config.ts"
|
|
61
|
+
vite_config = app_path / "vite.config.ts"
|
|
62
|
+
has_vitest_config = vitest_config.is_file()
|
|
63
|
+
if not has_vitest_config and vite_config.is_file():
|
|
64
|
+
content = vite_config.read_text()
|
|
65
|
+
has_vitest_config = (
|
|
66
|
+
("test" in content and "vitest" in content.lower()) or "test:" in content or "test :" in content
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not has_vitest_config:
|
|
70
|
+
violations.append(
|
|
71
|
+
Violation(
|
|
72
|
+
file="vitest.config.ts",
|
|
73
|
+
message="No vitest configuration found",
|
|
74
|
+
fix_hint="Add vitest.config.ts or test config in vite.config.ts",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return CategoryResult(
|
|
79
|
+
name=self.name,
|
|
80
|
+
label=self.label,
|
|
81
|
+
max_points=self.max_points,
|
|
82
|
+
violations=violations,
|
|
83
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Checker: theme setup and color token isolation."""
|
|
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
|
+
COLOR_TOKEN_PATTERN = re.compile(r"--(?:primary|secondary|accent|destructive)\s*:")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThemeChecker:
|
|
14
|
+
name = "theme"
|
|
15
|
+
label = "Theme Setup"
|
|
16
|
+
max_points = 6
|
|
17
|
+
|
|
18
|
+
def check(self, app_path: Path, app_name: str) -> CategoryResult:
|
|
19
|
+
violations: list[Violation] = []
|
|
20
|
+
src = app_path / "src"
|
|
21
|
+
|
|
22
|
+
# index.css should import themes/{app}.css
|
|
23
|
+
index_css = src / "index.css"
|
|
24
|
+
if index_css.is_file():
|
|
25
|
+
content = index_css.read_text()
|
|
26
|
+
expected_import = f"themes/{app_name}.css"
|
|
27
|
+
if expected_import not in content:
|
|
28
|
+
violations.append(
|
|
29
|
+
Violation(
|
|
30
|
+
file="src/index.css",
|
|
31
|
+
message=f"index.css does not import {expected_import}",
|
|
32
|
+
fix_hint=f'Add @import "{expected_import}" to index.css',
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# No color tokens in index.css
|
|
37
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
38
|
+
if COLOR_TOKEN_PATTERN.search(line):
|
|
39
|
+
violations.append(
|
|
40
|
+
Violation(
|
|
41
|
+
file="src/index.css",
|
|
42
|
+
line=i,
|
|
43
|
+
message="Color token in index.css — should be in theme file",
|
|
44
|
+
fix_hint="Move color tokens to themes/{app}.css",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
violations.append(Violation(file="src/index.css", message="Missing index.css"))
|
|
49
|
+
|
|
50
|
+
# Check globals.css for color tokens too
|
|
51
|
+
globals_css = src / "globals.css"
|
|
52
|
+
if globals_css.is_file():
|
|
53
|
+
content = globals_css.read_text()
|
|
54
|
+
for i, line in enumerate(content.splitlines(), 1):
|
|
55
|
+
if COLOR_TOKEN_PATTERN.search(line):
|
|
56
|
+
violations.append(
|
|
57
|
+
Violation(
|
|
58
|
+
file="src/globals.css",
|
|
59
|
+
line=i,
|
|
60
|
+
message="Color token in globals.css — should be in theme file",
|
|
61
|
+
fix_hint="Move color tokens to themes/{app}.css",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# ThemeToggle in AppLayout.tsx
|
|
66
|
+
layout = src / "components" / "AppLayout.tsx"
|
|
67
|
+
if layout.is_file():
|
|
68
|
+
content = layout.read_text()
|
|
69
|
+
if "ThemeToggle" not in content:
|
|
70
|
+
violations.append(
|
|
71
|
+
Violation(
|
|
72
|
+
file="src/components/AppLayout.tsx",
|
|
73
|
+
message="Missing ThemeToggle in AppLayout",
|
|
74
|
+
fix_hint="Add ThemeToggle from @kodemeio/ui",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
# Check app-specific storageKey
|
|
78
|
+
if "storageKey" not in content and "ThemeToggle" in content:
|
|
79
|
+
violations.append(
|
|
80
|
+
Violation(
|
|
81
|
+
file="src/components/AppLayout.tsx",
|
|
82
|
+
message="ThemeToggle missing app-specific storageKey",
|
|
83
|
+
fix_hint=f'Add storageKey="{app_name}_theme"',
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return CategoryResult(
|
|
88
|
+
name=self.name,
|
|
89
|
+
label=self.label,
|
|
90
|
+
max_points=self.max_points,
|
|
91
|
+
violations=violations,
|
|
92
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Checker: UI standardization — shared component usage and import consistency."""
|
|
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
|
+
# Components that should NOT exist locally — they are provided by @kodemeio/ui
|
|
11
|
+
SHARED_COMPONENTS = {
|
|
12
|
+
"Button.tsx",
|
|
13
|
+
"Input.tsx",
|
|
14
|
+
"Textarea.tsx",
|
|
15
|
+
"Label.tsx",
|
|
16
|
+
"Select.tsx",
|
|
17
|
+
"Badge.tsx",
|
|
18
|
+
"Card.tsx",
|
|
19
|
+
"Dialog.tsx",
|
|
20
|
+
"Sheet.tsx",
|
|
21
|
+
"Alert.tsx",
|
|
22
|
+
"Table.tsx",
|
|
23
|
+
"Tabs.tsx",
|
|
24
|
+
"Tooltip.tsx",
|
|
25
|
+
"Skeleton.tsx",
|
|
26
|
+
"Progress.tsx",
|
|
27
|
+
"Separator.tsx",
|
|
28
|
+
"ScrollArea.tsx",
|
|
29
|
+
"Checkbox.tsx",
|
|
30
|
+
"Switch.tsx",
|
|
31
|
+
"DropdownMenu.tsx",
|
|
32
|
+
"Popover.tsx",
|
|
33
|
+
"RadioGroup.tsx",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Shared components every app should import from @kodemeio/ui
|
|
37
|
+
REQUIRED_SHARED = ["PageHeader", "LoadingSpinner", "Skeleton", "EmptyState"]
|
|
38
|
+
|
|
39
|
+
# Pattern for relative path imports to packages/ui
|
|
40
|
+
RELATIVE_UI_IMPORT = re.compile(r"""from\s+["']\.{1,3}/.*packages/ui""")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UiStandardChecker:
|
|
44
|
+
name = "ui-standard"
|
|
45
|
+
label = "UI Standardization"
|
|
46
|
+
max_points = 6
|
|
47
|
+
|
|
48
|
+
def check(self, app_path: Path, app_name: str) -> CategoryResult:
|
|
49
|
+
violations: list[Violation] = []
|
|
50
|
+
src = app_path / "src"
|
|
51
|
+
|
|
52
|
+
# 1. Duplicate components
|
|
53
|
+
components_dir = src / "components"
|
|
54
|
+
if components_dir.is_dir():
|
|
55
|
+
for f in components_dir.rglob("*.tsx"):
|
|
56
|
+
if f.name in SHARED_COMPONENTS:
|
|
57
|
+
rel = str(f.relative_to(app_path))
|
|
58
|
+
violations.append(
|
|
59
|
+
Violation(
|
|
60
|
+
file=rel,
|
|
61
|
+
message=f"Local duplicate of @kodemeio/ui component: {f.name}",
|
|
62
|
+
auto_fixable=False,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Collect all tsx files for subsequent checks
|
|
67
|
+
all_tsx: list[Path] = []
|
|
68
|
+
if src.is_dir():
|
|
69
|
+
all_tsx = list(src.rglob("*.tsx"))
|
|
70
|
+
all_tsx = [f for f in all_tsx if "__tests__" not in f.parts]
|
|
71
|
+
|
|
72
|
+
all_contents: dict[Path, str] = {}
|
|
73
|
+
for f in all_tsx:
|
|
74
|
+
try:
|
|
75
|
+
all_contents[f] = f.read_text()
|
|
76
|
+
except OSError:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# 2. Required shared component imports
|
|
80
|
+
# Check LoadingSpinner OR Skeleton — either counts
|
|
81
|
+
found_page_header = False
|
|
82
|
+
found_loading = False
|
|
83
|
+
found_empty_state = False
|
|
84
|
+
|
|
85
|
+
for content in all_contents.values():
|
|
86
|
+
if "@kodemeio/ui" in content:
|
|
87
|
+
if "PageHeader" in content:
|
|
88
|
+
found_page_header = True
|
|
89
|
+
if "LoadingSpinner" in content or "Skeleton" in content:
|
|
90
|
+
found_loading = True
|
|
91
|
+
if "EmptyState" in content:
|
|
92
|
+
found_empty_state = True
|
|
93
|
+
|
|
94
|
+
if not found_page_header:
|
|
95
|
+
violations.append(
|
|
96
|
+
Violation(
|
|
97
|
+
file="src/",
|
|
98
|
+
message="No usage of shared PageHeader from @kodemeio/ui",
|
|
99
|
+
fix_hint="Import PageHeader from @kodemeio/ui for consistent UX",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
if not found_loading:
|
|
103
|
+
violations.append(
|
|
104
|
+
Violation(
|
|
105
|
+
file="src/",
|
|
106
|
+
message="No usage of shared LoadingSpinner or Skeleton from @kodemeio/ui",
|
|
107
|
+
fix_hint="Import LoadingSpinner or Skeleton from @kodemeio/ui for consistent UX",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if not found_empty_state:
|
|
111
|
+
violations.append(
|
|
112
|
+
Violation(
|
|
113
|
+
file="src/",
|
|
114
|
+
message="No usage of shared EmptyState from @kodemeio/ui",
|
|
115
|
+
fix_hint="Import EmptyState from @kodemeio/ui for consistent UX",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# 3. Badge wrapper pattern
|
|
120
|
+
if components_dir.is_dir():
|
|
121
|
+
for f in components_dir.rglob("*.tsx"):
|
|
122
|
+
name = f.stem # e.g. "StatusBadge"
|
|
123
|
+
if name.endswith("Badge") or name.endswith("StatusBadge"):
|
|
124
|
+
if f.name in SHARED_COMPONENTS:
|
|
125
|
+
continue # already caught by check 1
|
|
126
|
+
try:
|
|
127
|
+
content = f.read_text()
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
if "Badge" not in content or "@kodemeio/ui" not in content:
|
|
131
|
+
rel = str(f.relative_to(app_path))
|
|
132
|
+
violations.append(
|
|
133
|
+
Violation(
|
|
134
|
+
file=rel,
|
|
135
|
+
message=f"{f.name} should use Badge from @kodemeio/ui as base",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# 4. DataTable usage
|
|
140
|
+
table_page_count = 0
|
|
141
|
+
has_datatable = False
|
|
142
|
+
pages_dir = src / "pages"
|
|
143
|
+
|
|
144
|
+
for f, content in all_contents.items():
|
|
145
|
+
if "@kodemeio/ui" in content and "DataTable" in content:
|
|
146
|
+
has_datatable = True
|
|
147
|
+
|
|
148
|
+
if pages_dir.is_dir():
|
|
149
|
+
for f in pages_dir.rglob("*.tsx"):
|
|
150
|
+
if "__tests__" in f.parts:
|
|
151
|
+
continue
|
|
152
|
+
content = all_contents.get(f, "")
|
|
153
|
+
if "<Table" in content or "<table" in content:
|
|
154
|
+
table_page_count += 1
|
|
155
|
+
|
|
156
|
+
if table_page_count >= 3 and not has_datatable:
|
|
157
|
+
violations.append(
|
|
158
|
+
Violation(
|
|
159
|
+
file="src/pages/",
|
|
160
|
+
message=f"App has {table_page_count} table pages but doesn't use DataTable from @kodemeio/ui",
|
|
161
|
+
fix_hint="Use DataTable from @kodemeio/ui for consistent table behavior",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# 5. Import source consistency — relative path imports to packages/ui
|
|
166
|
+
for f, content in all_contents.items():
|
|
167
|
+
lines = content.splitlines()
|
|
168
|
+
rel = str(f.relative_to(app_path))
|
|
169
|
+
for i, line in enumerate(lines, 1):
|
|
170
|
+
if RELATIVE_UI_IMPORT.search(line):
|
|
171
|
+
violations.append(
|
|
172
|
+
Violation(
|
|
173
|
+
file=rel,
|
|
174
|
+
line=i,
|
|
175
|
+
message="Direct path import to packages/ui — use @kodemeio/ui",
|
|
176
|
+
auto_fixable=True,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return CategoryResult(
|
|
181
|
+
name=self.name,
|
|
182
|
+
label=self.label,
|
|
183
|
+
max_points=self.max_points,
|
|
184
|
+
violations=violations,
|
|
185
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Checker: Vite 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
|
+
# Expected ports per app
|
|
10
|
+
APP_PORTS: dict[str, str] = {
|
|
11
|
+
"sfa": "4004",
|
|
12
|
+
"lfa": "4005",
|
|
13
|
+
"shop": "4006",
|
|
14
|
+
"wms": "4007",
|
|
15
|
+
"bia": "4008",
|
|
16
|
+
"eam": "4009",
|
|
17
|
+
"mrp": "4010",
|
|
18
|
+
"hrm": "4011",
|
|
19
|
+
"tpm": "4012",
|
|
20
|
+
"dms": "4013",
|
|
21
|
+
"saas": "4014",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ViteChecker:
|
|
26
|
+
name = "vite"
|
|
27
|
+
label = "Vite Config"
|
|
28
|
+
max_points = 4
|
|
29
|
+
|
|
30
|
+
def check(self, app_path: Path, app_name: str) -> CategoryResult:
|
|
31
|
+
violations: list[Violation] = []
|
|
32
|
+
|
|
33
|
+
vite_config = app_path / "vite.config.ts"
|
|
34
|
+
if not vite_config.is_file():
|
|
35
|
+
violations.append(
|
|
36
|
+
Violation(
|
|
37
|
+
file="vite.config.ts",
|
|
38
|
+
message="Missing vite.config.ts",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return CategoryResult(
|
|
42
|
+
name=self.name,
|
|
43
|
+
label=self.label,
|
|
44
|
+
max_points=self.max_points,
|
|
45
|
+
violations=violations,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
content = vite_config.read_text()
|
|
49
|
+
|
|
50
|
+
if "createViteConfig" not in content:
|
|
51
|
+
violations.append(
|
|
52
|
+
Violation(
|
|
53
|
+
file="vite.config.ts",
|
|
54
|
+
message="Does not use createViteConfig from @kodemeio/vite-config",
|
|
55
|
+
fix_hint="Use createViteConfig() factory",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if "@kodemeio/vite-config" not in content:
|
|
60
|
+
violations.append(
|
|
61
|
+
Violation(
|
|
62
|
+
file="vite.config.ts",
|
|
63
|
+
message="Does not import from @kodemeio/vite-config",
|
|
64
|
+
fix_hint="Import createViteConfig from @kodemeio/vite-config",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
expected_port = APP_PORTS.get(app_name)
|
|
69
|
+
if expected_port and expected_port not in content:
|
|
70
|
+
violations.append(
|
|
71
|
+
Violation(
|
|
72
|
+
file="vite.config.ts",
|
|
73
|
+
message=f"Port {expected_port} not configured for {app_name}",
|
|
74
|
+
fix_hint=f"Set port to {expected_port} in vite config",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return CategoryResult(
|
|
79
|
+
name=self.name,
|
|
80
|
+
label=self.label,
|
|
81
|
+
max_points=self.max_points,
|
|
82
|
+
violations=violations,
|
|
83
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""ComplianceEngine — runs checkers and generates reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kctl_react.core.compliance.checks import CHECKER_MAP
|
|
8
|
+
from kctl_react.core.compliance.models import AppReport, CategoryResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComplianceEngine:
|
|
12
|
+
"""Runs compliance checkers and generates fix prompts."""
|
|
13
|
+
|
|
14
|
+
def audit(
|
|
15
|
+
self,
|
|
16
|
+
app_path: Path,
|
|
17
|
+
app_name: str,
|
|
18
|
+
categories: list[str] | None = None,
|
|
19
|
+
) -> AppReport:
|
|
20
|
+
"""Run checkers (filtered by categories if given), return AppReport."""
|
|
21
|
+
report = AppReport(app=app_name)
|
|
22
|
+
|
|
23
|
+
if categories:
|
|
24
|
+
checkers = [(name, fn) for name, fn in CHECKER_MAP.items() if name in categories]
|
|
25
|
+
else:
|
|
26
|
+
checkers = list(CHECKER_MAP.items())
|
|
27
|
+
|
|
28
|
+
for _name, checker_fn in checkers:
|
|
29
|
+
result: CategoryResult = checker_fn(app_path, app_name)
|
|
30
|
+
report.categories.append(result)
|
|
31
|
+
|
|
32
|
+
return report
|
|
33
|
+
|
|
34
|
+
def generate_prompt(self, report: AppReport) -> str:
|
|
35
|
+
"""Generate markdown prompt for AI/human fixing."""
|
|
36
|
+
pct = int(report.total_score / report.max_score * 100) if report.max_score > 0 else 100
|
|
37
|
+
|
|
38
|
+
lines: list[str] = [
|
|
39
|
+
f"# Fix Compliance Issues for `{report.app}`",
|
|
40
|
+
"",
|
|
41
|
+
f"- **App path**: `apps/{report.app}`",
|
|
42
|
+
f"- **Score**: {report.total_score}/{report.max_score} ({pct}%)",
|
|
43
|
+
f"- **Grade**: {report.grade}",
|
|
44
|
+
f"- **Total issues**: {report.total_violations}",
|
|
45
|
+
"",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Auto-fixable section
|
|
49
|
+
auto_violations = [(c, v) for c in report.categories for v in c.violations if v.auto_fixable]
|
|
50
|
+
lines.append("## Auto-fixable (run first)")
|
|
51
|
+
lines.append("")
|
|
52
|
+
if auto_violations:
|
|
53
|
+
lines.append(f"```bash\nkctl-react compliance fix {report.app}\n```")
|
|
54
|
+
lines.append("")
|
|
55
|
+
for cat, v in auto_violations:
|
|
56
|
+
hint = f" -- {v.fix_hint}" if v.fix_hint else ""
|
|
57
|
+
lines.append(f"- `{v.file}`:{v.line or '?'} {v.message}{hint}")
|
|
58
|
+
else:
|
|
59
|
+
lines.append("No auto-fixable issues found.")
|
|
60
|
+
lines.append("")
|
|
61
|
+
|
|
62
|
+
# Manual review section
|
|
63
|
+
manual_violations = [(c, v) for c in report.categories for v in c.violations if not v.auto_fixable]
|
|
64
|
+
lines.append("## Issues Requiring Review")
|
|
65
|
+
lines.append("")
|
|
66
|
+
if manual_violations:
|
|
67
|
+
current_cat = ""
|
|
68
|
+
for cat, v in manual_violations:
|
|
69
|
+
if cat.label != current_cat:
|
|
70
|
+
current_cat = cat.label
|
|
71
|
+
lines.append(f"### {current_cat}")
|
|
72
|
+
lines.append("")
|
|
73
|
+
loc = f":{v.line}" if v.line else ""
|
|
74
|
+
hint = f"\n - Hint: {v.fix_hint}" if v.fix_hint else ""
|
|
75
|
+
lines.append(f"- `{v.file}`{loc} {v.message}{hint}")
|
|
76
|
+
lines.append("")
|
|
77
|
+
else:
|
|
78
|
+
lines.append("No manual review issues found.")
|
|
79
|
+
lines.append("")
|
|
80
|
+
|
|
81
|
+
# Verification section
|
|
82
|
+
lines.append("## Verification")
|
|
83
|
+
lines.append("")
|
|
84
|
+
lines.append(f"```bash\nkctl-react compliance audit {report.app}\n```")
|
|
85
|
+
lines.append("")
|
|
86
|
+
|
|
87
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""App-specific exceptions for compliance checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
APP_EXCEPTIONS: dict[str, dict[str, list[str]]] = {
|
|
6
|
+
"mrp": {"skip_providers": ["GPSProvider"]},
|
|
7
|
+
"tpm": {"skip_providers": ["GPSProvider"]},
|
|
8
|
+
"saas": {"skip_providers": ["GPSProvider"]},
|
|
9
|
+
"shop": {"skip_providers": ["GPSProvider"]},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_skip_providers(app_name: str) -> list[str]:
|
|
14
|
+
"""Return list of providers to skip for the given app."""
|
|
15
|
+
return APP_EXCEPTIONS.get(app_name, {}).get("skip_providers", [])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Auto-fixers registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kctl_react.core.compliance.fixes.codegen_fix import fix_codegen
|
|
8
|
+
from kctl_react.core.compliance.fixes.i18n_fix import fix_i18n
|
|
9
|
+
from kctl_react.core.compliance.fixes.imports_fix import fix_imports
|
|
10
|
+
from kctl_react.core.compliance.fixes.structure_fix import fix_structure
|
|
11
|
+
from kctl_react.core.compliance.fixes.theme_fix import fix_theme
|
|
12
|
+
|
|
13
|
+
ALL_FIXERS: dict = {
|
|
14
|
+
"structure": fix_structure,
|
|
15
|
+
"imports": fix_imports,
|
|
16
|
+
"i18n": fix_i18n,
|
|
17
|
+
"codegen": fix_codegen,
|
|
18
|
+
"theme": fix_theme,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_fixes(
|
|
23
|
+
app_path: Path,
|
|
24
|
+
app_name: str,
|
|
25
|
+
dry_run: bool = False,
|
|
26
|
+
categories: list[str] | None = None,
|
|
27
|
+
) -> int:
|
|
28
|
+
"""Apply auto-fixes and return count of fixes applied."""
|
|
29
|
+
fixers = {k: v for k, v in ALL_FIXERS.items() if not categories or k in categories}
|
|
30
|
+
total = 0
|
|
31
|
+
for fix_fn in fixers.values():
|
|
32
|
+
total += fix_fn(app_path, app_name, dry_run=dry_run)
|
|
33
|
+
return total
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Fixer: create openapi-ts.config.ts if missing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_TEMPLATE = """\
|
|
8
|
+
import { defineConfig } from "@hey-api/openapi-ts";
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
client: "@hey-api/client-fetch",
|
|
12
|
+
input: "openapi.json",
|
|
13
|
+
output: "src/generated",
|
|
14
|
+
services: false,
|
|
15
|
+
schemas: false,
|
|
16
|
+
});
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fix_codegen(app_path: Path, app_name: str, dry_run: bool = False) -> int:
|
|
21
|
+
"""Create openapi-ts.config.ts if missing. Return count of fixes applied."""
|
|
22
|
+
config_file = app_path / "openapi-ts.config.ts"
|
|
23
|
+
if config_file.is_file():
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
if not dry_run:
|
|
27
|
+
config_file.write_text(_TEMPLATE, encoding="utf-8")
|
|
28
|
+
return 1
|