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

1"""Rhiza command-line interface (CLI). 

2 

3This module defines the Typer application entry points exposed by Rhiza. 

4Commands are thin wrappers around implementations in `rhiza.commands.*`. 

5""" 

6 

7from pathlib import Path 

8from typing import Annotated 

9 

10import typer 

11 

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 

20 

21app = typer.Typer( 

22 help=( 

23 """ 

24 Rhiza - Manage reusable configuration templates for Python projects 

25 

26 https://jebel-quant.github.io/rhiza-cli/ 

27 """ 

28 ), 

29 add_completion=True, 

30) 

31 

32 

33def version_callback(value: bool) -> None: 

34 """Print version information and exit. 

35 

36 Args: 

37 value: Whether the --version flag was provided. 

38 

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() 

45 

46 

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. 

59 

60 This callback is executed before any command. It handles global options 

61 like --version. 

62 

63 Args: 

64 version: Version flag (handled by callback). 

65 """ 

66 

67 

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. 

112 

113 Creates a default `.rhiza/template.yml` configuration file if one 

114 doesn't exist, or validates the existing configuration. 

115 

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) 

120 

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 ) 

139 

140 

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. 

162 

163 Materializes configuration files from the template repository specified 

164 in .rhiza/template.yml into your project. This command: 

165 

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. 

171 

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) 

180 

181 

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. 

195 

196 Validates the .rhiza/template.yml file to ensure it is syntactically 

197 correct and semantically valid. 

198 

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 

206 

207 

208 Returns exit code 0 on success, 1 on validation failure. 

209 

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) 

217 

218 

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. 

232 

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: 

236 

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` 

240 

241 The new `.rhiza/` folder structure separates Rhiza's state and configuration 

242 from the `.github/` directory, providing better organization. 

243 

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. 

247 

248 Examples: 

249 rhiza migrate 

250 rhiza migrate /path/to/project 

251 """ 

252 migrate_cmd(target) 

253 

254 

255@app.command() 

256def welcome() -> None: 

257 r"""Display a friendly welcome message and explain what Rhiza is. 

258 

259 Shows a welcome message, explains Rhiza's purpose, key features, 

260 and provides guidance on getting started with the tool. 

261 

262 Examples: 

263 rhiza welcome 

264 """ 

265 welcome_cmd() 

266 

267 

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. 

287 

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. 

291 

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 

298 

299 Use this command when you want to completely remove Rhiza templates 

300 from your project. 

301 

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) 

309 

310 

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. 

332 

333 Analyzes staged git changes and generates a structured PR description 

334 that includes: 

335 

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 

340 

341 This is useful when creating pull requests after running `rhiza materialize` 

342 to provide reviewers with a clear overview of what changed. 

343 

344 Examples: 

345 rhiza summarise 

346 rhiza summarise --output pr-description.md 

347 rhiza summarise /path/to/project -o description.md 

348 

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)