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,368 @@
1
+ """Configuration management commands.
2
+
3
+ Initialize, view, and manage CLI profiles and connection settings.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from kctl_react.core.callbacks import AppContext
14
+ from kctl_react.core.config import (
15
+ CONFIG_FILE,
16
+ SERVICE_KEY,
17
+ ServiceConfig,
18
+ get_all_services_in_profile,
19
+ get_default_profile,
20
+ get_profile_names,
21
+ get_service_config,
22
+ load_raw_config,
23
+ remove_profile,
24
+ resolve_active_profile_name,
25
+ resolve_project_root,
26
+ save_raw_config,
27
+ set_default_profile,
28
+ set_service_config,
29
+ )
30
+
31
+ app = typer.Typer(help="Manage CLI configuration and profiles.")
32
+
33
+
34
+ @app.command()
35
+ def init(
36
+ ctx: typer.Context,
37
+ root: Annotated[str | None, typer.Option("--root", help="Monorepo root directory.")] = None,
38
+ api_url: Annotated[str | None, typer.Option("--api-url", help="Backend API URL.")] = None,
39
+ name: Annotated[str | None, typer.Option("--name", "-n", help="Profile name.")] = None,
40
+ ) -> None:
41
+ """Initialize CLI configuration (interactive if no flags given)."""
42
+ actx: AppContext = ctx.obj
43
+ out = actx.output
44
+
45
+ profile_name = name or typer.prompt("Profile name", default="default")
46
+ project_root = root or str(resolve_project_root())
47
+ backend_url = api_url or typer.prompt("Backend API URL", default="https://api.kodeme.io")
48
+
49
+ # Verify project root
50
+ from pathlib import Path
51
+
52
+ root_path = Path(project_root)
53
+ if (root_path / "turbo.json").exists():
54
+ out.success(f"Monorepo detected at {project_root}")
55
+ else:
56
+ out.warn(f"No turbo.json found at {project_root}")
57
+ if not typer.confirm("Save configuration anyway?", default=False):
58
+ raise typer.Exit(code=1)
59
+
60
+ svc = ServiceConfig(project_root=project_root, api_url=backend_url)
61
+ set_service_config(profile_name, svc)
62
+
63
+ if len(get_profile_names()) <= 1:
64
+ set_default_profile(profile_name)
65
+
66
+ out.success(f"Configuration saved to {CONFIG_FILE}")
67
+ out.kv("Profile", profile_name)
68
+ out.kv("Service", SERVICE_KEY)
69
+ out.kv("Project root", project_root)
70
+ out.kv("API URL", backend_url)
71
+
72
+
73
+ @app.command()
74
+ def add(
75
+ ctx: typer.Context,
76
+ name: Annotated[str, typer.Argument(help="Profile name (e.g. abcfood, staging)")],
77
+ root: Annotated[str | None, typer.Option("--root", help="Monorepo root directory.")] = None,
78
+ api_url: Annotated[str | None, typer.Option("--api-url", help="Backend API URL.")] = None,
79
+ odoo_url: Annotated[str | None, typer.Option("--odoo-url", help="Odoo URL.")] = None,
80
+ odoo_db: Annotated[str | None, typer.Option("--odoo-db", help="Odoo database name.")] = None,
81
+ set_default: Annotated[bool, typer.Option("--default", help="Set as default profile.")] = False,
82
+ ) -> None:
83
+ """Add or update a profile's React monorepo connection."""
84
+ actx: AppContext = ctx.obj
85
+ out = actx.output
86
+
87
+ project_root = root or typer.prompt("Monorepo root", default=str(resolve_project_root()))
88
+ backend_url = api_url or typer.prompt("Backend API URL", default="https://api.kodeme.io")
89
+
90
+ existing = get_service_config(name)
91
+ if existing.project_root and not typer.confirm(f"Profile '{name}' already has {SERVICE_KEY} config. Overwrite?"):
92
+ raise typer.Exit(0)
93
+
94
+ svc = ServiceConfig(project_root=project_root, api_url=backend_url)
95
+ if odoo_url:
96
+ svc.odoo_url = odoo_url
97
+ if odoo_db:
98
+ svc.odoo_db = odoo_db
99
+
100
+ set_service_config(name, svc)
101
+
102
+ if set_default or len(get_profile_names()) == 1:
103
+ set_default_profile(name)
104
+
105
+ out.success(f"Profile '{name}' -> {SERVICE_KEY} configured")
106
+ out.kv("Root", project_root)
107
+ out.kv("API URL", backend_url)
108
+
109
+
110
+ @app.command()
111
+ def use(
112
+ ctx: typer.Context,
113
+ name: Annotated[str, typer.Argument(help="Profile name to switch to")],
114
+ ) -> None:
115
+ """Switch the default profile."""
116
+ actx: AppContext = ctx.obj
117
+ out = actx.output
118
+
119
+ profiles = get_profile_names()
120
+ if name not in profiles:
121
+ out.error(f"Profile '{name}' not found")
122
+ out.info(f"Available: {', '.join(profiles)}")
123
+ raise typer.Exit(1)
124
+
125
+ old_default = get_default_profile()
126
+ set_default_profile(name)
127
+
128
+ svc = get_service_config(name)
129
+ out.success(f"Switched to '{name}' (root: {svc.project_root or 'auto-detect'})")
130
+ out.info(f"Previous default: {old_default}")
131
+
132
+
133
+ @app.command()
134
+ def remove(
135
+ ctx: typer.Context,
136
+ name: Annotated[str, typer.Argument(help="Profile name to remove")],
137
+ force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False,
138
+ service_only: Annotated[bool, typer.Option("--service-only", help="Only remove react config")] = False,
139
+ ) -> None:
140
+ """Remove a profile or just its React config."""
141
+ actx: AppContext = ctx.obj
142
+ out = actx.output
143
+
144
+ profiles = get_profile_names()
145
+ if name not in profiles:
146
+ out.error(f"Profile '{name}' not found")
147
+ raise typer.Exit(1)
148
+
149
+ if service_only:
150
+ if not force and not typer.confirm(f"Remove {SERVICE_KEY} config from '{name}'?"):
151
+ raise typer.Exit(0)
152
+ data = load_raw_config()
153
+ profile = data.get("profiles", {}).get(name, {})
154
+ profile.pop(SERVICE_KEY, None)
155
+ save_raw_config(data)
156
+ out.success(f"Removed {SERVICE_KEY} config from profile '{name}'")
157
+ else:
158
+ if not force:
159
+ services = get_all_services_in_profile(name)
160
+ svc_list = ", ".join(services.keys())
161
+ if not typer.confirm(f"Remove entire profile '{name}' (services: {svc_list})?"):
162
+ raise typer.Exit(0)
163
+ remove_profile(name)
164
+ out.success(f"Profile '{name}' removed")
165
+
166
+
167
+ @app.command()
168
+ def show(ctx: typer.Context) -> None:
169
+ """Show full configuration."""
170
+ actx: AppContext = ctx.obj
171
+ out = actx.output
172
+
173
+ default = get_default_profile()
174
+ profiles = get_profile_names()
175
+
176
+ if out.json_mode:
177
+ out.raw_json(load_raw_config())
178
+ return
179
+
180
+ sections: list[tuple[str, list[tuple[str, str]]]] = []
181
+
182
+ sections.append(
183
+ (
184
+ "General",
185
+ [
186
+ ("Config file", str(CONFIG_FILE)),
187
+ ("Default profile", default),
188
+ ("Total profiles", str(len(profiles))),
189
+ ("This CLI", f"kctl-react -> service key: {SERVICE_KEY}"),
190
+ ],
191
+ )
192
+ )
193
+
194
+ for pname in profiles:
195
+ marker = " [green](default)[/green]" if pname == default else ""
196
+ services = get_all_services_in_profile(pname)
197
+
198
+ kvs: list[tuple[str, str]] = []
199
+ for svc_name, svc_data in services.items():
200
+ if not isinstance(svc_data, dict):
201
+ continue
202
+ indicator = "[green]●[/green]" if svc_name == SERVICE_KEY else "[dim]○[/dim]"
203
+ svc_root = svc_data.get("project_root", svc_data.get("url", ""))
204
+ kvs.append((f"{indicator} {svc_name}", svc_root))
205
+
206
+ if not kvs:
207
+ kvs.append(("(empty)", "no services configured"))
208
+
209
+ sections.append((f"Profile: {pname}{marker}", kvs))
210
+
211
+ out.detail("Configuration", sections)
212
+
213
+
214
+ @app.command("set")
215
+ def set_(
216
+ ctx: typer.Context,
217
+ key: Annotated[str, typer.Argument(help="Config key (e.g. project_root, api_url, odoo_url, odoo_db)")],
218
+ value: Annotated[str, typer.Argument(help="Value to set")],
219
+ ) -> None:
220
+ """Set a configuration value."""
221
+ actx: AppContext = ctx.obj
222
+ out = actx.output
223
+
224
+ if key == "default_profile":
225
+ set_default_profile(value)
226
+ out.success(f"Default profile set to: {value}")
227
+ return
228
+
229
+ pname = resolve_active_profile_name(actx.profile)
230
+ svc = get_service_config(pname)
231
+
232
+ valid_fields = {"project_root", "api_url", "odoo_url", "odoo_db"}
233
+ if key not in valid_fields:
234
+ out.error(f"Unknown key: {key}")
235
+ out.info(f"Valid keys: {', '.join(sorted(valid_fields))}, default_profile")
236
+ raise typer.Exit(1)
237
+
238
+ setattr(svc, key, value)
239
+ set_service_config(pname, svc)
240
+ out.success(f"[{pname}] {SERVICE_KEY}.{key} = {value}")
241
+
242
+
243
+ @app.command()
244
+ def profiles(ctx: typer.Context) -> None:
245
+ """List all profiles."""
246
+ actx: AppContext = ctx.obj
247
+ out = actx.output
248
+
249
+ profile_names = get_profile_names()
250
+ if not profile_names:
251
+ out.warn("No profiles configured. Run: kctl-react config init")
252
+ return
253
+
254
+ active = resolve_active_profile_name(actx.profile)
255
+ default = get_default_profile()
256
+
257
+ rows: list[list[str]] = []
258
+ json_data: list[dict] = []
259
+
260
+ for pname in profile_names:
261
+ svc = get_service_config(pname)
262
+ all_services = get_all_services_in_profile(pname)
263
+ other_services = [s for s in all_services if s != SERVICE_KEY]
264
+
265
+ is_active = pname == active
266
+ status_marker = "[green]active[/green]" if is_active else ("default" if pname == default else "")
267
+
268
+ other_str = ", ".join(other_services) if other_services else "[dim]-[/dim]"
269
+
270
+ rows.append([pname, svc.project_root or "-", svc.api_url or "-", other_str, status_marker])
271
+ json_data.append(
272
+ {
273
+ "name": pname,
274
+ "project_root": svc.project_root,
275
+ "api_url": svc.api_url,
276
+ "other_services": other_services,
277
+ "active": is_active,
278
+ "default": pname == default,
279
+ }
280
+ )
281
+
282
+ out.table(
283
+ "Profiles",
284
+ [("Name", "cyan"), ("Project Root", ""), ("API URL", ""), ("Other Services", "dim"), ("", "green")],
285
+ rows,
286
+ data_for_json=json_data,
287
+ )
288
+
289
+
290
+ @app.command()
291
+ def current(ctx: typer.Context) -> None:
292
+ """Show the active profile and project root."""
293
+ actx: AppContext = ctx.obj
294
+ out = actx.output
295
+
296
+ active = resolve_active_profile_name(actx.profile)
297
+ svc = get_service_config(active)
298
+ root = resolve_project_root(profile_name=actx.profile, root_override=actx.root_override)
299
+
300
+ source = "config default"
301
+ if actx.profile:
302
+ source = "--profile flag"
303
+ elif os.environ.get("KCTL_REACT_PROFILE"):
304
+ source = "KCTL_REACT_PROFILE env var"
305
+
306
+ turbo_exists = (root / "turbo.json").exists()
307
+
308
+ sections: list[tuple[str, list[tuple[str, str]]]] = [
309
+ (
310
+ "Active Connection",
311
+ [
312
+ ("Profile", active),
313
+ ("Service", SERVICE_KEY),
314
+ ("Source", source),
315
+ ("Project root", str(root)),
316
+ ("Monorepo detected", "[green]Yes[/green]" if turbo_exists else "[red]No[/red]"),
317
+ ("API URL", svc.api_url or "[dim]not set[/dim]"),
318
+ ],
319
+ ),
320
+ ]
321
+
322
+ if svc.odoo_url or svc.odoo_db:
323
+ sections.append(
324
+ (
325
+ "Odoo Settings",
326
+ [
327
+ ("Odoo URL", svc.odoo_url or "[dim]not set[/dim]"),
328
+ ("Odoo DB", svc.odoo_db or "[dim]not set[/dim]"),
329
+ ],
330
+ )
331
+ )
332
+
333
+ all_services = get_all_services_in_profile(active)
334
+ other = {k: v for k, v in all_services.items() if k != SERVICE_KEY and isinstance(v, dict)}
335
+ if other:
336
+ sections.append(
337
+ (
338
+ "Other Services in Profile",
339
+ [(svc_name, v.get("url", v.get("project_root", "(no url)"))) for svc_name, v in other.items()],
340
+ )
341
+ )
342
+
343
+ out.detail(
344
+ "Current Profile",
345
+ sections,
346
+ data_for_json={
347
+ "profile": active,
348
+ "service": SERVICE_KEY,
349
+ "source": source,
350
+ "project_root": str(root),
351
+ "monorepo_detected": turbo_exists,
352
+ "api_url": svc.api_url,
353
+ },
354
+ )
355
+
356
+
357
+ @app.command()
358
+ def test(ctx: typer.Context) -> None:
359
+ """Verify configuration is valid."""
360
+ actx: AppContext = ctx.obj
361
+ out = actx.output
362
+ active = resolve_active_profile_name(actx.profile)
363
+ out.info(f"Testing profile '{active}' \u2192 {SERVICE_KEY}")
364
+ svc = get_service_config(active)
365
+ if not svc or not svc.project_root:
366
+ out.error("No configuration found. Run: kctl-react config init")
367
+ raise typer.Exit(1)
368
+ out.success(f"Configuration valid for profile '{active}'")
@@ -0,0 +1,163 @@
1
+ """Top-level dashboard command group."""
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.core.callbacks import AppContext
13
+ from kctl_react.core.discovery import get_app_dir
14
+
15
+ app = typer.Typer(help="Monorepo overview dashboard.")
16
+
17
+
18
+ def _fetch_dashboard(actx: AppContext) -> dict:
19
+ """Collect all dashboard data."""
20
+ root = actx.project_root
21
+
22
+ apps_found = sum(1 for a in actx.app_names if (get_app_dir(root, a)).is_dir())
23
+ pkgs_found = sum(1 for p in actx.packages if (root / "packages" / p / "package.json").exists())
24
+
25
+ total_tests = 0
26
+ for name in actx.app_names:
27
+ src = get_app_dir(root, name) / "src"
28
+ if src.is_dir():
29
+ total_tests += len(list(src.rglob("*.test.ts"))) + len(list(src.rglob("*.test.tsx")))
30
+
31
+ built_apps = sum(
32
+ 1
33
+ for a in actx.app_names
34
+ if (get_app_dir(root, a) / "dist").is_dir() or (get_app_dir(root, a) / ".next").is_dir()
35
+ )
36
+
37
+ running = 0
38
+ app_running: dict[str, bool] = {}
39
+ for name in actx.app_names:
40
+ port = actx.apps[name]["port"]
41
+ try:
42
+ r = httpx.get(f"http://localhost:{port}", timeout=1, follow_redirects=True)
43
+ is_up = r.status_code < 500
44
+ except (httpx.HTTPError, Exception):
45
+ is_up = False
46
+ app_running[name] = is_up
47
+ if is_up:
48
+ running += 1
49
+
50
+ has_node_modules = (root / "node_modules").is_dir()
51
+ has_lockfile = (root / "pnpm-lock.yaml").exists()
52
+
53
+ git_branch = ""
54
+ try:
55
+ result = subprocess.run(
56
+ ["git", "branch", "--show-current"],
57
+ cwd=root,
58
+ capture_output=True,
59
+ text=True,
60
+ timeout=5,
61
+ )
62
+ git_branch = result.stdout.strip()
63
+ except Exception:
64
+ pass
65
+
66
+ return {
67
+ "apps_total": len(actx.app_names),
68
+ "apps_found": apps_found,
69
+ "packages_total": len(actx.packages),
70
+ "packages_found": pkgs_found,
71
+ "test_files": total_tests,
72
+ "built_apps": built_apps,
73
+ "running_apps": running,
74
+ "app_running": app_running,
75
+ "has_node_modules": has_node_modules,
76
+ "has_lockfile": has_lockfile,
77
+ "git_branch": git_branch,
78
+ "project_root": str(root),
79
+ }
80
+
81
+
82
+ def _display_dashboard(actx: AppContext, data: dict) -> None:
83
+ """Render dashboard output."""
84
+ out = actx.output
85
+
86
+ if out.json_mode:
87
+ out.raw_json(data)
88
+ return
89
+
90
+ sections: list[tuple[str, list[tuple[str, str]]]] = []
91
+
92
+ sections.append(
93
+ (
94
+ "Project",
95
+ [
96
+ ("Root", data["project_root"]),
97
+ ("Git branch", data["git_branch"] or "[dim]unknown[/dim]"),
98
+ ("Node modules", "[green]installed[/green]" if data["has_node_modules"] else "[red]missing[/red]"),
99
+ ("Lock file", "[green]OK[/green]" if data["has_lockfile"] else "[red]missing[/red]"),
100
+ ],
101
+ )
102
+ )
103
+
104
+ sections.append(
105
+ (
106
+ "Resources",
107
+ [
108
+ ("Apps", f"{data['apps_found']}/{data['apps_total']}"),
109
+ ("Shared packages", f"{data['packages_found']}/{data['packages_total']}"),
110
+ ("Test files", str(data["test_files"])),
111
+ ("Built apps", f"{data['built_apps']}/{data['apps_total']}"),
112
+ ("Running dev servers", f"{data['running_apps']}/{data['apps_total']}"),
113
+ ],
114
+ )
115
+ )
116
+
117
+ app_running = data.get("app_running", {})
118
+ app_kvs: list[tuple[str, str]] = []
119
+ for name in actx.app_names:
120
+ app_info = actx.apps[name]
121
+ port = app_info["port"]
122
+ is_running = app_running.get(name, False)
123
+
124
+ app_dir = actx.get_app_dir(name)
125
+ built = (app_dir / "dist").is_dir() or (app_dir / ".next").is_dir()
126
+ status_parts = []
127
+ if is_running:
128
+ status_parts.append(f"[green]:{port}[/green]")
129
+ if built:
130
+ status_parts.append("[blue]built[/blue]")
131
+ if not status_parts:
132
+ status_parts.append("[dim]idle[/dim]")
133
+
134
+ app_kvs.append((name, " ".join(status_parts)))
135
+
136
+ sections.append(("Apps", app_kvs))
137
+
138
+ root_name = data.get("project_root", "").rstrip("/").split("/")[-1]
139
+ out.detail(f"{root_name} Dashboard", sections, data_for_json=data)
140
+
141
+
142
+ @app.command("show")
143
+ def show(
144
+ ctx: typer.Context,
145
+ watch: Annotated[bool, typer.Option("--watch", "-w", help="Continuously refresh.")] = False,
146
+ interval: Annotated[int, typer.Option("--interval", "-i", help="Refresh interval in seconds.")] = 10,
147
+ ) -> None:
148
+ """Show monorepo overview dashboard."""
149
+ actx: AppContext = ctx.obj
150
+
151
+ if watch:
152
+ try:
153
+ while True:
154
+ data = _fetch_dashboard(actx)
155
+ actx.output.console.clear()
156
+ _display_dashboard(actx, data)
157
+ actx.output.text(f"\n[dim]Refreshing every {interval}s. Press Ctrl+C to stop.[/dim]")
158
+ time.sleep(interval)
159
+ except KeyboardInterrupt:
160
+ actx.output.info("Stopped watching.")
161
+ else:
162
+ data = _fetch_dashboard(actx)
163
+ _display_dashboard(actx, data)