Coverage for src / rhiza / cli.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-29 01:59 +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 

8 

9import typer 

10 

11from rhiza import __version__ 

12from rhiza.commands import init as init_cmd 

13from rhiza.commands import materialize as materialize_cmd 

14from rhiza.commands import validate as validate_cmd 

15from rhiza.commands.migrate import migrate as migrate_cmd 

16from rhiza.commands.uninstall import uninstall as uninstall_cmd 

17from rhiza.commands.welcome import welcome as welcome_cmd 

18 

19app = typer.Typer( 

20 help=( 

21 """ 

22 Rhiza - Manage reusable configuration templates for Python projects 

23 

24 \x1b]8;;https://jebel-quant.github.io/rhiza-cli/\x1b\\https://jebel-quant.github.io/rhiza-cli/\x1b]8;;\x1b\\ 

25 """ 

26 ), 

27 add_completion=True, 

28) 

29 

30 

31def version_callback(value: bool): 

32 """Print version information and exit. 

33 

34 Args: 

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

36 

37 Raises: 

38 typer.Exit: Always exits after printing version. 

39 """ 

40 if value: 

41 typer.echo(f"rhiza version {__version__}") 

42 raise typer.Exit() 

43 

44 

45@app.callback() 

46def main( 

47 version: bool = typer.Option( 

48 False, 

49 "--version", 

50 "-v", 

51 help="Show version and exit", 

52 callback=version_callback, 

53 is_eager=True, 

54 ), 

55): 

56 """Rhiza CLI main callback. 

57 

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

59 like --version. 

60 

61 Args: 

62 version: Version flag (handled by callback). 

63 """ 

64 

65 

66@app.command() 

67def init( 

68 target: Path = typer.Argument( 

69 default=Path("."), # default to current directory 

70 exists=True, 

71 file_okay=False, 

72 dir_okay=True, 

73 help="Target directory (defaults to current directory)", 

74 ), 

75 project_name: str = typer.Option( 

76 None, 

77 "--project-name", 

78 help="Custom project name (defaults to directory name)", 

79 ), 

80 package_name: str = typer.Option( 

81 None, 

82 "--package-name", 

83 help="Custom package name (defaults to normalized project name)", 

84 ), 

85 with_dev_dependencies: bool = typer.Option( 

86 False, 

87 "--with-dev-dependencies", 

88 help="Include development dependencies in pyproject.toml", 

89 ), 

90 git_host: str = typer.Option( 

91 None, 

92 "--git-host", 

93 help="Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. " 

94 "If not provided, will prompt interactively.", 

95 ), 

96): 

97 r"""Initialize or validate .github/rhiza/template.yml. 

98 

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

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

101 

102 The default template includes common Python project files. 

103 The --git-host option determines which CI/CD configuration to include: 

104 - github: includes .github folder (GitHub Actions workflows) 

105 - gitlab: includes .gitlab-ci.yml (GitLab CI configuration) 

106 

107 Examples: 

108 rhiza init 

109 rhiza init --git-host github 

110 rhiza init --git-host gitlab 

111 rhiza init /path/to/project 

112 rhiza init .. 

113 """ 

114 init_cmd( 

115 target, 

116 project_name=project_name, 

117 package_name=package_name, 

118 with_dev_dependencies=with_dev_dependencies, 

119 git_host=git_host, 

120 ) 

121 

122 

123@app.command() 

124def materialize( 

125 target: Path = typer.Argument( 

126 default=Path("."), # default to current directory 

127 exists=True, 

128 file_okay=False, 

129 dir_okay=True, 

130 help="Target git repository (defaults to current directory)", 

131 ), 

132 branch: str = typer.Option("main", "--branch", "-b", help="Rhiza branch to use"), 

133 target_branch: str = typer.Option( 

134 None, 

135 "--target-branch", 

136 "--checkout-branch", 

137 help="Create and checkout a new branch in the target repository for changes", 

138 ), 

139 force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"), 

140): 

