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
kctl_react/__init__.py
ADDED
kctl_react/__main__.py
ADDED
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")
|