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,306 @@
1
+ """Lint and type-check commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from kctl_react.core.analyzers import read_tsconfig_strict
13
+ from kctl_react.core.callbacks import AppContext
14
+ from kctl_react.core.discovery import get_app_dir
15
+ from kctl_react.core.exceptions import CommandError
16
+ from kctl_react.core.runner import run, run_turbo
17
+
18
+ app = typer.Typer(help="Lint, type-check, and format code.")
19
+
20
+ _SUBCOMMANDS = {"format", "strict-check", "tsconfig-audit", "conventions"}
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Core logic functions (shared by callback dispatch and @app.command stubs)
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def _run_strict_check(actx: AppContext) -> None:
29
+ """Check that every app has strict: true in its tsconfig.json."""
30
+ out = actx.output
31
+ root = actx.project_root
32
+
33
+ rows: list[list[str]] = []
34
+ json_data: list[dict[str, str]] = []
35
+ any_not_strict = False
36
+
37
+ for app_name in actx.app_names:
38
+ app_dir = get_app_dir(root, app_name)
39
+ is_strict = read_tsconfig_strict(app_dir)
40
+ status = "strict" if is_strict else "[red]NOT strict[/red]"
41
+ rows.append([app_name, status])
42
+ json_data.append({"app": app_name, "strict": str(is_strict).lower()})
43
+ if not is_strict:
44
+ any_not_strict = True
45
+
46
+ out.table(
47
+ "TypeScript Strict Mode",
48
+ [("App", "cyan"), ("Status", "green")],
49
+ rows,
50
+ data_for_json=json_data,
51
+ )
52
+
53
+ if any_not_strict:
54
+ out.error("Some apps are missing strict: true in tsconfig.json")
55
+ raise typer.Exit(1) from None
56
+
57
+ out.success("All apps have strict: true")
58
+
59
+
60
+ def _run_tsconfig_audit(actx: AppContext) -> None:
61
+ """Audit tsconfig.json settings for strict and jsx across all apps."""
62
+ out = actx.output
63
+ root = actx.project_root
64
+
65
+ rows: list[list[str]] = []
66
+ json_data: list[dict[str, str]] = []
67
+ any_issues = False
68
+
69
+ for app_name in actx.app_names:
70
+ app_dir = get_app_dir(root, app_name)
71
+ tsconfig_path = app_dir / "tsconfig.json"
72
+
73
+ if not tsconfig_path.exists():
74
+ rows.append([app_name, "[red]MISSING[/red]", "tsconfig.json not found"])
75
+ json_data.append({"app": app_name, "status": "MISSING", "issues": "tsconfig.json not found"})
76
+ any_issues = True
77
+ continue
78
+
79
+ try:
80
+ text = tsconfig_path.read_text()
81
+ # Strip single-line // comments (tsconfig allows them)
82
+ text = re.sub(r"//.*", "", text)
83
+ data = json.loads(text)
84
+ except (json.JSONDecodeError, OSError) as exc:
85
+ rows.append([app_name, "[red]INVALID[/red]", f"Parse error: {exc}"])
86
+ json_data.append({"app": app_name, "status": "INVALID", "issues": f"Parse error: {exc}"})
87
+ any_issues = True
88
+ continue
89
+
90
+ compiler_opts = data.get("compilerOptions", {})
91
+ issues: list[str] = []
92
+
93
+ if not compiler_opts.get("strict", False):
94
+ issues.append("strict: true missing")
95
+ if compiler_opts.get("jsx") != "react-jsx":
96
+ issues.append(f'jsx: expected "react-jsx", got {compiler_opts.get("jsx")!r}')
97
+
98
+ if issues:
99
+ issues_str = "; ".join(issues)
100
+ rows.append([app_name, "[yellow]ISSUES[/yellow]", issues_str])
101
+ json_data.append({"app": app_name, "status": "ISSUES", "issues": issues_str})
102
+ any_issues = True
103
+ else:
104
+ rows.append([app_name, "[green]OK[/green]", ""])
105
+ json_data.append({"app": app_name, "status": "OK", "issues": ""})
106
+
107
+ out.table(
108
+ "tsconfig.json Audit",
109
+ [("App", "cyan"), ("Status", "green"), ("Issues", "yellow")],
110
+ rows,
111
+ data_for_json=json_data,
112
+ )
113
+
114
+ if any_issues:
115
+ out.error("Some apps have tsconfig.json issues")
116
+ raise typer.Exit(1) from None
117
+
118
+ out.success("All apps have consistent tsconfig.json settings")
119
+
120
+
121
+ def _run_conventions(actx: AppContext, app_name: str) -> None:
122
+ """Check a single app for convention violations."""
123
+ out = actx.output
124
+ root = actx.project_root
125
+
126
+ actx.validate_app(app_name)
127
+
128
+ src_dir = get_app_dir(root, app_name) / "src"
129
+
130
+ rows: list[list[str]] = []
131
+ json_data: list[dict[str, str]] = []
132
+
133
+ _SKIP_DIRS = {"generated", "node_modules"}
134
+
135
+ def _iter_ts_files(base: Path) -> list[Path]:
136
+ files: list[Path] = []
137
+ for p in base.rglob("*.ts"):
138
+ if not any(part in _SKIP_DIRS for part in p.parts):
139
+ files.append(p)
140
+ for p in base.rglob("*.tsx"):
141
+ if not any(part in _SKIP_DIRS for part in p.parts):
142
+ files.append(p)
143
+ return files
144
+
145
+ if src_dir.is_dir():
146
+ for file_path in _iter_ts_files(src_dir):
147
+ try:
148
+ lines = file_path.read_text().splitlines()
149
+ except OSError:
150
+ continue
151
+ rel = str(file_path.relative_to(get_app_dir(root, app_name)))
152
+ for line_num, line in enumerate(lines, start=1):
153
+ if '"use client"' in line or "'use client'" in line:
154
+ rows.append([rel, str(line_num), "use client directive", line.strip()])
155
+ json_data.append(
156
+ {"file": rel, "line": str(line_num), "issue": "use client directive", "code": line.strip()}
157
+ )
158
+ if re.search(r'from\s+["\'](\.\./){2,}', line):
159
+ rows.append([rel, str(line_num), "deep relative import", line.strip()])
160
+ json_data.append(
161
+ {"file": rel, "line": str(line_num), "issue": "deep relative import", "code": line.strip()}
162
+ )
163
+
164
+ if rows:
165
+ out.table(
166
+ f"Convention Issues: {app_name}",
167
+ [("File", "cyan"), ("Line", "yellow"), ("Issue", "red"), ("Code", "dim")],
168
+ rows,
169
+ data_for_json=json_data,
170
+ )
171
+ out.error(f"{len(rows)} convention issue(s) found in {app_name}")
172
+ raise typer.Exit(1) from None
173
+
174
+ out.success(f"No convention issues found in {app_name}")
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Typer commands
179
+ # ---------------------------------------------------------------------------
180
+
181
+
182
+ @app.callback(invoke_without_command=True)
183
+ def lint(
184
+ ctx: typer.Context,
185
+ app_name: Annotated[str | None, typer.Option("--app", "-a", help="App name to lint (omit for all apps)")] = None,
186
+ fix: Annotated[bool, typer.Option("--fix", "-f", help="Auto-fix issues.")] = False,
187
+ ) -> None:
188
+ """Run ESLint + TypeScript type-check.
189
+
190
+ Examples:
191
+ kctl-react lint # Lint all apps
192
+ kctl-react lint --app sfa # Lint SFA only
193
+ kctl-react lint --app sfa --fix
194
+ kctl-react lint strict-check
195
+ kctl-react lint tsconfig-audit
196
+ kctl-react lint conventions sfa
197
+ """
198
+ if ctx.invoked_subcommand is not None:
199
+ return
200
+
201
+ actx: AppContext = ctx.obj
202
+ out = actx.output
203
+ root = actx.project_root
204
+
205
+ if app_name:
206
+ actx.validate_app(app_name)
207
+
208
+ target = app_name or "all apps"
209
+ failed = False
210
+
211
+ # ESLint
212
+ out.info(f"Running ESLint on {target}...")
213
+ try:
214
+ run_turbo("lint", root, filter_app=app_name, capture=False, timeout=300)
215
+ out.success("ESLint passed")
216
+ except CommandError:
217
+ out.error("ESLint failed")
218
+ failed = True
219
+ except Exception as e:
220
+ out.error(f"ESLint error: {e}")
221
+ failed = True
222
+
223
+ # TypeScript type-check
224
+ out.info(f"Running type-check on {target}...")
225
+ try:
226
+ run_turbo("type-check", root, filter_app=app_name, capture=False, timeout=300)
227
+ out.success("Type-check passed")
228
+ except CommandError:
229
+ out.error("Type-check failed")
230
+ failed = True
231
+ except Exception as e:
232
+ out.error(f"Type-check error: {e}")
233
+ failed = True
234
+
235
+ if failed:
236
+ raise typer.Exit(1) from None
237
+
238
+ out.success(f"All quality checks passed: {target}")
239
+
240
+
241
+ @app.command()
242
+ def format(
243
+ ctx: typer.Context,
244
+ check: Annotated[bool, typer.Option("--check", help="Check only, don't fix.")] = False,
245
+ ) -> None:
246
+ """Run Prettier on the codebase."""
247
+ actx: AppContext = ctx.obj
248
+ out = actx.output
249
+ root = actx.project_root
250
+
251
+ if check:
252
+ out.info("Checking formatting...")
253
+ cmd = ["pnpm", "format-check"]
254
+ else:
255
+ out.info("Formatting code...")
256
+ cmd = ["pnpm", "format"]
257
+
258
+ try:
259
+ run(cmd, cwd=root, capture=False, timeout=120)
260
+ out.success("Formatting OK")
261
+ except CommandError:
262
+ out.error("Formatting issues found")
263
+ raise typer.Exit(1) from None
264
+
265
+
266
+ @app.command("strict-check")
267
+ def strict_check(ctx: typer.Context) -> None:
268
+ """Verify all apps have TypeScript strict mode enabled.
269
+
270
+ Examples:
271
+ kctl-react lint strict-check
272
+ kctl-react --json lint strict-check
273
+ """
274
+ actx: AppContext = ctx.obj
275
+ _run_strict_check(actx)
276
+
277
+
278
+ @app.command("tsconfig-audit")
279
+ def tsconfig_audit(ctx: typer.Context) -> None:
280
+ """Audit tsconfig.json settings for consistency across all apps.
281
+
282
+ Checks that each app has strict: true and jsx: react-jsx.
283
+
284
+ Examples:
285
+ kctl-react lint tsconfig-audit
286
+ """
287
+ actx: AppContext = ctx.obj
288
+ _run_tsconfig_audit(actx)
289
+
290
+
291
+ @app.command()
292
+ def conventions(
293
+ ctx: typer.Context,
294
+ app_name: Annotated[str, typer.Argument(help="App name to check")],
295
+ ) -> None:
296
+ """Check project conventions for an app.
297
+
298
+ Scans .ts/.tsx files for anti-patterns:
299
+ - 'use client' directive (Next.js pattern, wrong for Vite)
300
+ - Deep relative imports (../../) — should use @/ alias
301
+
302
+ Examples:
303
+ kctl-react lint conventions sfa
304
+ """
305
+ actx: AppContext = ctx.obj
306
+ _run_conventions(actx, app_name)
@@ -0,0 +1,308 @@
1
+ """Maintenance commands: health-report, cleanup, dr-status, count-test-files, deps-sync."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
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
+
15
+ app = typer.Typer(help="Maintenance utilities for the React monorepo.")
16
+
17
+ # Artifact directories to clean up
18
+ _CLEANUP_TARGETS = [
19
+ "dist",
20
+ ".turbo",
21
+ "coverage",
22
+ Path("node_modules") / ".cache",
23
+ Path("node_modules") / ".vite",
24
+ ]
25
+
26
+
27
+ def _count_test_files(app_dir: Path) -> int:
28
+ """Count .test.ts and .test.tsx files under <app_dir>/src."""
29
+ src = app_dir / "src"
30
+ if not src.is_dir():
31
+ return 0
32
+ return len(list(src.rglob("*.test.ts")) + list(src.rglob("*.test.tsx")))
33
+
34
+
35
+ def _collect_dep_versions(
36
+ root: Path,
37
+ app_names: list[str],
38
+ package_names: list[str],
39
+ ) -> dict[str, dict[str, str]]:
40
+ """Collect dependency versions from apps and packages.
41
+
42
+ Returns: {dep_name: {source_name: version, ...}, ...}
43
+ """
44
+ versions: dict[str, dict[str, str]] = {}
45
+
46
+ sources: list[tuple[str, Path]] = []
47
+ for name in app_names:
48
+ sources.append((name, get_app_dir(root, name) / "package.json"))
49
+ for name in package_names:
50
+ sources.append((name, root / "packages" / name / "package.json"))
51
+
52
+ for source_name, pkg_file in sources:
53
+ if not pkg_file.exists():
54
+ continue
55
+ try:
56
+ pkg = json.loads(pkg_file.read_text())
57
+ except Exception:
58
+ continue
59
+ all_deps: dict[str, str] = {}
60
+ all_deps.update(pkg.get("dependencies", {}))
61
+ all_deps.update(pkg.get("devDependencies", {}))
62
+ for dep, ver in all_deps.items():
63
+ if dep not in versions:
64
+ versions[dep] = {}
65
+ versions[dep][source_name] = ver
66
+
67
+ return versions
68
+
69
+
70
+ @app.command("health-report")
71
+ def health_report(ctx: typer.Context) -> None:
72
+ """Show per-app health: test file count, codegen status, and dist presence."""
73
+ actx: AppContext = ctx.obj
74
+ out = actx.output
75
+ root = actx.project_root
76
+
77
+ rows: list[list[str]] = []
78
+ json_data: list[dict] = []
79
+
80
+ for name in actx.app_names:
81
+ app_dir = get_app_dir(root, name)
82
+ tests = _count_test_files(app_dir)
83
+ codegen = (app_dir / "src" / "generated").is_dir()
84
+ built = (app_dir / "dist").is_dir()
85
+ has_env = (app_dir / ".env").exists()
86
+
87
+ codegen_str = "[green]yes[/green]" if codegen else "[red]no[/red]"
88
+ built_str = "[green]yes[/green]" if built else "[dim]no[/dim]"
89
+ env_str = "[green]yes[/green]" if has_env else "[dim]no[/dim]"
90
+
91
+ rows.append([name, str(tests), codegen_str, built_str, env_str])
92
+ json_data.append(
93
+ {
94
+ "app": name,
95
+ "tests": tests,
96
+ "codegen": codegen,
97
+ "built": built,
98
+ "env": has_env,
99
+ }
100
+ )
101
+
102
+ out.table(
103
+ "Monorepo Health Report",
104
+ [
105
+ ("App", "cyan"),
106
+ ("Tests", "green"),
107
+ ("Codegen", ""),
108
+ ("Built", ""),
109
+ (".env", ""),
110
+ ],
111
+ rows,
112
+ data_for_json=json_data,
113
+ )
114
+
115
+
116
+ @app.command("cleanup")
117
+ def cleanup(
118
+ ctx: typer.Context,
119
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Show what would be removed without deleting")] = False,
120
+ ) -> None:
121
+ """Remove build artifacts (dist/, .turbo/, coverage/, node_modules/.cache)."""
122
+ actx: AppContext = ctx.obj
123
+ out = actx.output
124
+ root = actx.project_root
125
+
126
+ found: list[dict] = []
127
+
128
+ # Check apps
129
+ for name in actx.app_names:
130
+ app_dir = get_app_dir(root, name)
131
+ for target in _CLEANUP_TARGETS:
132
+ path = app_dir / target
133
+ if path.exists():
134
+ found.append({"path": str(path.relative_to(root)), "app": name, "target": str(target)})
135
+
136
+ # Check root
137
+ for target in _CLEANUP_TARGETS:
138
+ path = root / target
139
+ if path.exists():
140
+ found.append({"path": str(path.relative_to(root)), "app": "root", "target": str(target)})
141
+
142
+ if not found:
143
+ out.success("Nothing to clean — workspace is already tidy")
144
+ if out.json_mode:
145
+ out.raw_json([])
146
+ return
147
+
148
+ if out.json_mode:
149
+ out.raw_json(found)
150
+ return
151
+
152
+ label = "[dim](dry-run)[/dim]" if dry_run else ""
153
+ out.info(f"Cleaning build artifacts {label}...")
154
+
155
+ for item in found:
156
+ path = root / item["path"]
157
+ if dry_run:
158
+ out.info(f" Would remove: {item['path']}")
159
+ else:
160
+ try:
161
+ if path.is_dir():
162
+ shutil.rmtree(path)
163
+ else:
164
+ path.unlink()
165
+ out.info(f" Removed: {item['path']}")
166
+ except Exception as e:
167
+ out.warn(f" Could not remove {item['path']}: {e}")
168
+
169
+ if dry_run:
170
+ out.info(f"Dry-run complete — {len(found)} item(s) would be removed")
171
+ else:
172
+ out.success(f"Cleaned {len(found)} artifact(s)")
173
+
174
+
175
+ @app.command("dr-status")
176
+ def dr_status(ctx: typer.Context) -> None:
177
+ """Check deployment readiness: Dockerfile, docker-compose, and .env files."""
178
+ actx: AppContext = ctx.obj
179
+ out = actx.output
180
+ root = actx.project_root
181
+
182
+ rows: list[list[str]] = []
183
+ json_data: list[dict] = []
184
+
185
+ for name in actx.app_names:
186
+ app_dir = get_app_dir(root, name)
187
+
188
+ dockerfile = (app_dir / "Dockerfile").exists()
189
+ compose = (app_dir / "docker-compose.yml").exists() or (app_dir / "docker-compose.yaml").exists()
190
+ env_file = (app_dir / ".env").exists()
191
+ env_example = (app_dir / ".env.example").exists()
192
+
193
+ # Ready when at least Dockerfile is present
194
+ is_ready = dockerfile
195
+ status = "[green]ready[/green]" if is_ready else "[yellow]not ready[/yellow]"
196
+
197
+ def _icon(v: bool) -> str:
198
+ return "[green]yes[/green]" if v else "[dim]no[/dim]"
199
+
200
+ rows.append([name, _icon(dockerfile), _icon(compose), _icon(env_file), _icon(env_example), status])
201
+ json_data.append(
202
+ {
203
+ "app": name,
204
+ "build": dockerfile,
205
+ "docker": compose,
206
+ "env": env_file,
207
+ "env_example": env_example,
208
+ "ready": is_ready,
209
+ }
210
+ )
211
+
212
+ out.table(
213
+ "Deployment Readiness Status",
214
+ [
215
+ ("App", "cyan"),
216
+ ("Dockerfile", ""),
217
+ ("Compose", ""),
218
+ (".env", ""),
219
+ (".env.example", ""),
220
+ ("Status", ""),
221
+ ],
222
+ rows,
223
+ data_for_json=json_data,
224
+ )
225
+
226
+
227
+ @app.command("count-test-files")
228
+ def count_test_files_cmd(ctx: typer.Context) -> None:
229
+ """Count test files (.test.ts / .test.tsx) per app."""
230
+ actx: AppContext = ctx.obj
231
+ out = actx.output
232
+ root = actx.project_root
233
+
234
+ rows: list[list[str]] = []
235
+ json_data: list[dict] = []
236
+ total = 0
237
+
238
+ for name in actx.app_names:
239
+ app_dir = get_app_dir(root, name)
240
+ count = _count_test_files(app_dir)
241
+ total += count
242
+ rows.append([name, str(count)])
243
+ json_data.append({"app": name, "test_files": count})
244
+
245
+ rows.append(["[bold]TOTAL[/bold]", f"[bold]{total}[/bold]"])
246
+
247
+ out.table(
248
+ "Test File Count",
249
+ [("App", "cyan"), ("Test Files", "green")],
250
+ rows,
251
+ data_for_json=json_data,
252
+ )
253
+
254
+
255
+ @app.command("deps-sync")
256
+ def deps_sync(
257
+ ctx: typer.Context,
258
+ exclude: Annotated[str | None, typer.Option("--exclude", help="Comma-separated dep names to ignore")] = None,
259
+ ) -> None:
260
+ """Check dependency version consistency across all apps and packages."""
261
+ actx: AppContext = ctx.obj
262
+ out = actx.output
263
+ root = actx.project_root
264
+
265
+ excluded = {e.strip() for e in exclude.split(",")} if exclude else set()
266
+
267
+ # Collect all package names from packages/
268
+ package_names: list[str] = []
269
+ packages_dir = root / "packages"
270
+ if packages_dir.is_dir():
271
+ for p in sorted(packages_dir.iterdir()):
272
+ if (p / "package.json").exists():
273
+ package_names.append(p.name)
274
+
275
+ all_versions = _collect_dep_versions(root, actx.app_names, package_names)
276
+
277
+ # Find inconsistencies: same dep, multiple different versions
278
+ inconsistent: list[dict] = []
279
+ for dep, sources in sorted(all_versions.items()):
280
+ if dep in excluded:
281
+ continue
282
+ unique_versions = set(sources.values())
283
+ if len(unique_versions) > 1:
284
+ inconsistent.append({"package": dep, "versions": sources})
285
+
286
+ if not inconsistent:
287
+ out.success("All shared dependencies are in sync")
288
+ if out.json_mode:
289
+ out.raw_json([])
290
+ return
291
+
292
+ if out.json_mode:
293
+ out.raw_json(inconsistent)
294
+ return
295
+
296
+ out.warn(f"Found {len(inconsistent)} inconsistent dependency(s):")
297
+ rows: list[list[str]] = []
298
+ for item in inconsistent:
299
+ for source, ver in item["versions"].items():
300
+ rows.append([item["package"], source, ver])
301
+
302
+ out.table(
303
+ "Inconsistent Dependencies",
304
+ [("Package", "cyan"), ("App/Package", "dim"), ("Version", "yellow")],
305
+ rows,
306
+ data_for_json=inconsistent,
307
+ )
308
+ out.warn("Run `pnpm update <package>` to align versions")
@@ -0,0 +1,50 @@
1
+ """Production monitoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from kctl_lib.monitor_base import health_check_url, ssl_check
9
+
10
+ from kctl_react.core.callbacks import AppContext
11
+
12
+ app = typer.Typer(help="Production monitoring.")
13
+
14
+
15
+ @app.command()
16
+ def health(
17
+ ctx: typer.Context,
18
+ app_name: Annotated[str, typer.Argument(help="App name")],
19
+ url: Annotated[str | None, typer.Option("--url")] = None,
20
+ ) -> None:
21
+ """HTTP health check against deployed app."""
22
+ actx: AppContext = ctx.obj
23
+ out = actx.output
24
+ actx.validate_app(app_name)
25
+ target = url or f"https://{app_name}.kodeme.io"
26
+ result = health_check_url(target)
27
+ if out.json_mode:
28
+ out.raw_json(result)
29
+ return
30
+ if result["healthy"]:
31
+ out.success(f"{target} — {result['status_code']} ({result['latency_ms']}ms)")
32
+ else:
33
+ out.error(f"{target} — {result.get('error', result.get('status_code', 'unknown'))}")
34
+
35
+
36
+ @app.command("ssl")
37
+ def ssl_cmd(ctx: typer.Context, domain: Annotated[str, typer.Argument(help="Domain to check")]) -> None:
38
+ """Check SSL certificate."""
39
+ actx: AppContext = ctx.obj
40
+ out = actx.output
41
+ result = ssl_check(domain)
42
+ if out.json_mode:
43
+ out.raw_json(result)
44
+ return
45
+ if result.get("valid"):
46
+ out.success(
47
+ f"{domain} — SSL valid, expires in {result.get('days_remaining', '?')} days ({result.get('issuer', '')})"
48
+ )
49
+ else:
50
+ out.error(f"{domain} — SSL check failed: {result.get('error', 'unknown')}")
@@ -0,0 +1,34 @@
1
+ """Error tracking and observability."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_react.core.callbacks import AppContext
10
+
11
+ app = typer.Typer(help="Error tracking and observability.")
12
+
13
+
14
+ @app.command()
15
+ def sentry(ctx: typer.Context, app_name: Annotated[str | None, typer.Argument(help="App name")] = None) -> None:
16
+ """Show recent Sentry issues."""
17
+ actx: AppContext = ctx.obj
18
+ out = actx.output
19
+ out.info("Sentry integration — requires SENTRY_AUTH_TOKEN and SENTRY_ORG in env")
20
+ out.info("Configure via: kctl-react config set sentry_org kodemeio")
21
+
22
+
23
+ @app.command()
24
+ def errors(ctx: typer.Context, app_name: Annotated[str | None, typer.Argument(help="App name")] = None) -> None:
25
+ """Show error trends."""
26
+ ctx.invoke(sentry, app_name=app_name)
27
+
28
+
29
+ @app.command()
30
+ def uptime(ctx: typer.Context, app_name: Annotated[str | None, typer.Argument(help="App name")] = None) -> None:
31
+ """Show Gatus uptime status."""
32
+ actx: AppContext = ctx.obj
33
+ out = actx.output
34
+ out.info("Gatus integration — check https://status.kodeme.io")