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,353 @@
|
|
|
1
|
+
"""App inventory and health check commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from kctl_react import __version__
|
|
13
|
+
from kctl_react.commands.clean import run_clean
|
|
14
|
+
from kctl_react.commands.dashboard import _display_dashboard, _fetch_dashboard
|
|
15
|
+
from kctl_react.commands.doctor import run_doctor
|
|
16
|
+
from kctl_react.core.callbacks import AppContext
|
|
17
|
+
from kctl_react.core.discovery import get_app_dir
|
|
18
|
+
from kctl_react.core.config import resolve_active_profile_name
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help="App inventory, status, and health checks.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("list")
|
|
24
|
+
def list_(ctx: typer.Context) -> None:
|
|
25
|
+
"""List all apps with ports and package names."""
|
|
26
|
+
actx: AppContext = ctx.obj
|
|
27
|
+
out = actx.output
|
|
28
|
+
|
|
29
|
+
rows: list[list[str]] = []
|
|
30
|
+
json_data: list[dict] = []
|
|
31
|
+
|
|
32
|
+
for app_name, info in actx.apps.items():
|
|
33
|
+
app_dir = actx.get_app_dir(app_name)
|
|
34
|
+
exists = app_dir.is_dir()
|
|
35
|
+
status = "[green]OK[/green]" if exists else "[red]missing[/red]"
|
|
36
|
+
|
|
37
|
+
rows.append(
|
|
38
|
+
[
|
|
39
|
+
app_name,
|
|
40
|
+
info["name"],
|
|
41
|
+
str(info["port"]),
|
|
42
|
+
info["package"],
|
|
43
|
+
status,
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
json_data.append(
|
|
47
|
+
{
|
|
48
|
+
"app": app_name,
|
|
49
|
+
"name": info["name"],
|
|
50
|
+
"port": info["port"],
|
|
51
|
+
"package": info["package"],
|
|
52
|
+
"exists": exists,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
out.table(
|
|
57
|
+
"Apps",
|
|
58
|
+
[("App", "cyan"), ("Description", ""), ("Port", "green"), ("Package", "dim"), ("Status", "")],
|
|
59
|
+
rows,
|
|
60
|
+
data_for_json=json_data,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def ports(ctx: typer.Context) -> None:
|
|
66
|
+
"""Show port assignments for all apps."""
|
|
67
|
+
actx: AppContext = ctx.obj
|
|
68
|
+
out = actx.output
|
|
69
|
+
|
|
70
|
+
rows: list[list[str]] = []
|
|
71
|
+
for app_name, info in actx.apps.items():
|
|
72
|
+
rows.append([app_name, str(info["port"]), f"http://localhost:{info['port']}"])
|
|
73
|
+
|
|
74
|
+
out.table(
|
|
75
|
+
"Port Assignments",
|
|
76
|
+
[("App", "cyan"), ("Port", "green"), ("URL", "dim")],
|
|
77
|
+
rows,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def status(
|
|
83
|
+
ctx: typer.Context,
|
|
84
|
+
app_name: Annotated[str | None, typer.Argument(help="App name (optional, checks all if omitted)")] = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Check status of app(s): directory exists, package.json, env file, dist."""
|
|
87
|
+
actx: AppContext = ctx.obj
|
|
88
|
+
out = actx.output
|
|
89
|
+
root = actx.project_root
|
|
90
|
+
|
|
91
|
+
apps_to_check = [app_name] if app_name else actx.app_names
|
|
92
|
+
if app_name:
|
|
93
|
+
actx.validate_app(app_name)
|
|
94
|
+
|
|
95
|
+
rows: list[list[str]] = []
|
|
96
|
+
json_data: list[dict] = []
|
|
97
|
+
|
|
98
|
+
for name in apps_to_check:
|
|
99
|
+
info = actx.apps[name]
|
|
100
|
+
app_dir = get_app_dir(root, name)
|
|
101
|
+
|
|
102
|
+
has_dir = app_dir.is_dir()
|
|
103
|
+
has_pkg = (app_dir / "package.json").exists()
|
|
104
|
+
has_src = (app_dir / "src").is_dir()
|
|
105
|
+
has_dist = (app_dir / "dist").is_dir() or (app_dir / ".next").is_dir()
|
|
106
|
+
has_env = (app_dir / ".env").exists() or (app_dir / ".env.local").exists()
|
|
107
|
+
has_tests = (
|
|
108
|
+
(
|
|
109
|
+
(app_dir / "src" / "__tests__").is_dir()
|
|
110
|
+
or any((app_dir / "src").rglob("*.test.ts"))
|
|
111
|
+
or any((app_dir / "src").rglob("*.test.tsx"))
|
|
112
|
+
)
|
|
113
|
+
if has_src
|
|
114
|
+
else False
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def icon(ok: bool) -> str:
|
|
118
|
+
return "[green]OK[/green]" if ok else "[red]--[/red]"
|
|
119
|
+
|
|
120
|
+
rows.append(
|
|
121
|
+
[
|
|
122
|
+
name,
|
|
123
|
+
str(info["port"]),
|
|
124
|
+
icon(has_dir),
|
|
125
|
+
icon(has_pkg),
|
|
126
|
+
icon(has_src),
|
|
127
|
+
icon(has_dist),
|
|
128
|
+
icon(has_env),
|
|
129
|
+
icon(has_tests),
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
json_data.append(
|
|
133
|
+
{
|
|
134
|
+
"app": name,
|
|
135
|
+
"port": info["port"],
|
|
136
|
+
"directory": has_dir,
|
|
137
|
+
"package_json": has_pkg,
|
|
138
|
+
"src": has_src,
|
|
139
|
+
"dist": has_dist,
|
|
140
|
+
"env": has_env,
|
|
141
|
+
"tests": has_tests,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
out.table(
|
|
146
|
+
"App Status",
|
|
147
|
+
[
|
|
148
|
+
("App", "cyan"),
|
|
149
|
+
("Port", "green"),
|
|
150
|
+
("Dir", ""),
|
|
151
|
+
("Pkg", ""),
|
|
152
|
+
("Src", ""),
|
|
153
|
+
("Dist", ""),
|
|
154
|
+
("Env", ""),
|
|
155
|
+
("Tests", ""),
|
|
156
|
+
],
|
|
157
|
+
rows,
|
|
158
|
+
data_for_json=json_data,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _check_health(actx: AppContext, app_name: str | None) -> tuple[list[list[str]], list[dict], int, int]:
|
|
163
|
+
"""Run health checks and return (rows, json_data, healthy, total)."""
|
|
164
|
+
apps_to_check = [app_name] if app_name else actx.app_names
|
|
165
|
+
|
|
166
|
+
rows: list[list[str]] = []
|
|
167
|
+
json_data: list[dict] = []
|
|
168
|
+
healthy = 0
|
|
169
|
+
total = 0
|
|
170
|
+
|
|
171
|
+
for name in apps_to_check:
|
|
172
|
+
info = actx.apps[name]
|
|
173
|
+
port = info["port"]
|
|
174
|
+
url = f"http://localhost:{port}"
|
|
175
|
+
total += 1
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
r = httpx.get(url, timeout=3, follow_redirects=True)
|
|
179
|
+
is_up = r.status_code < 500
|
|
180
|
+
status = f"[green]{r.status_code}[/green]" if is_up else f"[red]{r.status_code}[/red]"
|
|
181
|
+
if is_up:
|
|
182
|
+
healthy += 1
|
|
183
|
+
except (httpx.HTTPError, Exception):
|
|
184
|
+
is_up = False
|
|
185
|
+
status = "[red]offline[/red]"
|
|
186
|
+
|
|
187
|
+
rows.append([name, str(port), status])
|
|
188
|
+
json_data.append({"app": name, "port": port, "healthy": is_up})
|
|
189
|
+
|
|
190
|
+
return rows, json_data, healthy, total
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command()
|
|
194
|
+
def health(
|
|
195
|
+
ctx: typer.Context,
|
|
196
|
+
app_name: Annotated[str | None, typer.Argument(help="App name (optional, checks all if omitted)")] = None,
|
|
197
|
+
watch: Annotated[bool, typer.Option("--watch", "-w", help="Continuously monitor.")] = False,
|
|
198
|
+
interval: Annotated[int, typer.Option("--interval", "-i", help="Refresh interval in seconds.")] = 5,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Check health of running dev servers."""
|
|
201
|
+
actx: AppContext = ctx.obj
|
|
202
|
+
out = actx.output
|
|
203
|
+
|
|
204
|
+
if app_name:
|
|
205
|
+
actx.validate_app(app_name)
|
|
206
|
+
|
|
207
|
+
if watch:
|
|
208
|
+
try:
|
|
209
|
+
while True:
|
|
210
|
+
rows, json_data, healthy, total = _check_health(actx, app_name)
|
|
211
|
+
out.console.clear()
|
|
212
|
+
out.table(
|
|
213
|
+
"Dev Server Health",
|
|
214
|
+
[("App", "cyan"), ("Port", "green"), ("Status", "")],
|
|
215
|
+
rows,
|
|
216
|
+
data_for_json=json_data,
|
|
217
|
+
)
|
|
218
|
+
out.text(f"\n {healthy}/{total} healthy — refreshing every {interval}s (Ctrl+C to stop)")
|
|
219
|
+
time.sleep(interval)
|
|
220
|
+
except KeyboardInterrupt:
|
|
221
|
+
out.info("Stopped watching.")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
rows, json_data, healthy, total = _check_health(actx, app_name)
|
|
225
|
+
|
|
226
|
+
out.table(
|
|
227
|
+
"Dev Server Health",
|
|
228
|
+
[("App", "cyan"), ("Port", "green"), ("Status", "")],
|
|
229
|
+
rows,
|
|
230
|
+
data_for_json=json_data,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if healthy == total:
|
|
234
|
+
out.success(f"All {total} apps healthy")
|
|
235
|
+
elif healthy > 0:
|
|
236
|
+
out.warn(f"{healthy}/{total} apps healthy")
|
|
237
|
+
else:
|
|
238
|
+
out.info(f"No apps running (checked {total})")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command()
|
|
242
|
+
def info(ctx: typer.Context) -> None:
|
|
243
|
+
"""Show quick project info: version, root, profile, node/pnpm versions."""
|
|
244
|
+
actx: AppContext = ctx.obj
|
|
245
|
+
out = actx.output
|
|
246
|
+
root = actx.project_root
|
|
247
|
+
|
|
248
|
+
def _cmd_version(cmd: str) -> str:
|
|
249
|
+
try:
|
|
250
|
+
r = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5)
|
|
251
|
+
return r.stdout.strip().split("\n")[0]
|
|
252
|
+
except Exception:
|
|
253
|
+
return "not found"
|
|
254
|
+
|
|
255
|
+
node_ver = _cmd_version("node")
|
|
256
|
+
pnpm_ver = _cmd_version("pnpm")
|
|
257
|
+
|
|
258
|
+
git_branch = ""
|
|
259
|
+
try:
|
|
260
|
+
r = subprocess.run(
|
|
261
|
+
["git", "branch", "--show-current"],
|
|
262
|
+
cwd=root,
|
|
263
|
+
capture_output=True,
|
|
264
|
+
text=True,
|
|
265
|
+
timeout=5,
|
|
266
|
+
)
|
|
267
|
+
git_branch = r.stdout.strip()
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
profile = resolve_active_profile_name(actx.profile)
|
|
272
|
+
apps_count = sum(1 for a in actx.app_names if (get_app_dir(root, a)).is_dir())
|
|
273
|
+
|
|
274
|
+
sections: list[tuple[str, list[tuple[str, str]]]] = [
|
|
275
|
+
(
|
|
276
|
+
"kctl-react",
|
|
277
|
+
[
|
|
278
|
+
("Version", __version__),
|
|
279
|
+
("Profile", profile),
|
|
280
|
+
("Project root", str(root)),
|
|
281
|
+
("Git branch", git_branch or "[dim]unknown[/dim]"),
|
|
282
|
+
("Node", node_ver),
|
|
283
|
+
("pnpm", pnpm_ver),
|
|
284
|
+
("Apps", f"{apps_count}/{len(actx.app_names)}"),
|
|
285
|
+
],
|
|
286
|
+
),
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
out.detail(
|
|
290
|
+
"Info",
|
|
291
|
+
sections,
|
|
292
|
+
data_for_json={
|
|
293
|
+
"version": __version__,
|
|
294
|
+
"profile": profile,
|
|
295
|
+
"root": str(root),
|
|
296
|
+
"git_branch": git_branch,
|
|
297
|
+
"node": node_ver,
|
|
298
|
+
"pnpm": pnpm_ver,
|
|
299
|
+
"apps": apps_count,
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command()
|
|
305
|
+
def dashboard(
|
|
306
|
+
ctx: typer.Context,
|
|
307
|
+
watch: Annotated[bool, typer.Option("--watch", "-w", help="Continuously refresh.")] = False,
|
|
308
|
+
interval: Annotated[int, typer.Option("--interval", "-i", help="Refresh interval in seconds.")] = 10,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Show monorepo overview dashboard."""
|
|
311
|
+
actx: AppContext = ctx.obj
|
|
312
|
+
|
|
313
|
+
if watch:
|
|
314
|
+
try:
|
|
315
|
+
while True:
|
|
316
|
+
data = _fetch_dashboard(actx)
|
|
317
|
+
actx.output.console.clear()
|
|
318
|
+
_display_dashboard(actx, data)
|
|
319
|
+
actx.output.text(f"\n[dim]Refreshing every {interval}s. Press Ctrl+C to stop.[/dim]")
|
|
320
|
+
time.sleep(interval)
|
|
321
|
+
except KeyboardInterrupt:
|
|
322
|
+
actx.output.info("Stopped watching.")
|
|
323
|
+
else:
|
|
324
|
+
data = _fetch_dashboard(actx)
|
|
325
|
+
_display_dashboard(actx, data)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@app.command()
|
|
329
|
+
def doctor(ctx: typer.Context) -> None:
|
|
330
|
+
"""Run comprehensive monorepo health checks.
|
|
331
|
+
|
|
332
|
+
Checks: node, pnpm, turbo, git, docker, all apps, packages, env files,
|
|
333
|
+
codegen config, and dependency installation.
|
|
334
|
+
"""
|
|
335
|
+
actx: AppContext = ctx.obj
|
|
336
|
+
run_doctor(actx)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@app.command()
|
|
340
|
+
def clean(
|
|
341
|
+
ctx: typer.Context,
|
|
342
|
+
app_name: Annotated[str | None, typer.Argument(help="App name (omit for all)")] = None,
|
|
343
|
+
all_: Annotated[bool, typer.Option("--all", "-a", help="Also remove node_modules.")] = False,
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Clean dist, .turbo, and coverage directories.
|
|
346
|
+
|
|
347
|
+
Examples:
|
|
348
|
+
kctl-react apps clean # Clean all apps
|
|
349
|
+
kctl-react apps clean sfa # Clean SFA only
|
|
350
|
+
kctl-react apps clean --all # Clean + remove node_modules
|
|
351
|
+
"""
|
|
352
|
+
actx: AppContext = ctx.obj
|
|
353
|
+
run_clean(actx, app_name=app_name, all_=all_)
|