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
kctl_react/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """kctl-react: Kodemeio React Monorepo CLI."""
2
+
3
+ __version__ = "0.6.2"
kctl_react/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m kctl_react."""
2
+
3
+ from kctl_react.cli import app
4
+
5
+ app()
kctl_react/cli.py ADDED
@@ -0,0 +1,201 @@
1
+ """Main CLI entry point for kctl-react."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from kctl_lib import KctlError, handle_cli_error
10
+
11
+ from kctl_react import __version__
12
+ from kctl_react.commands.a11y import app as a11y_app
13
+ from kctl_react.commands.affected import app as affected_app
14
+ from kctl_react.commands.apps import app as apps_app
15
+ from kctl_react.commands.build import app as build_app
16
+ from kctl_react.commands.bundle_cmd import app as bundle_app
17
+ from kctl_react.commands.cap import app as cap_app
18
+ from kctl_react.commands.clean import app as clean_app
19
+ from kctl_react.commands.compliance import app as compliance_app
20
+ from kctl_react.commands.codegen import app as codegen_app
21
+ from kctl_react.commands.config_cmd import app as config_app
22
+ from kctl_react.commands.dashboard import app as dashboard_app
23
+ from kctl_react.commands.deploy import app as deploy_app
24
+ from kctl_react.commands.deps import app as deps_app
25
+ from kctl_react.commands.dev import app as dev_app
26
+ from kctl_react.commands.docker_cmd import app as docker_app
27
+ from kctl_react.commands.doctor import app as doctor_app
28
+ from kctl_react.commands.e2e import app as e2e_app
29
+ from kctl_react.commands.env import app as env_app
30
+ from kctl_react.commands.i18n import app as i18n_app
31
+ from kctl_react.commands.lint import app as lint_app
32
+ from kctl_react.commands.maintenance import app as maintenance_app
33
+ from kctl_react.commands.monitor_cmd import app as monitor_app
34
+ from kctl_react.commands.observe import app as observe_app
35
+ from kctl_react.commands.packages import app as packages_app
36
+ from kctl_react.commands.perf import app as perf_app
37
+ from kctl_react.commands.pipeline import app as pipeline_app
38
+ from kctl_react.commands.pwa import app as pwa_app
39
+ from kctl_react.commands.scaffold import app as scaffold_app
40
+ from kctl_react.commands.security import app as security_app
41
+ from kctl_react.commands.skill_cmd import app as skill_app
42
+ from kctl_react.commands.state import app as state_app
43
+ from kctl_react.commands.test_cmd import app as test_app
44
+ from kctl_react.commands.ui_audit import app as ui_audit_app
45
+ from kctl_react.core.callbacks import AppContext
46
+ from kctl_react.core.plugins import discover_and_load_plugins
47
+ from kctl_lib.self_update import notify_if_outdated
48
+ from kctl_lib.tui import add_tui_command
49
+
50
+
51
+ def version_callback(value: bool) -> None:
52
+ if value:
53
+ typer.echo(f"kctl-react {__version__}")
54
+ raise typer.Exit()
55
+
56
+
57
+ app = typer.Typer(
58
+ name="kctl-react",
59
+ help="Kodemeio React Monorepo CLI — manage Vite PWAs + Next.js apps + shared packages.",
60
+ no_args_is_help=True,
61
+ rich_markup_mode="rich",
62
+ pretty_exceptions_enable=False,
63
+ )
64
+
65
+
66
+ @app.callback()
67
+ def main(
68
+ ctx: typer.Context,
69
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
70
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
71
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
72
+ root: Annotated[str | None, typer.Option("--root", help="Monorepo root override")] = None,
73
+ format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
74
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
75
+ version: Annotated[
76
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
77
+ ] = False,
78
+ ) -> None:
79
+ """Kodemeio React Monorepo CLI."""
80
+ ctx.ensure_object(dict)
81
+ ctx.obj = AppContext(
82
+ json_mode=json_output,
83
+ quiet=quiet,
84
+ profile=profile,
85
+ root_override=root,
86
+ format=format,
87
+ no_header=no_header,
88
+ )
89
+ notify_if_outdated(ctx.obj.output, "kctl-react", __version__)
90
+
91
+
92
+ @app.command("info")
93
+ def info_cmd(ctx: typer.Context) -> None:
94
+ """Show monorepo summary: apps, packages, and CLI version."""
95
+
96
+ actx: AppContext = ctx.obj
97
+ out = actx.output
98
+ root = actx.project_root
99
+
100
+ out.info(f"kctl-react {__version__}")
101
+ out.info(f"Root: {root}")
102
+ apps = actx.apps
103
+ vite_apps = [n for n, info in apps.items() if info.get("framework") == "vite"]
104
+ next_apps = [n for n, info in apps.items() if info.get("framework") == "nextjs"]
105
+ out.info(f"Apps ({len(apps)}): {len(vite_apps)} Vite + {len(next_apps)} Next.js")
106
+ if vite_apps:
107
+ out.info(f" Vite: {', '.join(vite_apps)}")
108
+ if next_apps:
109
+ out.info(f" Next.js: {', '.join(next_apps)}")
110
+ packages = actx.packages
111
+ out.info(f"Packages ({len(packages)}): {', '.join(packages)}")
112
+
113
+
114
+ # Register all command groups
115
+ app.add_typer(affected_app, name="affected")
116
+ app.add_typer(apps_app, name="apps")
117
+ app.add_typer(build_app, name="build")
118
+ app.add_typer(cap_app, name="cap")
119
+ app.add_typer(clean_app, name="clean")
120
+ app.add_typer(codegen_app, name="codegen")
121
+ app.add_typer(compliance_app, name="compliance")
122
+ app.add_typer(config_app, name="config")
123
+ app.add_typer(dashboard_app, name="dashboard")
124
+ app.add_typer(deploy_app, name="deploy")
125
+ app.add_typer(deps_app, name="deps")
126
+ app.add_typer(dev_app, name="dev")
127
+ app.add_typer(doctor_app, name="doctor")
128
+ app.add_typer(e2e_app, name="e2e")
129
+ app.add_typer(env_app, name="env")
130
+ app.add_typer(lint_app, name="lint")
131
+ app.add_typer(packages_app, name="packages")
132
+ app.add_typer(perf_app, name="perf")
133
+ app.add_typer(pipeline_app, name="pipeline")
134
+ app.add_typer(scaffold_app, name="scaffold")
135
+ app.add_typer(security_app, name="security")
136
+ app.add_typer(test_app, name="test")
137
+ app.add_typer(pwa_app, name="pwa")
138
+ app.add_typer(a11y_app, name="a11y")
139
+ app.add_typer(i18n_app, name="i18n")
140
+ app.add_typer(ui_audit_app, name="ui")
141
+ app.add_typer(state_app, name="state")
142
+ app.add_typer(bundle_app, name="bundle")
143
+ app.add_typer(observe_app, name="observe")
144
+ app.add_typer(docker_app, name="docker")
145
+ app.add_typer(monitor_app, name="monitor")
146
+ app.add_typer(maintenance_app, name="maintenance")
147
+ app.add_typer(skill_app, name="skill")
148
+
149
+ # Load third-party plugins via entry points
150
+ discover_and_load_plugins(app)
151
+ add_tui_command(app, service_key="react", version=__version__)
152
+
153
+
154
+ @app.command("self-update")
155
+ def self_update_cmd(ctx: typer.Context) -> None:
156
+ """Check for updates and upgrade kctl-react."""
157
+ actx = ctx.obj
158
+ out = actx.output
159
+
160
+ from kctl_lib.self_update import check_update
161
+ from kctl_lib.self_update import update as do_update
162
+
163
+ latest = check_update("kctl-react", __version__)
164
+ if latest:
165
+ out.info(f"Updating to {latest}...")
166
+ do_update("kctl-react")
167
+ out.success(f"Updated to {latest}")
168
+ else:
169
+ out.success("Already up to date")
170
+
171
+
172
+ @app.command()
173
+ def completions(
174
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
175
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
176
+ ) -> None:
177
+ """Generate or install shell completions."""
178
+ from kctl_lib.completions import get_completion_script, install_completions
179
+
180
+ if install:
181
+ path = install_completions("kctl-react", shell)
182
+ if path:
183
+ typer.echo(f"Completions installed to {path}")
184
+ else:
185
+ typer.echo(f"Could not install completions for {shell}", err=True)
186
+ raise typer.Exit(code=1)
187
+ else:
188
+ script = get_completion_script("kctl-react", shell)
189
+ typer.echo(script)
190
+
191
+
192
+ def _run() -> None:
193
+ """Entry point with error handling."""
194
+ try:
195
+ app()
196
+ except KctlError as e:
197
+ handle_cli_error(e)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ _run()
File without changes
@@ -0,0 +1,78 @@
1
+ """Accessibility auditing via axe-core."""
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
+ from kctl_react.core.runner import run_quiet
11
+
12
+ app = typer.Typer(help="Accessibility auditing (WCAG 2.1).")
13
+
14
+
15
+ @app.command()
16
+ def audit(
17
+ ctx: typer.Context,
18
+ app_name: Annotated[str, typer.Argument(help="App name")],
19
+ url: Annotated[str | None, typer.Option("--url", help="URL to audit")] = None,
20
+ ) -> None:
21
+ """Run axe-core accessibility audit."""
22
+ actx: AppContext = ctx.obj
23
+ out = actx.output
24
+ actx.validate_app(app_name)
25
+ target_url = url or f"http://localhost:{actx.apps[app_name].get('port', 5173)}"
26
+ # Use lighthouse for a11y if available
27
+ result = run_quiet(
28
+ [
29
+ "npx",
30
+ "lighthouse",
31
+ target_url,
32
+ "--only-categories=accessibility",
33
+ "--output=json",
34
+ "--chrome-flags=--headless --no-sandbox",
35
+ ],
36
+ cwd=actx.project_root,
37
+ )
38
+ if result.returncode != 0:
39
+ out.warn("Lighthouse not available. Install: npm i -g lighthouse")
40
+ out.info(f"Manual check: open {target_url} in Chrome DevTools → Lighthouse → Accessibility")
41
+ return
42
+ import json
43
+
44
+ try:
45
+ data = json.loads(result.stdout)
46
+ score = data.get("categories", {}).get("accessibility", {}).get("score", 0) * 100
47
+ audits = data.get("audits", {})
48
+ violations = [(k, v) for k, v in audits.items() if v.get("score") == 0 and v.get("details", {}).get("items")]
49
+ if out.json_mode:
50
+ out.raw_json({"score": score, "violations": len(violations), "url": target_url})
51
+ return
52
+ out.info(f"Accessibility score: {score:.0f}/100")
53
+ if violations:
54
+ rows = [
55
+ [v[0], v[1].get("title", ""), str(len(v[1].get("details", {}).get("items", [])))]
56
+ for v in violations[:20]
57
+ ]
58
+ out.table("Violations", [("ID", "red"), ("Title", ""), ("Elements", "dim")], rows)
59
+ else:
60
+ out.success("No accessibility violations found")
61
+ except (json.JSONDecodeError, KeyError):
62
+ out.error("Failed to parse Lighthouse output")
63
+
64
+
65
+ @app.command()
66
+ def report(ctx: typer.Context, app_name: Annotated[str, typer.Argument(help="App name")]) -> None:
67
+ """Full WCAG 2.1 AA compliance report."""
68
+ ctx.invoke(audit, app_name=app_name)
69
+
70
+
71
+ @app.command()
72
+ def violations(
73
+ ctx: typer.Context,
74
+ app_name: Annotated[str, typer.Argument(help="App name")],
75
+ severity: Annotated[str, typer.Option("--severity", help="Filter by severity")] = "all",
76
+ ) -> None:
77
+ """List accessibility violations filtered by severity."""
78
+ ctx.invoke(audit, app_name=app_name)
@@ -0,0 +1,170 @@
1
+ """Git-aware affected app detection."""
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
+ from kctl_react.core.git import get_affected_apps, get_changed_files, get_uncommitted_files
11
+ from kctl_react.core.runner import run_turbo
12
+
13
+ app = typer.Typer(help="Git-aware change detection and targeted operations.")
14
+
15
+
16
+ @app.callback(invoke_without_command=True)
17
+ def affected(
18
+ ctx: typer.Context,
19
+ base: Annotated[str, typer.Option("--base", "-b", help="Base git ref to compare against.")] = "HEAD~1",
20
+ ) -> None:
21
+ """Show which apps are affected by recent changes.
22
+
23
+ Detects changes in apps/, packages/, and root config files.
24
+ Package changes propagate to all apps that depend on them.
25
+
26
+ Examples:
27
+ kctl-react affected # Changes since last commit
28
+ kctl-react affected --base main # Changes since main branch
29
+ """
30
+ if ctx.invoked_subcommand is not None:
31
+ return
32
+
33
+ actx: AppContext = ctx.obj
34
+ out = actx.output
35
+ root = actx.project_root
36
+
37
+ apps = get_affected_apps(root, base)
38
+ changed = get_changed_files(root, base) + get_uncommitted_files(root)
39
+
40
+ if not apps:
41
+ out.info("No apps affected by current changes")
42
+ if out.json_mode:
43
+ out.raw_json({"affected": [], "changed_files": len(changed)})
44
+ return
45
+
46
+ # Categorize changes
47
+ app_changes: dict[str, list[str]] = {a: [] for a in apps}
48
+ pkg_changes: list[str] = []
49
+
50
+ for f in changed:
51
+ parts = f.split("/")
52
+ if len(parts) >= 3 and parts[0] == "apps" and parts[1] in ("spa", "web", "api") and parts[2] in apps:
53
+ app_changes[parts[2]].append(f)
54
+ elif len(parts) >= 2 and parts[0] == "apps" and parts[1] in apps:
55
+ app_changes[parts[1]].append(f)
56
+ elif parts[0] == "packages":
57
+ pkg_changes.append(f)
58
+
59
+ rows: list[list[str]] = []
60
+ json_data: list[dict] = []
61
+
62
+ for app_name in apps:
63
+ direct = len(app_changes.get(app_name, []))
64
+ reason = f"{direct} file(s)" if direct else "[dim]via package dep[/dim]"
65
+ rows.append([app_name, reason])
66
+ json_data.append({"app": app_name, "direct_changes": direct})
67
+
68
+ out.table(
69
+ f"Affected Apps (vs {base})",
70
+ [("App", "cyan"), ("Reason", "")],
71
+ rows,
72
+ data_for_json=json_data,
73
+ )
74
+
75
+ if pkg_changes:
76
+ out.info(f"{len(pkg_changes)} package file(s) changed")
77
+ out.success(f"{len(apps)} app(s) affected, {len(changed)} file(s) changed")
78
+
79
+
80
+ @app.command()
81
+ def test(
82
+ ctx: typer.Context,
83
+ base: Annotated[str, typer.Option("--base", "-b", help="Base git ref.")] = "HEAD~1",
84
+ coverage: Annotated[bool, typer.Option("--coverage", "-c", help="Run with coverage.")] = False,
85
+ ) -> None:
86
+ """Run tests only for affected apps."""
87
+ actx: AppContext = ctx.obj
88
+ out = actx.output
89
+ root = actx.project_root
90
+
91
+ apps = get_affected_apps(root, base)
92
+ if not apps:
93
+ out.success("No apps affected — skipping tests")
94
+ return
95
+
96
+ out.info(f"Running tests for {len(apps)} affected app(s): {', '.join(apps)}")
97
+ task = "test:ci" if coverage else "test"
98
+ failed = 0
99
+
100
+ for app_name in apps:
101
+ try:
102
+ run_turbo(task, root, filter_app=app_name, capture=False, timeout=300)
103
+ except Exception:
104
+ failed += 1
105
+
106
+ if failed:
107
+ out.error(f"{failed}/{len(apps)} app(s) failed")
108
+ raise typer.Exit(1) from None
109
+ out.success(f"All {len(apps)} affected app(s) passed")
110
+
111
+
112
+ @app.command()
113
+ def build(
114
+ ctx: typer.Context,
115
+ base: Annotated[str, typer.Option("--base", "-b", help="Base git ref.")] = "HEAD~1",
116
+ ) -> None:
117
+ """Build only affected apps."""
118
+ actx: AppContext = ctx.obj
119
+ out = actx.output
120
+ root = actx.project_root
121
+
122
+ apps = get_affected_apps(root, base)
123
+ if not apps:
124
+ out.success("No apps affected — skipping build")
125
+ return
126
+
127
+ out.info(f"Building {len(apps)} affected app(s): {', '.join(apps)}")
128
+ failed = 0
129
+
130
+ for app_name in apps:
131
+ try:
132
+ run_turbo("build", root, filter_app=app_name, capture=False, timeout=600)
133
+ except Exception:
134
+ failed += 1
135
+
136
+ if failed:
137
+ out.error(f"{failed}/{len(apps)} app(s) failed to build")
138
+ raise typer.Exit(1) from None
139
+ out.success(f"All {len(apps)} affected app(s) built")
140
+
141
+
142
+ @app.command()
143
+ def lint(
144
+ ctx: typer.Context,
145
+ base: Annotated[str, typer.Option("--base", "-b", help="Base git ref.")] = "HEAD~1",
146
+ ) -> None:
147
+ """Lint only affected apps."""
148
+ actx: AppContext = ctx.obj
149
+ out = actx.output
150
+ root = actx.project_root
151
+
152
+ apps = get_affected_apps(root, base)
153
+ if not apps:
154
+ out.success("No apps affected — skipping lint")
155
+ return
156
+
157
+ out.info(f"Linting {len(apps)} affected app(s): {', '.join(apps)}")
158
+ failed = 0
159
+
160
+ for app_name in apps:
161
+ try:
162
+ run_turbo("lint", root, filter_app=app_name, capture=False, timeout=300)
163
+ run_turbo("type-check", root, filter_app=app_name, capture=False, timeout=300)
164
+ except Exception:
165
+ failed += 1
166
+
167
+ if failed:
168
+ out.error(f"{failed}/{len(apps)} app(s) have lint errors")
169
+ raise typer.Exit(1) from None
170
+ out.success(f"All {len(apps)} affected app(s) passed lint")