Coverage for src / rhiza / cli.py: 100%
40 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-12 20:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-12 20:13 +0000
1"""Rhiza command-line interface (CLI).
3This module defines the Typer application entry points exposed by Rhiza.
4Commands are thin wrappers around implementations in `rhiza.commands.*`.
5"""
7from pathlib import Path
8from typing import Annotated
10import typer
12from rhiza import __version__
13from rhiza.commands import init as init_cmd
14from rhiza.commands import materialize as materialize_cmd
15from rhiza.commands import validate as validate_cmd
16from rhiza.commands.migrate import migrate as migrate_cmd
17from rhiza.commands.summarise import summarise as summarise_cmd
18from rhiza.commands.uninstall import uninstall as uninstall_cmd
19from rhiza.commands.welcome import welcome as welcome_cmd
21app = typer.Typer(
22 help=(
23 """
24 Rhiza - Manage reusable configuration templates for Python projects
26 https://jebel-quant.github.io/rhiza-cli/
27 """
28 ),
29 add_completion=True,
30)
33def version_callback(value: bool) -> None:
34 """Print version information and exit.
36 Args:
37 value: Whether the --version flag was provided.
39 Raises:
40 typer.Exit: Always exits after printing version.
41 """
42 if value:
43 typer.echo(f"rhiza version {__version__}")
44 raise typer.Exit()
47@app.callback()
48def main(
49 version: bool = typer.Option(
50 False,
51 "--version",
52 "-v",
53 help="Show version and exit",
54 callback=version_callback,
55 is_eager=True,
56 ),
57) -> None:
58 """Rhiza CLI main callback.
60 This callback is executed before any command. It handles global options
61 like --version.
63 Args:
64 version: Version flag (handled by callback).
65 """
68@app.command()
69def init(
70 target: Annotated[
71 Path,
72 typer.Argument(
73 exists=True,
74 file_okay=False,
75 dir_okay=True,
76 help="Target directory (defaults to current directory)",
77 ),
78 ] = Path("."),
79 project_name: str = typer.Option(
80 None,
81 "--project-name",
82 help="Custom project name (defaults to directory name)",
83 ),
84 package_name: str = typer.Option(
85 None,
86 "--package-name",
87 help="Custom package name (defaults to normalized project name)",
88 ),
89 with_dev_dependencies: bool = typer.Option(
90 False,
91 "--with-dev-dependencies",
92 help="Include development dependencies in pyproject.toml",
93 ),
94 git_host: str = typer.Option(
95 None,
96 "--git-host",
97 help="Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. "
98 "If not provided, will prompt interactively.",
99 ),
100 template_repository: str = typer.Option(
101 None,
102 "--template-repository",
103 help="Custom template repository (format: owner/repo). Defaults to 'jebel-quant/rhiza'.",
104 ),
105 template_branch: str = typer.Option(
106 None,
107 "--template-branch",
108 help="Custom template branch. Defaults to 'main'.",
109 ),
110) -> None:
111 r"""Initialize or validate .rhiza/template.yml.
113 Creates a default `.rhiza/template.yml` configuration file if one
114 doesn't exist, or validates the existing configuration.
116 The default template includes common Python project files.
117 The --git-host option determines which CI/CD configuration to include:
118 - github: includes .github folder (GitHub Actions workflows)
119 - gitlab: includes .gitlab-ci.yml (GitLab CI configuration)
121 Examples:
122 rhiza init
123 rhiza init --git-host github
124 rhiza init --git-host gitlab
125 rhiza init --template-repository myorg/my-templates
126 rhiza init --template-repository myorg/my-templates --template-branch develop
127 rhiza init /path/to/project
128 rhiza init ..
129 """
130 init_cmd(
131 target,
132 project_name=project_name,
133 package_name=package_name,
134 with_dev_dependencies=with_dev_dependencies,
135 git_host=git_host,
136 template_repository=template_repository,
137 template_branch=template_branch,
138 )
141@app.command()
142def materialize(
143 target: Annotated[
144 Path,
145 typer.Argument(
146 exists=True,
147 file_okay=False,
148 dir_okay=True,
149 help="Target git repository (defaults to current directory)",
150 ),
151 ] = Path("."),
152 branch: str = typer.Option("main", "--branch", "-b", help="Rhiza branch to use"),
153 target_branch: str = typer.Option(
154 None,
155 "--target-branch",
156 "--checkout-branch",
157 help="Create and checkout a new branch in the target repository for changes",
158 ),
159 force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"),
160) -> None:
161 r"""Inject Rhiza configuration templates into a target repository.
163 Materializes configuration files from the template repository specified
164 in .rhiza/template.yml into your project. This command:
166 - Reads .rhiza/template.yml configuration
167 - Performs a sparse clone of the template repository
168 - Copies specified files/directories to your project
169 - Respects exclusion patterns defined in the configuration
170 - Files that already exist will NOT be overwritten unless --force is used.
172 Examples:
173 rhiza materialize
174 rhiza materialize --branch develop
175 rhiza materialize --force
176 rhiza materialize --target-branch feature/update-templates
177 rhiza materialize /path/to/project -b v2.0 -y
178 """
179 materialize_cmd(target, branch, target_branch, force)
182@app.command()
183def validate(
184 target: Annotated[
185 Path,
186 typer.Argument(
187 exists=True,
188 file_okay=False,
189 dir_okay=True,
190 help="Target git repository (defaults to current directory)",
191 ),
192 ] = Path("."),
193) -> None:
194 r"""Validate Rhiza template configuration.
196 Validates the .rhiza/template.yml file to ensure it is syntactically
197 correct and semantically valid.
199 Performs comprehensive validation:
200 - Checks if template.yml exists
201 - Validates YAML syntax
202 - Verifies required fields are present (template-repository, include)
203 - Validates field types and formats
204 - Ensures repository name follows owner/repo format
205 - Confirms include paths are not empty
208 Returns exit code 0 on success, 1 on validation failure.
210 Examples:
211 rhiza validate
212 rhiza validate /path/to/project
213 rhiza validate ..
214 """
215 if not validate_cmd(target):
216 raise typer.Exit(code=1)
219@app.command()
220def migrate(
221 target: Annotated[
222 Path,
223 typer.Argument(
224 exists=True,
225 file_okay=False,
226 dir_okay=True,
227 help="Target git repository (defaults to current directory)",
228 ),
229 ] = Path("."),
230) -> None:
231 r"""Migrate project to the new .rhiza folder structure.
233 This command helps transition projects to use the new `.rhiza/` folder
234 structure for storing Rhiza state and configuration files. It performs
235 the following migrations:
237 - Creates the `.rhiza/` directory in the project root
238 - Moves `.github/rhiza/template.yml` or `.github/template.yml` to `.rhiza/template.yml`
239 - Moves `.rhiza.history` to `.rhiza/history`
241 The new `.rhiza/` folder structure separates Rhiza's state and configuration
242 from the `.github/` directory, providing better organization.
244 If files already exist in `.rhiza/`, the migration will skip them and leave
245 the old files in place. You can manually remove old files after verifying
246 the migration was successful.
248 Examples:
249 rhiza migrate
250 rhiza migrate /path/to/project
251 """
252 migrate_cmd(target)
255@app.command()
256def welcome() -> None:
257 r"""Display a friendly welcome message and explain what Rhiza is.
259 Shows a welcome message, explains Rhiza's purpose, key features,
260 and provides guidance on getting started with the tool.
262 Examples:
263 rhiza welcome
264 """
265 welcome_cmd()
268@app.command()
269def uninstall(
270 target: Annotated[
271 Path,
272 typer.Argument(
273 exists=True,
274 file_okay=False,
275 dir_okay=True,
276 help="Target git repository (defaults to current directory)",
277 ),
278 ] = Path("."),
279 force: bool = typer.Option(
280 False,
281 "--force",
282 "-y",
283 help="Skip confirmation prompt and proceed with deletion",
284 ),
285) -> None:
286 r"""Remove all Rhiza-managed files from the repository.
288 Reads the `.rhiza.history` file and removes all files that were
289 previously materialized by Rhiza templates. This provides a clean
290 way to uninstall all template-managed files from a project.
292 The command will:
293 - Read the list of files from `.rhiza.history`
294 - Prompt for confirmation (unless --force is used)
295 - Delete all listed files that exist
296 - Remove empty directories left behind
297 - Delete the `.rhiza.history` file itself
299 Use this command when you want to completely remove Rhiza templates
300 from your project.
302 Examples:
303 rhiza uninstall
304 rhiza uninstall --force
305 rhiza uninstall /path/to/project
306 rhiza uninstall /path/to/project -y
307 """
308 uninstall_cmd(target, force)
311@app.command()
312def summarise(
313 target: Annotated[
314 Path,
315 typer.Argument(
316 exists=True,
317 file_okay=False,
318 dir_okay=True,
319 help="Target git repository (defaults to current directory)",
320 ),
321 ] = Path("."),
322 output: Annotated[
323 Path | None,
324 typer.Option(
325 "--output",
326 "-o",
327 help="Output file path (defaults to stdout)",
328 ),
329 ] = None,
330) -> None:
331 r"""Generate a summary of staged changes for PR descriptions.
333 Analyzes staged git changes and generates a structured PR description
334 that includes:
336 - Summary statistics (files added/modified/deleted)
337 - Changes categorized by type (workflows, configs, docs, tests, etc.)
338 - Template repository information
339 - Last sync date
341 This is useful when creating pull requests after running `rhiza materialize`
342 to provide reviewers with a clear overview of what changed.
344 Examples:
345 rhiza summarise
346 rhiza summarise --output pr-description.md
347 rhiza summarise /path/to/project -o description.md
349 Typical workflow:
350 rhiza materialize
351 git add .
352 rhiza summarise --output pr-body.md
353 gh pr create --title "chore: Sync with rhiza" --body-file pr-body.md
354 """
355 summarise_cmd(target, output)