rpr-cli 0.1.1__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 (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tomllib
5
+ from pathlib import Path
6
+
7
+ from rpr.checks.base import CheckResult
8
+
9
+
10
+ def check_workspace(project_dir: Path) -> list[CheckResult]:
11
+ results: list[CheckResult] = []
12
+
13
+ # NX workspace
14
+ nx_json = project_dir / "nx.json"
15
+ nx_data: dict[str, object] = {}
16
+ nx_ok = False
17
+ nx_cloud_disabled = False
18
+ if nx_json.exists():
19
+ try:
20
+ nx_data = json.loads(nx_json.read_text())
21
+ nx_ok = True
22
+ nx_cloud_disabled = nx_data.get("neverConnectToCloud") is True
23
+ except (OSError, json.JSONDecodeError):
24
+ nx_ok = False
25
+
26
+ results.append(
27
+ CheckResult(
28
+ group="Workspace",
29
+ area="NX workspace",
30
+ status=nx_ok,
31
+ path=str(nx_json) if nx_ok else "nx.json missing",
32
+ suggestion="rpr init <name>",
33
+ )
34
+ )
35
+ results.append(
36
+ CheckResult(
37
+ group="Workspace",
38
+ area="NX Cloud disabled",
39
+ status=nx_cloud_disabled,
40
+ path=str(nx_json)
41
+ if nx_cloud_disabled
42
+ else "nx.json missing neverConnectToCloud=true",
43
+ suggestion="rpr init <name>",
44
+ )
45
+ )
46
+
47
+ # UV workspace
48
+ pyproject = project_dir / "pyproject.toml"
49
+ pyproject_data: dict[str, object] = {}
50
+ uv_ok = False
51
+ if pyproject.exists():
52
+ try:
53
+ pyproject_data = tomllib.loads(pyproject.read_text())
54
+ tool = pyproject_data.get("tool", {})
55
+ uv_ok = "uv" in tool and "workspace" in tool["uv"]
56
+ except (OSError, tomllib.TOMLDecodeError):
57
+ uv_ok = False
58
+
59
+ results.append(
60
+ CheckResult(
61
+ group="Workspace",
62
+ area="UV workspace",
63
+ status=uv_ok,
64
+ path=str(pyproject)
65
+ if uv_ok
66
+ else "pyproject.toml with [tool.uv.workspace] missing",
67
+ suggestion="rpr init <name>",
68
+ )
69
+ )
70
+
71
+ tool = pyproject_data.get("tool", {}) if pyproject_data else {}
72
+ uv_require_version = False
73
+ pytest_config = False
74
+ dev_dependencies_ok = False
75
+ if tool:
76
+ uv_require_version = "require-version" in tool.get("uv", {})
77
+ pytest_config = "pytest" in tool and "ini_options" in tool["pytest"]
78
+
79
+ dependency_groups = pyproject_data.get("dependency-groups", {}) if pyproject_data else {}
80
+ dev_dependencies = dependency_groups.get("dev", []) if isinstance(dependency_groups, dict) else []
81
+ if isinstance(dev_dependencies, list):
82
+ required = {
83
+ "pytest",
84
+ "pytest-asyncio",
85
+ "pytest-cov",
86
+ "ipykernel",
87
+ "pre-commit",
88
+ "pyright",
89
+ "ruff",
90
+ "maturin",
91
+ }
92
+ dev_dependencies_ok = required.issubset(set(dev_dependencies))
93
+
94
+ results.append(
95
+ CheckResult(
96
+ group="Workspace",
97
+ area="Root Python tooling",
98
+ status=uv_require_version and pytest_config and dev_dependencies_ok,
99
+ path=str(pyproject)
100
+ if uv_require_version and pytest_config and dev_dependencies_ok
101
+ else "pyproject.toml missing guide-aligned UV/Python sections",
102
+ suggestion="rpr init <name>",
103
+ )
104
+ )
105
+
106
+ package_json = project_dir / "package.json"
107
+ node_workspace_ok = False
108
+ if package_json.exists():
109
+ try:
110
+ package_data = json.loads(package_json.read_text())
111
+ workspaces = package_data.get("workspaces", [])
112
+ node_workspace_ok = isinstance(workspaces, list) and {
113
+ "apps/*",
114
+ "packages/node/*",
115
+ }.issubset(set(workspaces))
116
+ except (OSError, json.JSONDecodeError):
117
+ node_workspace_ok = False
118
+
119
+ results.append(
120
+ CheckResult(
121
+ group="Workspace",
122
+ area="Node workspace",
123
+ status=node_workspace_ok,
124
+ path=str(package_json)
125
+ if node_workspace_ok
126
+ else "package.json missing npm workspaces",
127
+ suggestion="rpr init <name>",
128
+ )
129
+ )
130
+
131
+ pre_commit = project_dir / ".pre-commit-config.yaml"
132
+ pre_commit_ok = False
133
+ if pre_commit.exists():
134
+ content = pre_commit.read_text()
135
+ pre_commit_ok = all(
136
+ needle in content
137
+ for needle in (
138
+ "pre-commit-hooks",
139
+ "nbstripout",
140
+ "uv run ruff format",
141
+ "uv run ruff check --fix",
142
+ "npx nx run-many --target=lint --all --fix --exclude=engine",
143
+ )
144
+ )
145
+
146
+ results.append(
147
+ CheckResult(
148
+ group="Workspace",
149
+ area="Pre-commit config",
150
+ status=pre_commit_ok,
151
+ path=str(pre_commit)
152
+ if pre_commit_ok
153
+ else ".pre-commit-config.yaml missing guide-aligned hooks",
154
+ suggestion="rpr init <name>",
155
+ )
156
+ )
157
+
158
+ # .rpr.yaml
159
+ rpr_yaml = project_dir / ".rpr.yaml"
160
+ results.append(
161
+ CheckResult(
162
+ group="Workspace",
163
+ area=".rpr.yaml config",
164
+ status=rpr_yaml.exists(),
165
+ path=str(rpr_yaml) if rpr_yaml.exists() else ".rpr.yaml missing",
166
+ suggestion="rpr init <name>",
167
+ )
168
+ )
169
+
170
+ return results
rpr/cli.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rpr import __version__
7
+ from rpr.ui.console import console
8
+
9
+ app = typer.Typer(
10
+ name="rpr",
11
+ help="RPR CLI -- React Python Rust monorepo scaffolding orchestrator.",
12
+ no_args_is_help=False,
13
+ )
14
+
15
+ # --- Command Imports (must be after app is defined) ---
16
+ from rpr.commands import init as _init_mod # noqa: E402, F401
17
+ from rpr.commands import check as _check_mod # noqa: E402, F401
18
+ from rpr.commands import settings as _settings_mod # noqa: E402, F401
19
+ from rpr.commands import map as _map_mod # noqa: E402, F401
20
+ from rpr.commands import chat as _chat_mod # noqa: E402, F401
21
+
22
+ generate_app = typer.Typer(
23
+ name="generate",
24
+ help="Scaffold apps and packages (api, ui, domain, engine, storybook).",
25
+ no_args_is_help=True,
26
+ )
27
+ app.add_typer(generate_app, name="generate")
28
+
29
+ sync_app = typer.Typer(
30
+ name="sync",
31
+ help="Sync AI instructions to Cursor, Copilot, and Claude Code.",
32
+ no_args_is_help=True,
33
+ )
34
+ app.add_typer(sync_app, name="sync")
35
+
36
+ add_app = typer.Typer(
37
+ name="add",
38
+ help="Add code templates to the current project.",
39
+ no_args_is_help=True,
40
+ )
41
+ app.add_typer(add_app, name="add")
42
+
43
+ # Import subcommands
44
+ from rpr.commands.generate import domain as _domain_mod # noqa: E402, F401
45
+ from rpr.commands.generate import api as _api_mod # noqa: E402, F401
46
+ from rpr.commands.generate import ui as _ui_mod # noqa: E402, F401
47
+ from rpr.commands.generate import engine as _engine_mod # noqa: E402, F401
48
+ from rpr.commands.generate import storybook as _storybook_mod # noqa: E402, F401
49
+ from rpr.commands import sync as _sync_mod # noqa: E402, F401
50
+ from rpr.commands import add as _add_mod # noqa: E402, F401
51
+
52
+
53
+ def _version_callback(value: bool) -> None:
54
+ if value:
55
+ typer.echo(f"rpr {__version__}")
56
+ raise typer.Exit()
57
+
58
+
59
+ @app.callback(invoke_without_command=True)
60
+ def main(
61
+ ctx: typer.Context,
62
+ version: Annotated[
63
+ bool,
64
+ typer.Option(
65
+ "--version",
66
+ "-v",
67
+ help="Show the version and exit.",
68
+ callback=_version_callback,
69
+ is_eager=True,
70
+ ),
71
+ ] = False,
72
+ ) -> None:
73
+ if ctx.invoked_subcommand is None:
74
+ ctx.invoke(_chat_mod.chat)
75
+
76
+
77
+ if __name__ == "__main__":
78
+ app()
File without changes
rpr/commands/add.py ADDED
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from rpr.cli import add_app
11
+ from rpr.context import RunContext
12
+ from rpr.generators.base import SyncConfig, interpolate
13
+ from rpr.templates.registry import TEMPLATES
14
+ from rpr.workspace import derive_variables
15
+
16
+ console = Console()
17
+
18
+
19
+ def _extract_code_blocks(markdown: str, language: str) -> list[str]:
20
+ """Extract fenced code blocks from markdown matching the given language."""
21
+ pattern = rf"```{language}\s*\n(.*?)```"
22
+ matches = re.findall(pattern, markdown, re.DOTALL)
23
+ if not matches:
24
+ pattern = r"```\w*\s*\n(.*?)```"
25
+ matches = re.findall(pattern, markdown, re.DOTALL)
26
+ return matches
27
+
28
+
29
+ @add_app.command("template")
30
+ def template(
31
+ template_id: Annotated[
32
+ str | None,
33
+ typer.Argument(help="Template ID to add (e.g., mapper_util, dto_util)."),
34
+ ] = None,
35
+ list_templates: Annotated[
36
+ bool, typer.Option("--list", "-l", help="List all available templates.")
37
+ ] = False,
38
+ force: Annotated[
39
+ bool,
40
+ typer.Option(
41
+ "--force", "-f", help="Overwrite existing files without prompting."
42
+ ),
43
+ ] = False,
44
+ name: Annotated[
45
+ str | None,
46
+ typer.Option("--name", "-n", help="App name for variable interpolation."),
47
+ ] = None,
48
+ source: Annotated[
49
+ str | None,
50
+ typer.Option("--source", "-s", help="Path to the rules source directory."),
51
+ ] = None,
52
+ dry_run: Annotated[
53
+ bool, typer.Option("--dry-run", help="Preview changes without writing.")
54
+ ] = False,
55
+ ) -> None:
56
+ """Add a code template file to the current project."""
57
+ ctx = RunContext(dry_run=dry_run, project_dir=Path.cwd())
58
+ config_path = ctx.project_dir / ".rpr.yaml"
59
+ config = SyncConfig.load(config_path)
60
+
61
+ if name:
62
+ variables = derive_variables(name)
63
+ elif config.variables:
64
+ variables = config.variables
65
+ else:
66
+ # For list command, we don't necessarily want to prompt
67
+ if list_templates:
68
+ variables = {}
69
+ else:
70
+ project_name = typer.prompt("Project name", default=ctx.project_dir.name)
71
+ variables = derive_variables(project_name)
72
+
73
+ if list_templates:
74
+ console.print("\n[bold]Available templates:[/]\n")
75
+ for tid, entry in TEMPLATES.items():
76
+ console.print(f" [bold cyan]{tid:<20}[/] {entry.description}")
77
+ path_str = entry.target_path
78
+ if variables:
79
+ path_str = interpolate(path_str, variables)
80
+ console.print(f" {'':20} -> {path_str}")
81
+ console.print()
82
+ raise typer.Exit(0)
83
+
84
+ if not template_id:
85
+ template_id = typer.prompt("Template ID")
86
+
87
+ if template_id not in TEMPLATES:
88
+ console.print(f"[bold red]Error:[/] Unknown template: {template_id}")
89
+ console.print("Run 'rpr add template --list' to see available templates.")
90
+ raise typer.Exit(1)
91
+
92
+ entry = TEMPLATES[template_id]
93
+
94
+ source_dir = (
95
+ Path(source).expanduser().resolve() if source else _find_source_dir(config)
96
+ )
97
+ if not source_dir:
98
+ console.print(
99
+ "[bold red]Error:[/] Cannot find rules source. Use --source or create .rpr.yaml"
100
+ )
101
+ raise typer.Exit(1)
102
+
103
+ source_file = source_dir / entry.source_subdir / entry.source_filename
104
+ if not source_file.exists():
105
+ console.print(f"[bold red]Error:[/] Source file not found: {source_file}")
106
+ raise typer.Exit(1)
107
+
108
+ markdown = source_file.read_text()
109
+
110
+ lang_hint = "python" if entry.language == "python" else "ts"
111
+ code_blocks = _extract_code_blocks(markdown, lang_hint)
112
+
113
+ if not code_blocks:
114
+ console.print(f"[bold red]Error:[/] No code blocks found in {source_file}")
115
+ raise typer.Exit(1)
116
+
117
+ code = code_blocks[0].strip() + "\n"
118
+
119
+ # Robust interpolation
120
+ code = interpolate(code, variables)
121
+ # Legacy support for older templates that might use these placeholders
122
+ name_val = variables.get("name", "")
123
+ name_underscore_val = variables.get("name_underscore", "")
124
+ code = code.replace("your_app_domain", f"{name_underscore_val}_domain")
125
+ code = code.replace("your-app-domain", f"{name_val}-domain")
126
+ code = code.replace("my_app_domain", f"{name_underscore_val}_domain")
127
+ code = code.replace("my-app-domain", f"{name_val}-domain")
128
+
129
+ target_path_str = interpolate(entry.target_path, variables)
130
+ target_path = ctx.project_dir / target_path_str
131
+
132
+ if not dry_run and target_path.exists() and not force:
133
+ console.print(f"[bold yellow]Warning:[/] File already exists: {target_path}")
134
+ overwrite = typer.confirm("Overwrite?")
135
+ if not overwrite:
136
+ console.print("Skipped.")
137
+ raise typer.Exit(0)
138
+
139
+ console.print(f"\n[bold]Adding template:[/] {template_id}\n")
140
+ ctx.write_file(target_path, code)
141
+
142
+ console.print(f"\n[bold green]Done![/] Template written to {target_path}")
143
+
144
+
145
+ def _find_source_dir(config: SyncConfig) -> Path | None:
146
+ """Try to find the source directory from SyncConfig or common locations."""
147
+ if config.source:
148
+ p = Path(config.source).expanduser().resolve()
149
+ if p.exists():
150
+ # If the source points to 'instructions', we should look at its parent
151
+ # as templates are usually sibling directories to instructions.
152
+ if p.name == "instructions" and (p.parent / "special_files").exists():
153
+ return p.parent
154
+ return p
155
+
156
+ # Try relative to this file (development mode)
157
+ # src/rpr/commands/add.py -> parent x 2 is src/rpr/ -> / scaffolds
158
+ scaffold_root = Path(__file__).resolve().parent.parent / "scaffolds"
159
+ if (scaffold_root / "special_files").exists():
160
+ return scaffold_root
161
+
162
+ common = Path.home() / "development" / "project-getting-started"
163
+ if common.exists():
164
+ return common
165
+
166
+ return None
rpr/commands/chat.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from rpr.agent.bootstrap import resolve_agent_client
9
+ from rpr.agent.client import AgentClient
10
+ from rpr.application.chat_service import ChatCommandService
11
+ from rpr.cli import app
12
+ from rpr.context import RunContext
13
+ from rpr.ui.theme import settings_manager
14
+
15
+
16
+ def _get_agent_client(provider: str | None, model: str | None) -> AgentClient:
17
+ return resolve_agent_client(provider, model)
18
+
19
+
20
+ @app.command()
21
+ def chat(
22
+ provider: Annotated[
23
+ str | None, typer.Option(help="Override the AI provider to use.")
24
+ ] = None,
25
+ model: Annotated[
26
+ str | None, typer.Option(help="Override the model to use.")
27
+ ] = None,
28
+ approval_mode: Annotated[
29
+ str | None,
30
+ typer.Option(
31
+ help="Override the approval mode for this session.", case_sensitive=False
32
+ ),
33
+ ] = None,
34
+ ) -> None:
35
+ """Start an interactive chat session with the AI agent."""
36
+ try:
37
+ asyncio.run(
38
+ ChatCommandService.instance().run(
39
+ provider=provider,
40
+ model=model,
41
+ approval_mode_override=approval_mode,
42
+ client_factory=_get_agent_client,
43
+ context_factory=RunContext,
44
+ settings=settings_manager,
45
+ )
46
+ )
47
+ except ValueError as exc:
48
+ raise typer.BadParameter(str(exc)) from exc
rpr/commands/check.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rpr.application.checks import collect_check_report, serialize_check_results
9
+ from rpr.cli import app
10
+ from rpr.ui.console import console
11
+ from rpr.ui.renderers import render_heading, render_status_row
12
+
13
+
14
+ @app.command()
15
+ def check(
16
+ group: Annotated[
17
+ str | None,
18
+ typer.Option(
19
+ "--group",
20
+ help="Run only a specific group: workspace, instructions, packages.",
21
+ ),
22
+ ] = None,
23
+ as_json: Annotated[
24
+ bool, typer.Option("--json", help="Emit machine-readable JSON output.")
25
+ ] = False,
26
+ ) -> None:
27
+ """Inspect the current project and report setup status."""
28
+ project_dir = Path.cwd()
29
+ report = collect_check_report(project_dir, group=group)
30
+
31
+ if as_json:
32
+ typer.echo(json.dumps(serialize_check_results(report.results)))
33
+ if any(not result.status for result in report.results):
34
+ raise typer.Exit(1)
35
+ return
36
+
37
+ render_heading(f"rpr check — project: {report.project_name}")
38
+
39
+ current_group = None
40
+ for result in report.results:
41
+ if result.group != current_group:
42
+ current_group = result.group
43
+ console.print(f"[heading]{current_group}[/]")
44
+
45
+ icon_char = "✓" if result.status else "✗"
46
+ render_status_row(
47
+ icon_char, result.area, str(result.path), result.status, result.suggestion
48
+ )
49
+
50
+ console.print()
51
+
52
+ if any(not result.status for result in report.results):
53
+ raise typer.Exit(1)
File without changes