141 r"""Inject Rhiza configuration templates into a target repository. 

142 

143 Materializes configuration files from the template repository specified 

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

145 

146 - Reads .github/rhiza/template.yml configuration 

147 - Performs a sparse clone of the template repository 

148 - Copies specified files/directories to your project 

149 - Respects exclusion patterns defined in the configuration 

150 - Files that already exist will NOT be overwritten unless --force is used. 

151 

152 Examples: 

153 rhiza materialize 

154 rhiza materialize --branch develop 

155 rhiza materialize --force 

156 rhiza materialize --target-branch feature/update-templates 

157 rhiza materialize /path/to/project -b v2.0 -y 

158 """ 

159 materialize_cmd(target, branch, target_branch, force) 

160 

161 

162@app.command() 

163def validate( 

164 target: Path = typer.Argument( 

165 default=Path("."), # default to current directory 

166 exists=True, 

167 file_okay=False, 

168 dir_okay=True, 

169 help="Target git repository (defaults to current directory)", 

170 ), 

171): 

172 r"""Validate Rhiza template configuration. 

173 

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

175 correct and semantically valid. 

176 

177 Performs comprehensive validation: 

178 - Checks if template.yml exists 

179 - Validates YAML syntax 

180 - Verifies required fields are present (template-repository, include) 

181 - Validates field types and formats 

182 - Ensures repository name follows owner/repo format 

183 - Confirms include paths are not empty 

184 

185 

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

187 

188 Examples: 

189 rhiza validate 

190 rhiza validate /path/to/project 

191 rhiza validate .. 

192 """ 

193 if not validate_cmd(target): 

194 raise typer.Exit(code=1) 

195 

196 

197@app.command() 

198def migrate( 

199 target: Path = typer.Argument( 

200 default=Path("."), # default to current directory 

201 exists=True, 

202 file_okay=False, 

203 dir_okay=True, 

204 help="Target git repository (defaults to current directory)", 

205 ), 

206): 

207 r"""Migrate project to the new .rhiza folder structure. 

208 

209 This command helps transition projects to use the new `.rhiza/` folder 

210 structure for storing Rhiza state and configuration files. It performs 

211 the following migrations: 

212 

213 - Creates the `.rhiza/` directory in the project root 

214 - Moves `.github/rhiza/template.yml` or `.github/template.yml` to `.rhiza/template.yml` 

215 - Moves `.rhiza.history` to `.rhiza/history` 

216 

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

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

219 

220 If files already exist in `.rhiza/`, the migration will skip them and leave 

221 the old files in place. You can manually remove old files after verifying 

222 the migration was successful. 

223 

224 Examples: 

225 rhiza migrate 

226 rhiza migrate /path/to/project 

227 """ 

228 migrate_cmd(target) 

229 

230 

231@app.command() 

232def welcome(): 

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

234 

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

236 and provides guidance on getting started with the tool. 

237 

238 Examples: 

239 rhiza welcome 

240 """ 

241 welcome_cmd() 

242 

243 

244@app.command() 

245def uninstall( 

246 target: Path = typer.Argument( 

247 default=Path("."), # default to current directory 

248 exists=True, 

249 file_okay=False, 

250 dir_okay=True, 

251 help="Target git repository (defaults to current directory)", 

252 ), 

253 force: bool = typer.Option( 

254 False, 

255 "--force", 

256 "-y", 

257 help="Skip confirmation prompt and proceed with deletion", 

258 ), 

259): 

260 r"""Remove all Rhiza-managed files from the repository. 

261 

262 Reads the `.rhiza.history` file and removes all files that were 

263 previously materialized by Rhiza templates. This provides a clean 

264 way to uninstall all template-managed files from a project. 

265 

266 The command will: 

267 - Read the list of files from `.rhiza.history` 

268 - Prompt for confirmation (unless --force is used) 

269 - Delete all listed files that exist 

270 - Remove empty directories left behind 

271 - Delete the `.rhiza.history` file itself 

272 

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

274 from your project. 

275 

276 Examples: 

277 rhiza uninstall 

278 rhiza uninstall --force 

279 rhiza uninstall /path/to/project 

280 rhiza uninstall /path/to/project -y 

281 """ 

282 uninstall_cmd(target, force)