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