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,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
+ )