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,318 @@
|
|
|
1
|
+
"""Deployment commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from kctl_react.core.callbacks import AppContext
|
|
13
|
+
from kctl_react.core.discovery import get_app_dir
|
|
14
|
+
from kctl_react.core.runner import run
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Docker Compose deployment management.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def build(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
app_name: Annotated[str, typer.Argument(help="App name to deploy")],
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Build and deploy an app via Docker Compose."""
|
|
25
|
+
actx: AppContext = ctx.obj
|
|
26
|
+
out = actx.output
|
|
27
|
+
root = actx.project_root
|
|
28
|
+
|
|
29
|
+
actx.validate_app(app_name)
|
|
30
|
+
compose_file = root / f"docker-compose.{app_name}.yml"
|
|
31
|
+
env_file = root / "env" / f".env.prod.{app_name}"
|
|
32
|
+
|
|
33
|
+
if not compose_file.exists():
|
|
34
|
+
out.error(f"Compose file not found: {compose_file}")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
if not env_file.exists():
|
|
37
|
+
out.error(f"Env file not found: {env_file}")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
out.info(f"Building and deploying {app_name}...")
|
|
41
|
+
try:
|
|
42
|
+
run(
|
|
43
|
+
["docker", "compose", "-f", str(compose_file), "--env-file", str(env_file), "up", "-d", "--build"],
|
|
44
|
+
cwd=root,
|
|
45
|
+
capture=False,
|
|
46
|
+
timeout=600,
|
|
47
|
+
)
|
|
48
|
+
out.success(f"{app_name} deployed")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
out.error(f"Deploy failed: {e}")
|
|
51
|
+
raise typer.Exit(1) from None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def status(
|
|
56
|
+
ctx: typer.Context,
|
|
57
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Show container status for a deployed app."""
|
|
60
|
+
actx: AppContext = ctx.obj
|
|
61
|
+
out = actx.output
|
|
62
|
+
root = actx.project_root
|
|
63
|
+
|
|
64
|
+
actx.validate_app(app_name)
|
|
65
|
+
compose_file = root / f"docker-compose.{app_name}.yml"
|
|
66
|
+
env_file = root / "env" / f".env.prod.{app_name}"
|
|
67
|
+
|
|
68
|
+
if not compose_file.exists():
|
|
69
|
+
out.warn(f"No compose file for {app_name}")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
cmd = ["docker", "compose", "-f", str(compose_file)]
|
|
73
|
+
if env_file.exists():
|
|
74
|
+
cmd.extend(["--env-file", str(env_file)])
|
|
75
|
+
cmd.append("ps")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
run(cmd, cwd=root, capture=False, timeout=30)
|
|
79
|
+
except Exception:
|
|
80
|
+
out.info(f"{app_name}: no containers running")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def logs(
|
|
85
|
+
ctx: typer.Context,
|
|
86
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
87
|
+
follow: Annotated[bool, typer.Option("--follow", "-f", help="Follow log output.")] = True,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Show logs for a deployed app."""
|
|
90
|
+
actx: AppContext = ctx.obj
|
|
91
|
+
out = actx.output
|
|
92
|
+
root = actx.project_root
|
|
93
|
+
|
|
94
|
+
actx.validate_app(app_name)
|
|
95
|
+
compose_file = root / f"docker-compose.{app_name}.yml"
|
|
96
|
+
env_file = root / "env" / f".env.prod.{app_name}"
|
|
97
|
+
|
|
98
|
+
if not compose_file.exists():
|
|
99
|
+
out.error(f"No compose file for {app_name}")
|
|
100
|
+
raise typer.Exit(1) from None
|
|
101
|
+
|
|
102
|
+
cmd = ["docker", "compose", "-f", str(compose_file)]
|
|
103
|
+
if env_file.exists():
|
|
104
|
+
cmd.extend(["--env-file", str(env_file)])
|
|
105
|
+
cmd.append("logs")
|
|
106
|
+
if follow:
|
|
107
|
+
cmd.append("-f")
|
|
108
|
+
cmd.append(app_name)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
proc = subprocess.Popen(cmd, cwd=root)
|
|
112
|
+
proc.wait()
|
|
113
|
+
except KeyboardInterrupt:
|
|
114
|
+
proc.send_signal(signal.SIGINT)
|
|
115
|
+
proc.wait()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def down(
|
|
120
|
+
ctx: typer.Context,
|
|
121
|
+
app_name: Annotated[str, typer.Argument(help="App name")],
|
|
122
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation.")] = False,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Stop a deployed app."""
|
|
125
|
+
actx: AppContext = ctx.obj
|
|
126
|
+
out = actx.output
|
|
127
|
+
root = actx.project_root
|
|
128
|
+
|
|
129
|
+
actx.validate_app(app_name)
|
|
130
|
+
compose_file = root / f"docker-compose.{app_name}.yml"
|
|
131
|
+
env_file = root / "env" / f".env.prod.{app_name}"
|
|
132
|
+
|
|
133
|
+
if not compose_file.exists():
|
|
134
|
+
out.error(f"No compose file for {app_name}")
|
|
135
|
+
raise typer.Exit(1) from None
|
|
136
|
+
|
|
137
|
+
if not force and not typer.confirm(f"Stop deployed app '{app_name}'?"):
|
|
138
|
+
raise typer.Exit(0)
|
|
139
|
+
|
|
140
|
+
out.info(f"Stopping {app_name}...")
|
|
141
|
+
cmd = ["docker", "compose", "-f", str(compose_file)]
|
|
142
|
+
if env_file.exists():
|
|
143
|
+
cmd.extend(["--env-file", str(env_file)])
|
|
144
|
+
cmd.append("down")
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
run(cmd, cwd=root, capture=False, timeout=60)
|
|
148
|
+
out.success(f"{app_name} stopped")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
out.error(f"Stop failed: {e}")
|
|
151
|
+
raise typer.Exit(1) from None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def images(ctx: typer.Context) -> None:
|
|
156
|
+
"""Show Docker images for all apps."""
|
|
157
|
+
actx: AppContext = ctx.obj
|
|
158
|
+
out = actx.output
|
|
159
|
+
root = actx.project_root
|
|
160
|
+
|
|
161
|
+
rows: list[list[str]] = []
|
|
162
|
+
json_data: list[dict] = []
|
|
163
|
+
|
|
164
|
+
for name in actx.app_names:
|
|
165
|
+
# Check for Dockerfile or compose file
|
|
166
|
+
compose_file = root / f"docker-compose.{name}.yml"
|
|
167
|
+
dockerfile = get_app_dir(root, name) / "Dockerfile"
|
|
168
|
+
has_compose = compose_file.exists()
|
|
169
|
+
has_dockerfile = dockerfile.exists()
|
|
170
|
+
|
|
171
|
+
# Check if image exists
|
|
172
|
+
image_name = f"kodemeio-{name}"
|
|
173
|
+
try:
|
|
174
|
+
result = run(
|
|
175
|
+
["docker", "images", image_name, "--format", "{{.Size}}\t{{.CreatedSince}}"],
|
|
176
|
+
cwd=root,
|
|
177
|
+
capture=True,
|
|
178
|
+
timeout=10,
|
|
179
|
+
)
|
|
180
|
+
if result.stdout.strip():
|
|
181
|
+
line = result.stdout.strip().splitlines()[0]
|
|
182
|
+
parts = line.split("\t")
|
|
183
|
+
img_size = parts[0] if parts else "[dim]--[/dim]"
|
|
184
|
+
img_age = parts[1] if len(parts) > 1 else ""
|
|
185
|
+
else:
|
|
186
|
+
img_size = "[dim]not built[/dim]"
|
|
187
|
+
img_age = ""
|
|
188
|
+
except Exception:
|
|
189
|
+
img_size = "[dim]no docker[/dim]"
|
|
190
|
+
img_age = ""
|
|
191
|
+
|
|
192
|
+
def icon(ok: bool) -> str:
|
|
193
|
+
return "[green]OK[/green]" if ok else "[dim]--[/dim]"
|
|
194
|
+
|
|
195
|
+
rows.append([name, icon(has_compose), icon(has_dockerfile), img_size, img_age])
|
|
196
|
+
json_data.append(
|
|
197
|
+
{
|
|
198
|
+
"app": name,
|
|
199
|
+
"compose": has_compose,
|
|
200
|
+
"dockerfile": has_dockerfile,
|
|
201
|
+
"image_size": img_size,
|
|
202
|
+
"age": img_age,
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
out.table(
|
|
207
|
+
"Docker Images",
|
|
208
|
+
[("App", "cyan"), ("Compose", ""), ("Dockerfile", ""), ("Image Size", "green"), ("Age", "dim")],
|
|
209
|
+
rows,
|
|
210
|
+
data_for_json=json_data,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.command()
|
|
215
|
+
def ps(ctx: typer.Context) -> None:
|
|
216
|
+
"""Show all running containers for kodemeio apps."""
|
|
217
|
+
actx: AppContext = ctx.obj
|
|
218
|
+
out = actx.output
|
|
219
|
+
root = actx.project_root
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
result = run(
|
|
223
|
+
["docker", "ps", "--format", "{{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Size}}", "--filter", "name=kodemeio"],
|
|
224
|
+
cwd=root,
|
|
225
|
+
capture=True,
|
|
226
|
+
timeout=10,
|
|
227
|
+
)
|
|
228
|
+
if not result.stdout.strip():
|
|
229
|
+
out.info("No kodemeio containers running")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
rows: list[list[str]] = []
|
|
233
|
+
for line in result.stdout.strip().splitlines():
|
|
234
|
+
parts = line.split("\t")
|
|
235
|
+
rows.append(parts + [""] * (4 - len(parts)))
|
|
236
|
+
|
|
237
|
+
out.table(
|
|
238
|
+
"Running Containers",
|
|
239
|
+
[("Name", "cyan"), ("Status", "green"), ("Ports", ""), ("Size", "dim")],
|
|
240
|
+
rows,
|
|
241
|
+
)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
out.error(f"Docker not available: {e}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@app.command("readiness")
|
|
247
|
+
def readiness(ctx: typer.Context) -> None:
|
|
248
|
+
"""Show deployment readiness status per app."""
|
|
249
|
+
actx: AppContext = ctx.obj
|
|
250
|
+
out = actx.output
|
|
251
|
+
root = actx.project_root
|
|
252
|
+
|
|
253
|
+
from kctl_react.commands.build import _find_build_dir, _format_size, _get_dist_size
|
|
254
|
+
|
|
255
|
+
out.info("Checking deployment readiness...")
|
|
256
|
+
|
|
257
|
+
rows: list[list[str]] = []
|
|
258
|
+
json_data: list[dict] = []
|
|
259
|
+
|
|
260
|
+
for name in actx.app_names:
|
|
261
|
+
app_dir = get_app_dir(root, name)
|
|
262
|
+
|
|
263
|
+
build_dir = _find_build_dir(app_dir)
|
|
264
|
+
has_build = build_dir is not None
|
|
265
|
+
build_size = _get_dist_size(build_dir) if build_dir else 0
|
|
266
|
+
|
|
267
|
+
has_dockerfile = (app_dir / "Dockerfile").exists()
|
|
268
|
+
has_compose = (app_dir / "docker-compose.yml").exists() or (app_dir / "docker-compose.yaml").exists()
|
|
269
|
+
docker_status = "[green]ready[/green]" if (has_dockerfile or has_compose) else "[dim]no docker[/dim]"
|
|
270
|
+
|
|
271
|
+
last_commit = "[dim]unknown[/dim]"
|
|
272
|
+
last_commit_iso = ""
|
|
273
|
+
try:
|
|
274
|
+
result = run(
|
|
275
|
+
["git", "log", "-1", "--format=%ci", "--", str(get_app_dir(root, name).relative_to(root)) + "/"],
|
|
276
|
+
cwd=root,
|
|
277
|
+
capture=True,
|
|
278
|
+
timeout=10,
|
|
279
|
+
)
|
|
280
|
+
if result.stdout.strip():
|
|
281
|
+
last_commit = result.stdout.strip()[:19]
|
|
282
|
+
last_commit_iso = result.stdout.strip()
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
pkg_file = app_dir / "package.json"
|
|
287
|
+
has_deploy_script = False
|
|
288
|
+
if pkg_file.exists():
|
|
289
|
+
try:
|
|
290
|
+
pkg = json.loads(pkg_file.read_text())
|
|
291
|
+
has_deploy_script = "deploy" in pkg.get("scripts", {})
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
has_env = (app_dir / ".env").exists()
|
|
296
|
+
has_env_example = (app_dir / ".env.example").exists()
|
|
297
|
+
|
|
298
|
+
build_status = _format_size(build_size) if has_build else "[yellow]not built[/yellow]"
|
|
299
|
+
|
|
300
|
+
rows.append([name, build_status, docker_status, last_commit])
|
|
301
|
+
json_data.append(
|
|
302
|
+
{
|
|
303
|
+
"app": name,
|
|
304
|
+
"build": {"built": has_build, "bytes": build_size},
|
|
305
|
+
"docker": {"dockerfile": has_dockerfile, "compose": has_compose},
|
|
306
|
+
"last_commit": last_commit_iso,
|
|
307
|
+
"deploy_script": has_deploy_script,
|
|
308
|
+
"env": has_env,
|
|
309
|
+
"env_example": has_env_example,
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
out.table(
|
|
314
|
+
"Deployment Readiness",
|
|
315
|
+
[("App", "cyan"), ("Build", "green"), ("Docker", ""), ("Last Commit", "dim")],
|
|
316
|
+
rows,
|
|
317
|
+
data_for_json=json_data,
|
|
318
|
+
)
|