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,54 @@
|
|
|
1
|
+
"""Skill generation for Claude Code integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kctl_react.core.callbacks import AppContext
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Claude Code skill management.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def generate(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
output: Annotated[str, typer.Option("--output", "-o", help="Output directory")] = "",
|
|
19
|
+
install: Annotated[bool, typer.Option("--install", help="Install to ~/.claude/skills/")] = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Auto-generate SKILL.md from CLI command registry."""
|
|
22
|
+
actx: AppContext = ctx.obj
|
|
23
|
+
out = actx.output
|
|
24
|
+
from kctl_lib.skill_generator import generate_skill
|
|
25
|
+
|
|
26
|
+
from kctl_react.cli import app as cli_app
|
|
27
|
+
|
|
28
|
+
skill_name = "react-admin"
|
|
29
|
+
description = "React PWA monorepo management via kctl-react CLI"
|
|
30
|
+
|
|
31
|
+
# Determine output directory
|
|
32
|
+
if output:
|
|
33
|
+
output_dir = Path(output)
|
|
34
|
+
elif install:
|
|
35
|
+
output_dir = Path.home() / ".claude" / "skills" / skill_name
|
|
36
|
+
else:
|
|
37
|
+
output_dir = actx.project_root / "cli" / "skills" / skill_name
|
|
38
|
+
|
|
39
|
+
# Check for extra content
|
|
40
|
+
extra = output_dir / "SKILL.extra.md"
|
|
41
|
+
if not extra.exists():
|
|
42
|
+
extra = actx.project_root / "cli" / "skills" / skill_name / "SKILL.extra.md"
|
|
43
|
+
|
|
44
|
+
generate_skill(
|
|
45
|
+
cli_app,
|
|
46
|
+
"kctl-react",
|
|
47
|
+
skill_name,
|
|
48
|
+
description,
|
|
49
|
+
output_dir=output_dir,
|
|
50
|
+
extra_file=extra if extra.exists() else None,
|
|
51
|
+
)
|
|
52
|
+
out.success(f"Generated {output_dir / 'SKILL.md'}")
|
|
53
|
+
if install:
|
|
54
|
+
out.success(f"Installed to ~/.claude/skills/{skill_name}/")
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""State management analysis — TanStack Query static analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import re
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from kctl_react.core.analyzers import find_hook_files, find_query_keys
|
|
14
|
+
from kctl_react.core.callbacks import AppContext
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="TanStack Query static analysis (query keys, hooks audit, invalidation map).")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("query-keys")
|
|
20
|
+
def query_keys(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""List all TanStack Query queryKey definitions in hook files and inline queries."""
|
|
25
|
+
actx: AppContext = ctx.obj
|
|
26
|
+
out = actx.output
|
|
27
|
+
actx.validate_app(app_name)
|
|
28
|
+
src_dir = actx.get_app_dir(app_name) / "src"
|
|
29
|
+
|
|
30
|
+
results: list[dict] = []
|
|
31
|
+
|
|
32
|
+
# Scan hook files
|
|
33
|
+
for hook_file in find_hook_files(src_dir):
|
|
34
|
+
for entry in find_query_keys(hook_file):
|
|
35
|
+
entry = dict(entry)
|
|
36
|
+
with contextlib.suppress(ValueError):
|
|
37
|
+
entry["file"] = str(Path(str(entry["file"])).relative_to(actx.project_root))
|
|
38
|
+
results.append(entry)
|
|
39
|
+
|
|
40
|
+
# Also scan .tsx files for inline queries
|
|
41
|
+
for tsx_file in src_dir.rglob("*.tsx"):
|
|
42
|
+
for entry in find_query_keys(tsx_file):
|
|
43
|
+
entry = dict(entry)
|
|
44
|
+
with contextlib.suppress(ValueError):
|
|
45
|
+
entry["file"] = str(Path(str(entry["file"])).relative_to(actx.project_root))
|
|
46
|
+
results.append(entry)
|
|
47
|
+
|
|
48
|
+
if not results:
|
|
49
|
+
out.warn(f"No queryKey definitions found in {app_name}")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
rows = [[r["file"], str(r["line"]), r["key_prefix"]] for r in results]
|
|
53
|
+
out.table(
|
|
54
|
+
f"Query Keys ({len(results)} found in {app_name})",
|
|
55
|
+
[("File", "cyan"), ("Line", "dim"), ("Key Prefix", "green")],
|
|
56
|
+
rows,
|
|
57
|
+
data_for_json=results,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("consistency")
|
|
62
|
+
def consistency(
|
|
63
|
+
ctx: typer.Context,
|
|
64
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Check query key consistency — duplicates and mutations missing invalidation."""
|
|
67
|
+
actx: AppContext = ctx.obj
|
|
68
|
+
out = actx.output
|
|
69
|
+
actx.validate_app(app_name)
|
|
70
|
+
src_dir = actx.get_app_dir(app_name) / "src"
|
|
71
|
+
|
|
72
|
+
# Collect query keys grouped by prefix and the hook files they appear in
|
|
73
|
+
prefix_to_files: dict[str, set[str]] = defaultdict(set)
|
|
74
|
+
for hook_file in find_hook_files(src_dir):
|
|
75
|
+
for entry in find_query_keys(hook_file):
|
|
76
|
+
prefix = str(entry["key_prefix"])
|
|
77
|
+
prefix_to_files[prefix].add(hook_file.name)
|
|
78
|
+
|
|
79
|
+
issues: list[dict] = []
|
|
80
|
+
|
|
81
|
+
# Check: same prefix in multiple hook files (possible duplication)
|
|
82
|
+
for prefix, files in sorted(prefix_to_files.items()):
|
|
83
|
+
if len(files) > 1:
|
|
84
|
+
issues.append(
|
|
85
|
+
{
|
|
86
|
+
"type": "duplicate_prefix",
|
|
87
|
+
"prefix": prefix,
|
|
88
|
+
"detail": f"Appears in {len(files)} files: {', '.join(sorted(files))}",
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Check: mutations without invalidateQueries
|
|
93
|
+
for hook_file in find_hook_files(src_dir):
|
|
94
|
+
try:
|
|
95
|
+
text = hook_file.read_text()
|
|
96
|
+
except OSError:
|
|
97
|
+
continue
|
|
98
|
+
# Find mutation functions (useCreate*, useUpdate*, useDelete*)
|
|
99
|
+
mutation_pattern = re.compile(r"export\s+function\s+(use(?:Create|Update|Delete)\w+)")
|
|
100
|
+
for match in mutation_pattern.finditer(text):
|
|
101
|
+
fn_name = match.group(1)
|
|
102
|
+
# Find the function body (simple heuristic: text from match to next export)
|
|
103
|
+
start = match.start()
|
|
104
|
+
# Check if invalidateQueries appears after this mutation definition
|
|
105
|
+
fn_region = text[start : start + 600]
|
|
106
|
+
if "invalidateQueries" not in fn_region:
|
|
107
|
+
issues.append(
|
|
108
|
+
{
|
|
109
|
+
"type": "missing_invalidation",
|
|
110
|
+
"prefix": fn_name,
|
|
111
|
+
"detail": f"Mutation in {hook_file.name} has no invalidateQueries",
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not issues:
|
|
116
|
+
out.success(f"Query key consistency OK for {app_name}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
rows = [[i["type"], i["prefix"], i["detail"]] for i in issues]
|
|
120
|
+
out.table(
|
|
121
|
+
f"Consistency Issues ({len(issues)} found in {app_name})",
|
|
122
|
+
[("Type", "yellow"), ("Prefix/Function", "red"), ("Detail", "dim")],
|
|
123
|
+
rows,
|
|
124
|
+
data_for_json=issues,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command("hooks-audit")
|
|
129
|
+
def hooks_audit(
|
|
130
|
+
ctx: typer.Context,
|
|
131
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Audit hook best practices — imports, onError handlers, getErrorMessage usage."""
|
|
134
|
+
actx: AppContext = ctx.obj
|
|
135
|
+
out = actx.output
|
|
136
|
+
actx.validate_app(app_name)
|
|
137
|
+
src_dir = actx.get_app_dir(app_name) / "src"
|
|
138
|
+
|
|
139
|
+
issues: list[dict] = []
|
|
140
|
+
|
|
141
|
+
for hook_file in find_hook_files(src_dir):
|
|
142
|
+
try:
|
|
143
|
+
text = hook_file.read_text()
|
|
144
|
+
except OSError:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
rel = hook_file.name
|
|
148
|
+
|
|
149
|
+
# Check: hooks should import from @/types/api (not manual types)
|
|
150
|
+
if "from '@/generated/" in text or 'from "@/generated/' in text:
|
|
151
|
+
issues.append(
|
|
152
|
+
{
|
|
153
|
+
"file": rel,
|
|
154
|
+
"check": "import_path",
|
|
155
|
+
"detail": "Imports directly from @/generated/ — use @/types/api instead",
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Find all useMutation blocks and check for onError
|
|
160
|
+
mutation_blocks = re.findall(
|
|
161
|
+
r"useMutation\s*\(\s*\{([^}]{0,800})\}",
|
|
162
|
+
text,
|
|
163
|
+
re.DOTALL,
|
|
164
|
+
)
|
|
165
|
+
for block in mutation_blocks:
|
|
166
|
+
if "onError" not in block:
|
|
167
|
+
issues.append(
|
|
168
|
+
{
|
|
169
|
+
"file": rel,
|
|
170
|
+
"check": "missing_onError",
|
|
171
|
+
"detail": "useMutation missing onError handler",
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
elif "getErrorMessage" not in block:
|
|
175
|
+
issues.append(
|
|
176
|
+
{
|
|
177
|
+
"file": rel,
|
|
178
|
+
"check": "missing_getErrorMessage",
|
|
179
|
+
"detail": "onError does not use getErrorMessage()",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not issues:
|
|
184
|
+
out.success(f"Hook best practices OK for {app_name}")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
rows = [[i["file"], i["check"], i["detail"]] for i in issues]
|
|
188
|
+
out.table(
|
|
189
|
+
f"Hook Issues ({len(issues)} found in {app_name})",
|
|
190
|
+
[("File", "cyan"), ("Check", "yellow"), ("Detail", "red")],
|
|
191
|
+
rows,
|
|
192
|
+
data_for_json=issues,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.command("invalidation-map")
|
|
197
|
+
def invalidation_map(
|
|
198
|
+
ctx: typer.Context,
|
|
199
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Show mutation → invalidation mapping for all hook files."""
|
|
202
|
+
actx: AppContext = ctx.obj
|
|
203
|
+
out = actx.output
|
|
204
|
+
actx.validate_app(app_name)
|
|
205
|
+
src_dir = actx.get_app_dir(app_name) / "src"
|
|
206
|
+
|
|
207
|
+
entries: list[dict] = []
|
|
208
|
+
|
|
209
|
+
for hook_file in find_hook_files(src_dir):
|
|
210
|
+
try:
|
|
211
|
+
text = hook_file.read_text()
|
|
212
|
+
except OSError:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
rel = hook_file.name
|
|
216
|
+
|
|
217
|
+
# Find mutation function definitions
|
|
218
|
+
fn_pattern = re.compile(
|
|
219
|
+
r"export\s+function\s+(use(?:Create|Update|Delete)\w+)",
|
|
220
|
+
)
|
|
221
|
+
# Find invalidateQueries calls with queryKey
|
|
222
|
+
inv_pattern = re.compile(
|
|
223
|
+
r"invalidateQueries\s*\(\s*\{[^}]*queryKey\s*:\s*\[([^\]]+)\]",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for fn_match in fn_pattern.finditer(text):
|
|
227
|
+
fn_name = fn_match.group(1)
|
|
228
|
+
# Grab region after function definition
|
|
229
|
+
region = text[fn_match.start() : fn_match.start() + 600]
|
|
230
|
+
inv_keys: list[str] = []
|
|
231
|
+
for inv_match in inv_pattern.finditer(region):
|
|
232
|
+
raw = inv_match.group(1).strip()
|
|
233
|
+
parts = [p.strip().strip("\"'") for p in raw.split(",")]
|
|
234
|
+
inv_keys.append(parts[0] if parts else raw)
|
|
235
|
+
|
|
236
|
+
entries.append(
|
|
237
|
+
{
|
|
238
|
+
"file": rel,
|
|
239
|
+
"mutation": fn_name,
|
|
240
|
+
"invalidates": ", ".join(inv_keys) if inv_keys else "(none)",
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if not entries:
|
|
245
|
+
out.warn(f"No mutation functions (useCreate*, useUpdate*, useDelete*) found in {app_name}")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
rows = [[e["file"], e["mutation"], e["invalidates"]] for e in entries]
|
|
249
|
+
out.table(
|
|
250
|
+
f"Invalidation Map ({len(entries)} mutations in {app_name})",
|
|
251
|
+
[("File", "cyan"), ("Mutation", "yellow"), ("Invalidates", "green")],
|
|
252
|
+
rows,
|
|
253
|
+
data_for_json=entries,
|
|
254
|
+
)
|