rhiza 0.7.1__py3-none-any.whl → 0.8.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.
- rhiza/_templates/basic/__init__.py.jinja2 +2 -0
- rhiza/_templates/basic/main.py.jinja2 +23 -0
- rhiza/_templates/basic/pyproject.toml.jinja2 +32 -0
- rhiza/cli.py +98 -10
- rhiza/commands/init.py +72 -42
- rhiza/commands/materialize.py +42 -14
- rhiza/commands/migrate.py +147 -0
- rhiza/commands/uninstall.py +151 -0
- rhiza/commands/validate.py +46 -22
- {rhiza-0.7.1.dist-info → rhiza-0.8.1.dist-info}/METADATA +83 -1
- rhiza-0.8.1.dist-info/RECORD +19 -0
- rhiza-0.7.1.dist-info/RECORD +0 -14
- {rhiza-0.7.1.dist-info → rhiza-0.8.1.dist-info}/WHEEL +0 -0
- {rhiza-0.7.1.dist-info → rhiza-0.8.1.dist-info}/entry_points.txt +0 -0
- {rhiza-0.7.1.dist-info → rhiza-0.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Main module for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def say_hello(name: str) -> str:
|
|
5
|
+
"""Say hello to the user.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
name: The name of the user.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
A greeting string.
|
|
12
|
+
"""
|
|
13
|
+
return f"Hello, {name}!"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Execute the main function."""
|
|
18
|
+
print(say_hello("World"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
|
23
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "{{ project_name }}"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = []
|
|
12
|
+
{% if with_dev_dependencies %}
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = [
|
|
16
|
+
"pytest-cov>=7.0.0",
|
|
17
|
+
"pytest-html>=4.1.1",
|
|
18
|
+
"pytest==9.0.2",
|
|
19
|
+
"pre-commit==4.5.1",
|
|
20
|
+
"marimo==0.18.4",
|
|
21
|
+
"pdoc>=16.0.0",
|
|
22
|
+
]
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/{{ package_name }}"]
|
|
27
|
+
{% if with_dev_dependencies %}
|
|
28
|
+
|
|
29
|
+
[tool.deptry]
|
|
30
|
+
# see https://deptry.com/usage/#pep-621-dev-dependency-groups
|
|
31
|
+
pep621_dev_dependency_groups = ["dev"]
|
|
32
|
+
{% endif %}
|
rhiza/cli.py
CHANGED
|
@@ -12,6 +12,8 @@ from rhiza import __version__
|
|
|
12
12
|
from rhiza.commands import init as init_cmd
|
|
13
13
|
from rhiza.commands import materialize as materialize_cmd
|
|
14
14
|
from rhiza.commands import validate as validate_cmd
|
|
15
|
+
from rhiza.commands.migrate import migrate as migrate_cmd
|
|
16
|
+
from rhiza.commands.uninstall import uninstall as uninstall_cmd
|
|
15
17
|
from rhiza.commands.welcome import welcome as welcome_cmd
|
|
16
18
|
|
|
17
19
|
app = typer.Typer(
|
|
@@ -70,14 +72,27 @@ def init(
|
|
|
70
72
|
dir_okay=True,
|
|
71
73
|
help="Target directory (defaults to current directory)",
|
|
72
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
|
+
),
|
|
73
90
|
):
|
|
74
91
|
r"""Initialize or validate .github/rhiza/template.yml.
|
|
75
92
|
|
|
76
|
-
\b
|
|
77
93
|
Creates a default `.github/rhiza/template.yml` configuration file if one
|
|
78
94
|
doesn't exist, or validates the existing configuration.
|
|
79
95
|
|
|
80
|
-
\b
|
|
81
96
|
The default template includes common Python project files:
|
|
82
97
|
- .github (workflows, actions, etc.)
|
|
83
98
|
- .editorconfig
|
|
@@ -86,13 +101,17 @@ def init(
|
|
|
86
101
|
- Makefile
|
|
87
102
|
- pytest.ini
|
|
88
103
|
|
|
89
|
-
\b
|
|
90
104
|
Examples:
|
|
91
105
|
rhiza init
|
|
92
106
|
rhiza init /path/to/project
|
|
93
107
|
rhiza init ..
|
|
94
108
|
"""
|
|
95
|
-
init_cmd(
|
|
109
|
+
init_cmd(
|
|
110
|
+
target,
|
|
111
|
+
project_name=project_name,
|
|
112
|
+
package_name=package_name,
|
|
113
|
+
with_dev_dependencies=with_dev_dependencies,
|
|
114
|
+
)
|
|
96
115
|
|
|
97
116
|
|
|
98
117
|
@app.command()
|
|
@@ -115,18 +134,15 @@ def materialize(
|
|
|
115
134
|
):
|
|
116
135
|
r"""Inject Rhiza configuration templates into a target repository.
|
|
117
136
|
|
|
118
|
-
\b
|
|
119
137
|
Materializes configuration files from the template repository specified
|
|
120
138
|
in .github/rhiza/template.yml into your project. This command:
|
|
121
139
|
|
|
122
|
-
\b
|
|
123
140
|
- Reads .github/rhiza/template.yml configuration
|
|
124
141
|
- Performs a sparse clone of the template repository
|
|
125
142
|
- Copies specified files/directories to your project
|
|
126
143
|
- Respects exclusion patterns defined in the configuration
|
|
127
144
|
- Files that already exist will NOT be overwritten unless --force is used.
|
|
128
145
|
|
|
129
|
-
\b
|
|
130
146
|
Examples:
|
|
131
147
|
rhiza materialize
|
|
132
148
|
rhiza materialize --branch develop
|
|
@@ -152,7 +168,6 @@ def validate(
|
|
|
152
168
|
Validates the .github/rhiza/template.yml file to ensure it is syntactically
|
|
153
169
|
correct and semantically valid.
|
|
154
170
|
|
|
155
|
-
\b
|
|
156
171
|
Performs comprehensive validation:
|
|
157
172
|
- Checks if template.yml exists
|
|
158
173
|
- Validates YAML syntax
|
|
@@ -164,7 +179,6 @@ def validate(
|
|
|
164
179
|
|
|
165
180
|
Returns exit code 0 on success, 1 on validation failure.
|
|
166
181
|
|
|
167
|
-
\b
|
|
168
182
|
Examples:
|
|
169
183
|
rhiza validate
|
|
170
184
|
rhiza validate /path/to/project
|
|
@@ -174,6 +188,40 @@ def validate(
|
|
|
174
188
|
raise typer.Exit(code=1)
|
|
175
189
|
|
|
176
190
|
|
|
191
|
+
@app.command()
|
|
192
|
+
def migrate(
|
|
193
|
+
target: Path = typer.Argument(
|
|
194
|
+
default=Path("."), # default to current directory
|
|
195
|
+
exists=True,
|
|
196
|
+
file_okay=False,
|
|
197
|
+
dir_okay=True,
|
|
198
|
+
help="Target git repository (defaults to current directory)",
|
|
199
|
+
),
|
|
200
|
+
):
|
|
201
|
+
r"""Migrate project to the new .rhiza folder structure.
|
|
202
|
+
|
|
203
|
+
This command helps transition projects to use the new `.rhiza/` folder
|
|
204
|
+
structure for storing Rhiza state and configuration files. It performs
|
|
205
|
+
the following migrations:
|
|
206
|
+
|
|
207
|
+
- Creates the `.rhiza/` directory in the project root
|
|
208
|
+
- Moves `.github/rhiza/template.yml` or `.github/template.yml` to `.rhiza/template.yml`
|
|
209
|
+
- Moves `.rhiza.history` to `.rhiza/history`
|
|
210
|
+
|
|
211
|
+
The new `.rhiza/` folder structure separates Rhiza's state and configuration
|
|
212
|
+
from the `.github/` directory, providing better organization.
|
|
213
|
+
|
|
214
|
+
If files already exist in `.rhiza/`, the migration will skip them and leave
|
|
215
|
+
the old files in place. You can manually remove old files after verifying
|
|
216
|
+
the migration was successful.
|
|
217
|
+
|
|
218
|
+
Examples:
|
|
219
|
+
rhiza migrate
|
|
220
|
+
rhiza migrate /path/to/project
|
|
221
|
+
"""
|
|
222
|
+
migrate_cmd(target)
|
|
223
|
+
|
|
224
|
+
|
|
177
225
|
@app.command()
|
|
178
226
|
def welcome():
|
|
179
227
|
r"""Display a friendly welcome message and explain what Rhiza is.
|
|
@@ -181,8 +229,48 @@ def welcome():
|
|
|
181
229
|
Shows a welcome message, explains Rhiza's purpose, key features,
|
|
182
230
|
and provides guidance on getting started with the tool.
|
|
183
231
|
|
|
184
|
-
\b
|
|
185
232
|
Examples:
|
|
186
233
|
rhiza welcome
|
|
187
234
|
"""
|
|
188
235
|
welcome_cmd()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command()
|
|
239
|
+
def uninstall(
|
|
240
|
+
target: Path = typer.Argument(
|
|
241
|
+
default=Path("."), # default to current directory
|
|
242
|
+
exists=True,
|
|
243
|
+
file_okay=False,
|
|
244
|
+
dir_okay=True,
|
|
245
|
+
help="Target git repository (defaults to current directory)",
|
|
246
|
+
),
|
|
247
|
+
force: bool = typer.Option(
|
|
248
|
+
False,
|
|
249
|
+
"--force",
|
|
250
|
+
"-y",
|
|
251
|
+
help="Skip confirmation prompt and proceed with deletion",
|
|
252
|
+
),
|
|
253
|
+
):
|
|
254
|
+
r"""Remove all Rhiza-managed files from the repository.
|
|
255
|
+
|
|
256
|
+
Reads the `.rhiza.history` file and removes all files that were
|
|
257
|
+
previously materialized by Rhiza templates. This provides a clean
|
|
258
|
+
way to uninstall all template-managed files from a project.
|
|
259
|
+
|
|
260
|
+
The command will:
|
|
261
|
+
- Read the list of files from `.rhiza.history`
|
|
262
|
+
- Prompt for confirmation (unless --force is used)
|
|
263
|
+
- Delete all listed files that exist
|
|
264
|
+
- Remove empty directories left behind
|
|
265
|
+
- Delete the `.rhiza.history` file itself
|
|
266
|
+
|
|
267
|
+
Use this command when you want to completely remove Rhiza templates
|
|
268
|
+
from your project.
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
rhiza uninstall
|
|
272
|
+
rhiza uninstall --force
|
|
273
|
+
rhiza uninstall /path/to/project
|
|
274
|
+
rhiza uninstall /path/to/project -y
|
|
275
|
+
"""
|
|
276
|
+
uninstall_cmd(target, force)
|
rhiza/commands/init.py
CHANGED
|
@@ -5,16 +5,47 @@ This module provides the init command that creates or validates the
|
|
|
5
5
|
and what paths are governed by Rhiza.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import importlib.resources
|
|
9
|
+
import keyword
|
|
10
|
+
import re
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
13
|
+
from jinja2 import Template
|
|
11
14
|
from loguru import logger
|
|
12
15
|
|
|
13
16
|
from rhiza.commands.validate import validate
|
|
14
17
|
from rhiza.models import RhizaTemplate
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
def
|
|
20
|
+
def _normalize_package_name(name: str) -> str:
|
|
21
|
+
"""Normalize a string into a valid Python package name.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: The input string (e.g., project name).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A valid Python identifier safe for use as a package name.
|
|
28
|
+
"""
|
|
29
|
+
# Replace any character that is not a letter, number, or underscore with an underscore
|
|
30
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
31
|
+
|
|
32
|
+
# Ensure it doesn't start with a number
|
|
33
|
+
if name[0].isdigit():
|
|
34
|
+
name = f"_{name}"
|
|
35
|
+
|
|
36
|
+
# Ensure it's not a Python keyword
|
|
37
|
+
if keyword.iskeyword(name):
|
|
38
|
+
name = f"{name}_"
|
|
39
|
+
|
|
40
|
+
return name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def init(
|
|
44
|
+
target: Path,
|
|
45
|
+
project_name: str | None = None,
|
|
46
|
+
package_name: str | None = None,
|
|
47
|
+
with_dev_dependencies: bool = False,
|
|
48
|
+
):
|
|
18
49
|
"""Initialize or validate .github/rhiza/template.yml in the target repository.
|
|
19
50
|
|
|
20
51
|
Creates a default .github/rhiza/template.yml file if it doesn't exist,
|
|
@@ -22,6 +53,9 @@ def init(target: Path):
|
|
|
22
53
|
|
|
23
54
|
Args:
|
|
24
55
|
target: Path to the target directory. Defaults to the current working directory.
|
|
56
|
+
project_name: Custom project name. Defaults to target directory name.
|
|
57
|
+
package_name: Custom package name. Defaults to normalized project name.
|
|
58
|
+
with_dev_dependencies: Include development dependencies in pyproject.toml.
|
|
25
59
|
|
|
26
60
|
Returns:
|
|
27
61
|
bool: True if validation passes, False otherwise.
|
|
@@ -31,29 +65,18 @@ def init(target: Path):
|
|
|
31
65
|
|
|
32
66
|
logger.info(f"Initializing Rhiza configuration in: {target}")
|
|
33
67
|
|
|
34
|
-
# Create .
|
|
68
|
+
# Create .rhiza directory structure if it doesn't exist
|
|
35
69
|
# This is where Rhiza stores its configuration
|
|
36
|
-
|
|
37
|
-
rhiza_dir = github_dir / "rhiza"
|
|
70
|
+
rhiza_dir = target / ".rhiza"
|
|
38
71
|
logger.debug(f"Ensuring directory exists: {rhiza_dir}")
|
|
39
72
|
rhiza_dir.mkdir(parents=True, exist_ok=True)
|
|
40
73
|
|
|
41
|
-
#
|
|
42
|
-
# TODO: This migration logic can be removed in a future version
|
|
43
|
-
# after users have had time to migrate
|
|
44
|
-
template_file = github_dir / "template.yml"
|
|
45
|
-
if template_file.exists():
|
|
46
|
-
logger.warning(f"Found template.yml in old location: {template_file}")
|
|
47
|
-
logger.info(f"Copying to new location: {rhiza_dir / 'template.yml'}")
|
|
48
|
-
# Copy the file to the new location (not move, to preserve old one temporarily)
|
|
49
|
-
shutil.copyfile(template_file, rhiza_dir / "template.yml")
|
|
50
|
-
|
|
51
|
-
# Define the template file path (new location)
|
|
74
|
+
# Define the template file path
|
|
52
75
|
template_file = rhiza_dir / "template.yml"
|
|
53
76
|
|
|
54
77
|
if not template_file.exists():
|
|
55
78
|
# Create default template.yml with sensible defaults
|
|
56
|
-
logger.info("Creating default .
|
|
79
|
+
logger.info("Creating default .rhiza/template.yml")
|
|
57
80
|
logger.debug("Using default template configuration")
|
|
58
81
|
|
|
59
82
|
# Default template points to the jebel-quant/rhiza repository
|
|
@@ -66,6 +89,7 @@ def init(target: Path):
|
|
|
66
89
|
".editorconfig", # Editor configuration
|
|
67
90
|
".gitignore", # Git ignore patterns
|
|
68
91
|
".pre-commit-config.yaml", # Pre-commit hooks
|
|
92
|
+
"ruff.toml", # Ruff linter configuration
|
|
69
93
|
"Makefile", # Build and development tasks
|
|
70
94
|
"pytest.ini", # Pytest configuration
|
|
71
95
|
"book", # Documentation book
|
|
@@ -78,20 +102,26 @@ def init(target: Path):
|
|
|
78
102
|
logger.debug(f"Writing default template to: {template_file}")
|
|
79
103
|
default_template.to_yaml(template_file)
|
|
80
104
|
|
|
81
|
-
logger.success("✓ Created .
|
|
105
|
+
logger.success("✓ Created .rhiza/template.yml")
|
|
82
106
|
logger.info("""
|
|
83
107
|
Next steps:
|
|
84
|
-
1. Review and customize .
|
|
108
|
+
1. Review and customize .rhiza/template.yml to match your project needs
|
|
85
109
|
2. Run 'rhiza materialize' to inject templates into your repository
|
|
86
110
|
""")
|
|
87
111
|
|
|
88
112
|
# Bootstrap basic Python project structure if it doesn't exist
|
|
89
113
|
# Get the name of the parent directory to use as package name
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
if project_name is None:
|
|
115
|
+
project_name = target.name
|
|
116
|
+
|
|
117
|
+
if package_name is None:
|
|
118
|
+
package_name = _normalize_package_name(project_name)
|
|
119
|
+
|
|
120
|
+
logger.debug(f"Project name: {project_name}")
|
|
121
|
+
logger.debug(f"Package name: {package_name}")
|
|
92
122
|
|
|
93
|
-
# Create src/{
|
|
94
|
-
src_folder = target / "src" /
|
|
123
|
+
# Create src/{package_name} directory structure following src-layout
|
|
124
|
+
src_folder = target / "src" / package_name
|
|
95
125
|
if not (target / "src").exists():
|
|
96
126
|
logger.info(f"Creating Python package structure: {src_folder}")
|
|
97
127
|
src_folder.mkdir(parents=True)
|
|
@@ -101,22 +131,22 @@ Next steps:
|
|
|
101
131
|
logger.debug(f"Creating {init_file}")
|
|
102
132
|
init_file.touch()
|
|
103
133
|
|
|
134
|
+
template_content = (
|
|
135
|
+
importlib.resources.files("rhiza").joinpath("_templates/basic/__init__.py.jinja2").read_text()
|
|
136
|
+
)
|
|
137
|
+
template = Template(template_content)
|
|
138
|
+
code = template.render(project_name=project_name)
|
|
139
|
+
init_file.write_text(code)
|
|
140
|
+
|
|
104
141
|
# Create main.py with a simple "Hello World" example
|
|
105
142
|
main_file = src_folder / "main.py"
|
|
106
143
|
logger.debug(f"Creating {main_file} with example code")
|
|
107
144
|
main_file.touch()
|
|
108
145
|
|
|
109
146
|
# Write example code to main.py
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def main():
|
|
115
|
-
print(say_hello("World"))
|
|
116
|
-
|
|
117
|
-
if __name__ == "__main__":
|
|
118
|
-
main()
|
|
119
|
-
"""
|
|
147
|
+
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/main.py.jinja2").read_text()
|
|
148
|
+
template = Template(template_content)
|
|
149
|
+
code = template.render(project_name=project_name)
|
|
120
150
|
main_file.write_text(code)
|
|
121
151
|
logger.success(f"Created Python package structure in {src_folder}")
|
|
122
152
|
|
|
@@ -128,15 +158,15 @@ Next steps:
|
|
|
128
158
|
pyproject_file.touch()
|
|
129
159
|
|
|
130
160
|
# Write minimal pyproject.toml content
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
template_content = (
|
|
162
|
+
importlib.resources.files("rhiza").joinpath("_templates/basic/pyproject.toml.jinja2").read_text()
|
|
163
|
+
)
|
|
164
|
+
template = Template(template_content)
|
|
165
|
+
code = template.render(
|
|
166
|
+
project_name=project_name,
|
|
167
|
+
package_name=package_name,
|
|
168
|
+
with_dev_dependencies=with_dev_dependencies,
|
|
169
|
+
)
|
|
140
170
|
pyproject_file.write_text(code)
|
|
141
171
|
logger.success("Created pyproject.toml")
|
|
142
172
|
|
rhiza/commands/materialize.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This module implements the `materialize` command. It performs a sparse
|
|
4
4
|
checkout of the configured template repository, copies the selected files
|
|
5
5
|
into the target Git repository, and records managed files in
|
|
6
|
-
`.rhiza
|
|
6
|
+
`.rhiza/history`. Use this to take a one-shot snapshot of template files.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from loguru import logger
|
|
17
17
|
|
|
18
|
-
from rhiza.commands import
|
|
18
|
+
from rhiza.commands.validate import validate
|
|
19
19
|
from rhiza.models import RhizaTemplate
|
|
20
20
|
|
|
21
21
|
|
|
@@ -54,7 +54,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
54
54
|
|
|
55
55
|
This performs a sparse checkout of the template repository and copies the
|
|
56
56
|
selected files into the target repository, recording all files under
|
|
57
|
-
template control in `.rhiza
|
|
57
|
+
template control in `.rhiza/history`.
|
|
58
58
|
|
|
59
59
|
Args:
|
|
60
60
|
target (Path): Path to the target repository.
|
|
@@ -116,11 +116,11 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
116
116
|
sys.exit(1)
|
|
117
117
|
|
|
118
118
|
# -----------------------
|
|
119
|
-
#
|
|
119
|
+
# Validate Rhiza configuration
|
|
120
120
|
# -----------------------
|
|
121
|
-
# The
|
|
121
|
+
# The validate function checks if template.yml exists and is valid
|
|
122
122
|
# Returns True if valid, False otherwise
|
|
123
|
-
valid =
|
|
123
|
+
valid = validate(target)
|
|
124
124
|
|
|
125
125
|
if not valid:
|
|
126
126
|
logger.error(f"Rhiza template is invalid in: {target}")
|
|
@@ -128,8 +128,8 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
128
128
|
sys.exit(1)
|
|
129
129
|
|
|
130
130
|
# Load the template configuration from the validated file
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
# Validation ensures the file exists at .rhiza/template.yml
|
|
132
|
+
template_file = target / ".rhiza" / "template.yml"
|
|
133
133
|
template = RhizaTemplate.from_yaml(template_file)
|
|
134
134
|
|
|
135
135
|
# Extract template configuration settings
|
|
@@ -323,13 +323,26 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
323
323
|
# -----------------------
|
|
324
324
|
# Clean up orphaned files
|
|
325
325
|
# -----------------------
|
|
326
|
-
# Read the old
|
|
326
|
+
# Read the old history file to find files that are no longer
|
|
327
327
|
# part of the current materialization and should be deleted
|
|
328
|
-
|
|
328
|
+
# Check both new and old locations for backward compatibility
|
|
329
|
+
new_history_file = target / ".rhiza" / "history"
|
|
330
|
+
old_history_file = target / ".rhiza.history"
|
|
331
|
+
|
|
332
|
+
# Prefer new location, but check old location for migration
|
|
333
|
+
if new_history_file.exists():
|
|
334
|
+
history_file = new_history_file
|
|
335
|
+
logger.debug(f"Reading existing history file from new location: {history_file.relative_to(target)}")
|
|
336
|
+
elif old_history_file.exists():
|
|
337
|
+
history_file = old_history_file
|
|
338
|
+
logger.debug(f"Reading existing history file from old location: {history_file.relative_to(target)}")
|
|
339
|
+
else:
|
|
340
|
+
history_file = new_history_file # Default to new location for creation
|
|
341
|
+
logger.debug("No existing history file found, will create new one")
|
|
342
|
+
|
|
329
343
|
previously_tracked_files: set[Path] = set()
|
|
330
344
|
|
|
331
345
|
if history_file.exists():
|
|
332
|
-
logger.debug("Reading existing .rhiza.history file")
|
|
333
346
|
with history_file.open("r", encoding="utf-8") as f:
|
|
334
347
|
for line in f:
|
|
335
348
|
line = line.strip()
|
|
@@ -337,7 +350,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
337
350
|
if line and not line.startswith("#"):
|
|
338
351
|
previously_tracked_files.add(Path(line))
|
|
339
352
|
|
|
340
|
-
logger.debug(f"Found {len(previously_tracked_files)} file(s) in previous
|
|
353
|
+
logger.debug(f"Found {len(previously_tracked_files)} file(s) in previous history")
|
|
341
354
|
|
|
342
355
|
# Convert materialized_files list to a set for comparison
|
|
343
356
|
currently_materialized_files = set(materialized_files)
|
|
@@ -361,11 +374,17 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
361
374
|
logger.debug("No orphaned files to clean up")
|
|
362
375
|
|
|
363
376
|
# -----------------------
|
|
364
|
-
# Write
|
|
377
|
+
# Write history file
|
|
365
378
|
# -----------------------
|
|
366
379
|
# This file tracks which files were materialized by Rhiza
|
|
367
380
|
# Useful for understanding which files came from the template
|
|
368
|
-
|
|
381
|
+
# Always write to new location (.rhiza/history)
|
|
382
|
+
history_file = target / ".rhiza" / "history"
|
|
383
|
+
|
|
384
|
+
# Ensure .rhiza directory exists
|
|
385
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
|
|
387
|
+
logger.debug(f"Writing history file: {history_file.relative_to(target)}")
|
|
369
388
|
with history_file.open("w", encoding="utf-8") as f:
|
|
370
389
|
f.write("# Rhiza Template History\n")
|
|
371
390
|
f.write("# This file lists all files managed by the Rhiza template.\n")
|
|
@@ -379,6 +398,15 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
379
398
|
|
|
380
399
|
logger.info(f"Updated {history_file.relative_to(target)} with {len(materialized_files)} file(s)")
|
|
381
400
|
|
|
401
|
+
# Clean up old history file if it exists (migration)
|
|
402
|
+
old_history_file = target / ".rhiza.history"
|
|
403
|
+
if old_history_file.exists() and old_history_file != history_file:
|
|
404
|
+
try:
|
|
405
|
+
old_history_file.unlink()
|
|
406
|
+
logger.debug(f"Removed old history file: {old_history_file.relative_to(target)}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.warning(f"Could not remove old history file: {e}")
|
|
409
|
+
|
|
382
410
|
logger.success("Rhiza templates materialized successfully")
|
|
383
411
|
|
|
384
412
|
logger.info(
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Command for migrating to the new .rhiza folder structure.
|
|
2
|
+
|
|
3
|
+
This module implements the `migrate` command. It helps transition projects to use
|
|
4
|
+
the new `.rhiza/` folder structure for storing Rhiza state and configuration files,
|
|
5
|
+
separate from `.github/rhiza/` which contains template configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from rhiza.models import RhizaTemplate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def migrate(target: Path) -> None:
|
|
17
|
+
"""Migrate project to use the new .rhiza folder structure.
|
|
18
|
+
|
|
19
|
+
This command performs the following actions:
|
|
20
|
+
1. Creates the `.rhiza/` directory in the project root
|
|
21
|
+
2. Moves template.yml from `.github/rhiza/` or `.github/` to `.rhiza/template.yml`
|
|
22
|
+
3. Moves `.rhiza.history` to `.rhiza/history` if it exists
|
|
23
|
+
4. Provides instructions for next steps
|
|
24
|
+
|
|
25
|
+
The `.rhiza/` folder will contain:
|
|
26
|
+
- `template.yml` - Template configuration (replaces `.github/rhiza/template.yml`)
|
|
27
|
+
- `history` - List of files managed by Rhiza templates (replaces `.rhiza.history`)
|
|
28
|
+
- Future: Additional state, cache, or metadata files
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
target (Path): Path to the target repository.
|
|
32
|
+
"""
|
|
33
|
+
# Resolve to absolute path
|
|
34
|
+
target = target.resolve()
|
|
35
|
+
|
|
36
|
+
logger.info(f"Migrating Rhiza structure in: {target}")
|
|
37
|
+
logger.info("This will create the .rhiza folder and migrate configuration files")
|
|
38
|
+
|
|
39
|
+
# Create .rhiza directory
|
|
40
|
+
rhiza_dir = target / ".rhiza"
|
|
41
|
+
if not rhiza_dir.exists():
|
|
42
|
+
logger.info(f"Creating .rhiza directory at: {rhiza_dir.relative_to(target)}")
|
|
43
|
+
rhiza_dir.mkdir(exist_ok=True)
|
|
44
|
+
logger.success(f"✓ Created {rhiza_dir.relative_to(target)}")
|
|
45
|
+
else:
|
|
46
|
+
logger.debug(f".rhiza directory already exists at: {rhiza_dir.relative_to(target)}")
|
|
47
|
+
|
|
48
|
+
# Track what was migrated for summary
|
|
49
|
+
migrations_performed = []
|
|
50
|
+
|
|
51
|
+
# Migrate template.yml from .github to .rhiza if it exists
|
|
52
|
+
github_dir = target / ".github"
|
|
53
|
+
new_template_file = rhiza_dir / "template.yml"
|
|
54
|
+
|
|
55
|
+
# Check possible locations for template.yml in .github
|
|
56
|
+
possible_template_locations = [
|
|
57
|
+
github_dir / "rhiza" / "template.yml",
|
|
58
|
+
github_dir / "template.yml",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
template_migrated = False
|
|
62
|
+
for old_template_file in possible_template_locations:
|
|
63
|
+
if old_template_file.exists():
|
|
64
|
+
if new_template_file.exists():
|
|
65
|
+
logger.info(".rhiza/template.yml already exists")
|
|
66
|
+
logger.info(f"Skipping migration of {old_template_file.relative_to(target)}")
|
|
67
|
+
logger.info(f"Note: Old file at {old_template_file.relative_to(target)} still exists")
|
|
68
|
+
else:
|
|
69
|
+
logger.info(f"Found template.yml at: {old_template_file.relative_to(target)}")
|
|
70
|
+
logger.info(f"Moving to new location: {new_template_file.relative_to(target)}")
|
|
71
|
+
|
|
72
|
+
# Move the template file to new location (not copy)
|
|
73
|
+
shutil.move(str(old_template_file), str(new_template_file))
|
|
74
|
+
logger.success("✓ Moved template.yml to .rhiza/template.yml")
|
|
75
|
+
migrations_performed.append("Moved template.yml to .rhiza/template.yml")
|
|
76
|
+
template_migrated = True
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if not template_migrated:
|
|
80
|
+
if new_template_file.exists():
|
|
81
|
+
logger.info(".rhiza/template.yml already exists (no migration needed)")
|
|
82
|
+
else:
|
|
83
|
+
logger.warning("No existing template.yml file found in .github")
|
|
84
|
+
logger.info("You may need to run 'rhiza init' to create a template configuration")
|
|
85
|
+
|
|
86
|
+
# Ensure the .rhiza folder is included in template.yml include list (if template exists)
|
|
87
|
+
template_file = new_template_file
|
|
88
|
+
if template_file.exists():
|
|
89
|
+
# Load existing template configuration
|
|
90
|
+
template = RhizaTemplate.from_yaml(template_file)
|
|
91
|
+
template_include = template.include or []
|
|
92
|
+
if ".rhiza" not in template_include:
|
|
93
|
+
logger.warning("The .rhiza folder is not included in your template.yml")
|
|
94
|
+
template_include.append(".rhiza")
|
|
95
|
+
logger.info("The .rhiza folder is added to your template.yml to ensure it's included in your repository")
|
|
96
|
+
|
|
97
|
+
# Save the updated template.yml
|
|
98
|
+
template.include = template_include
|
|
99
|
+
template.to_yaml(template_file)
|
|
100
|
+
else:
|
|
101
|
+
logger.debug("No template.yml present in .rhiza; skipping include update")
|
|
102
|
+
|
|
103
|
+
# Migrate .rhiza.history to .rhiza/history if it exists
|
|
104
|
+
old_history_file = target / ".rhiza.history"
|
|
105
|
+
new_history_file = rhiza_dir / "history"
|
|
106
|
+
|
|
107
|
+
if old_history_file.exists():
|
|
108
|
+
if new_history_file.exists():
|
|
109
|
+
logger.info(".rhiza/history already exists")
|
|
110
|
+
logger.info(f"Skipping migration of {old_history_file.relative_to(target)}")
|
|
111
|
+
logger.info(f"Note: Old file at {old_history_file.relative_to(target)} still exists")
|
|
112
|
+
else:
|
|
113
|
+
logger.info("Found existing .rhiza.history file")
|
|
114
|
+
logger.info(f"Moving to new location: {new_history_file.relative_to(target)}")
|
|
115
|
+
|
|
116
|
+
# Move the history file to new location
|
|
117
|
+
shutil.move(str(old_history_file), str(new_history_file))
|
|
118
|
+
logger.success("✓ Moved history file to .rhiza/history")
|
|
119
|
+
migrations_performed.append("Moved history tracking to .rhiza/history")
|
|
120
|
+
else:
|
|
121
|
+
if new_history_file.exists():
|
|
122
|
+
logger.debug(".rhiza/history already exists (no migration needed)")
|
|
123
|
+
else:
|
|
124
|
+
logger.debug("No existing .rhiza.history file to migrate")
|
|
125
|
+
|
|
126
|
+
# Summary
|
|
127
|
+
logger.success("✓ Migration completed successfully")
|
|
128
|
+
|
|
129
|
+
if migrations_performed:
|
|
130
|
+
logger.info("\nMigration Summary:")
|
|
131
|
+
logger.info(" - Created .rhiza/ folder")
|
|
132
|
+
for migration in migrations_performed:
|
|
133
|
+
logger.info(f" - {migration}")
|
|
134
|
+
else:
|
|
135
|
+
logger.info("\nNo files needed migration (already using .rhiza structure)")
|
|
136
|
+
|
|
137
|
+
logger.info(
|
|
138
|
+
"\nNext steps:\n"
|
|
139
|
+
" 1. Review changes:\n"
|
|
140
|
+
" git status\n"
|
|
141
|
+
" git diff\n\n"
|
|
142
|
+
" 2. Update other commands to use new .rhiza/ location\n"
|
|
143
|
+
" (Future rhiza versions will automatically use .rhiza/)\n\n"
|
|
144
|
+
" 3. Commit the migration:\n"
|
|
145
|
+
" git add .\n"
|
|
146
|
+
' git commit -m "chore: migrate to .rhiza folder structure"\n'
|
|
147
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Command for uninstalling Rhiza template files from a repository.
|
|
2
|
+
|
|
3
|
+
This module implements the `uninstall` command. It reads the `.rhiza/history`
|
|
4
|
+
file and removes all files that were previously materialized by Rhiza templates.
|
|
5
|
+
This provides a clean way to remove all template-managed files from a project.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def uninstall(target: Path, force: bool) -> None:
|
|
15
|
+
"""Uninstall Rhiza templates from the target repository.
|
|
16
|
+
|
|
17
|
+
Reads the `.rhiza/history` file and removes all files listed in it.
|
|
18
|
+
This effectively removes all files that were materialized by Rhiza.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
target (Path): Path to the target repository.
|
|
22
|
+
force (bool): If True, skip confirmation prompt and proceed with deletion.
|
|
23
|
+
"""
|
|
24
|
+
# Resolve to absolute path to avoid any ambiguity
|
|
25
|
+
target = target.resolve()
|
|
26
|
+
|
|
27
|
+
logger.info(f"Target repository: {target}")
|
|
28
|
+
|
|
29
|
+
# Check for history file in new location only
|
|
30
|
+
history_file = target / ".rhiza" / "history"
|
|
31
|
+
|
|
32
|
+
if not history_file.exists():
|
|
33
|
+
logger.warning(f"No history file found at: {history_file.relative_to(target)}")
|
|
34
|
+
logger.info("Nothing to uninstall. This repository may not have Rhiza templates materialized.")
|
|
35
|
+
logger.info("If you haven't migrated yet, run 'rhiza migrate' first.")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Read the history file
|
|
39
|
+
logger.debug(f"Reading history file: {history_file.relative_to(target)}")
|
|
40
|
+
files_to_remove: list[Path] = []
|
|
41
|
+
|
|
42
|
+
with history_file.open("r", encoding="utf-8") as f:
|
|
43
|
+
for line in f:
|
|
44
|
+
line = line.strip()
|
|
45
|
+
# Skip comments and empty lines
|
|
46
|
+
if line and not line.startswith("#"):
|
|
47
|
+
file_path = Path(line)
|
|
48
|
+
files_to_remove.append(file_path)
|
|
49
|
+
|
|
50
|
+
if not files_to_remove:
|
|
51
|
+
logger.warning("History file is empty (only contains comments)")
|
|
52
|
+
logger.info("Nothing to uninstall.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
logger.info(f"Found {len(files_to_remove)} file(s) to remove")
|
|
56
|
+
|
|
57
|
+
# Show confirmation prompt unless --force is used
|
|
58
|
+
if not force:
|
|
59
|
+
logger.warning("This will remove the following files from your repository:")
|
|
60
|
+
for file_path in sorted(files_to_remove):
|
|
61
|
+
full_path = target / file_path
|
|
62
|
+
if full_path.exists():
|
|
63
|
+
logger.warning(f" - {file_path}")
|
|
64
|
+
else:
|
|
65
|
+
logger.debug(f" - {file_path} (already deleted)")
|
|
66
|
+
|
|
67
|
+
# Prompt for confirmation
|
|
68
|
+
try:
|
|
69
|
+
response = input("\nAre you sure you want to proceed? [y/N]: ").strip().lower()
|
|
70
|
+
if response not in ("y", "yes"):
|
|
71
|
+
logger.info("Uninstall cancelled by user")
|
|
72
|
+
return
|
|
73
|
+
except (KeyboardInterrupt, EOFError):
|
|
74
|
+
logger.info("\nUninstall cancelled by user")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Remove files
|
|
78
|
+
logger.info("Removing files...")
|
|
79
|
+
removed_count = 0
|
|
80
|
+
skipped_count = 0
|
|
81
|
+
error_count = 0
|
|
82
|
+
|
|
83
|
+
for file_path in sorted(files_to_remove):
|
|
84
|
+
full_path = target / file_path
|
|
85
|
+
|
|
86
|
+
if not full_path.exists():
|
|
87
|
+
logger.debug(f"[SKIP] {file_path} (already deleted)")
|
|
88
|
+
skipped_count += 1
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
full_path.unlink()
|
|
93
|
+
logger.success(f"[DEL] {file_path}")
|
|
94
|
+
removed_count += 1
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to delete {file_path}: {e}")
|
|
97
|
+
error_count += 1
|
|
98
|
+
|
|
99
|
+
# Clean up empty directories
|
|
100
|
+
logger.debug("Cleaning up empty directories...")
|
|
101
|
+
empty_dirs_removed = 0
|
|
102
|
+
for file_path in sorted(files_to_remove, reverse=True):
|
|
103
|
+
full_path = target / file_path
|
|
104
|
+
parent = full_path.parent
|
|
105
|
+
|
|
106
|
+
# Try to remove parent directories if they're empty
|
|
107
|
+
# Walk up the directory tree
|
|
108
|
+
while parent != target and parent.exists():
|
|
109
|
+
try:
|
|
110
|
+
# Only remove if directory is empty
|
|
111
|
+
if parent.is_dir() and not any(parent.iterdir()):
|
|
112
|
+
parent.rmdir()
|
|
113
|
+
logger.debug(f"[DEL] {parent.relative_to(target)}/ (empty directory)")
|
|
114
|
+
empty_dirs_removed += 1
|
|
115
|
+
parent = parent.parent
|
|
116
|
+
else:
|
|
117
|
+
break
|
|
118
|
+
except Exception:
|
|
119
|
+
# Directory not empty or other error, stop walking up
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
# Remove history file itself
|
|
123
|
+
try:
|
|
124
|
+
history_file.unlink()
|
|
125
|
+
logger.success(f"[DEL] {history_file.relative_to(target)}")
|
|
126
|
+
removed_count += 1
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to delete {history_file.relative_to(target)}: {e}")
|
|
129
|
+
error_count += 1
|
|
130
|
+
|
|
131
|
+
# Summary
|
|
132
|
+
logger.info("\nUninstall summary:")
|
|
133
|
+
logger.info(f" Files removed: {removed_count}")
|
|
134
|
+
if skipped_count > 0:
|
|
135
|
+
logger.info(f" Files skipped (already deleted): {skipped_count}")
|
|
136
|
+
if empty_dirs_removed > 0:
|
|
137
|
+
logger.info(f" Empty directories removed: {empty_dirs_removed}")
|
|
138
|
+
if error_count > 0:
|
|
139
|
+
logger.error(f" Errors encountered: {error_count}")
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
logger.success("Rhiza templates uninstalled successfully")
|
|
143
|
+
logger.info(
|
|
144
|
+
"Next steps:\n"
|
|
145
|
+
" Review changes:\n"
|
|
146
|
+
" git status\n"
|
|
147
|
+
" git diff\n\n"
|
|
148
|
+
" Commit:\n"
|
|
149
|
+
" git add .\n"
|
|
150
|
+
' git commit -m "chore: remove rhiza templates"'
|
|
151
|
+
)
|
rhiza/commands/validate.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Command for validating Rhiza template configuration.
|
|
2
2
|
|
|
3
|
-
This module provides functionality to validate
|
|
4
|
-
|
|
3
|
+
This module provides functionality to validate template.yml files in the
|
|
4
|
+
.rhiza/template.yml location (new standard location after migration).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
@@ -14,6 +14,9 @@ def validate(target: Path) -> bool:
|
|
|
14
14
|
"""Validate template.yml configuration in the target repository.
|
|
15
15
|
|
|
16
16
|
Performs authoritative validation of the template configuration:
|
|
17
|
+
- Checks if target is a git repository
|
|
18
|
+
- Checks for standard project structure (src and tests folders)
|
|
19
|
+
- Checks for pyproject.toml (required)
|
|
17
20
|
- Checks if template.yml exists
|
|
18
21
|
- Validates YAML syntax
|
|
19
22
|
- Validates required fields
|
|
@@ -37,30 +40,51 @@ def validate(target: Path) -> bool:
|
|
|
37
40
|
|
|
38
41
|
logger.info(f"Validating template configuration in: {target}")
|
|
39
42
|
|
|
40
|
-
# Check for
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
deprecated_location = target / ".github" / "template.yml"
|
|
43
|
+
# Check for standard project structure (src and tests folders)
|
|
44
|
+
logger.debug("Validating project structure")
|
|
45
|
+
src_dir = target / "src"
|
|
46
|
+
tests_dir = target / "tests"
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if not src_dir.exists():
|
|
49
|
+
logger.warning(f"Standard 'src' folder not found: {src_dir}")
|
|
50
|
+
logger.warning("Consider creating a 'src' directory for source code")
|
|
51
|
+
else:
|
|
52
|
+
logger.success(f"'src' folder exists: {src_dir}")
|
|
49
53
|
|
|
50
|
-
if not (
|
|
51
|
-
logger.
|
|
52
|
-
logger.
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
if not tests_dir.exists():
|
|
55
|
+
logger.warning(f"Standard 'tests' folder not found: {tests_dir}")
|
|
56
|
+
logger.warning("Consider creating a 'tests' directory for test files")
|
|
57
|
+
else:
|
|
58
|
+
logger.success(f"'tests' folder exists: {tests_dir}")
|
|
59
|
+
|
|
60
|
+
# Check for pyproject.toml - this is always required
|
|
61
|
+
logger.debug("Validating pyproject.toml")
|
|
62
|
+
pyproject_file = target / "pyproject.toml"
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
logger.
|
|
59
|
-
|
|
64
|
+
if not pyproject_file.exists():
|
|
65
|
+
logger.error(f"pyproject.toml not found: {pyproject_file}")
|
|
66
|
+
logger.error("pyproject.toml is required for Python projects")
|
|
67
|
+
logger.info("Run 'rhiza init' to create a default pyproject.toml")
|
|
68
|
+
return False
|
|
60
69
|
else:
|
|
61
|
-
logger.
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
logger.success(f"pyproject.toml exists: {pyproject_file}")
|
|
71
|
+
|
|
72
|
+
# Check for template.yml in new location only
|
|
73
|
+
template_file = target / ".rhiza" / "template.yml"
|
|
74
|
+
|
|
75
|
+
if not template_file.exists():
|
|
76
|
+
logger.error(f"No template file found at: {template_file.relative_to(target)}")
|
|
77
|
+
logger.error("The template configuration must be in the .rhiza folder.")
|
|
78
|
+
logger.info("")
|
|
79
|
+
logger.info("To fix this:")
|
|
80
|
+
logger.info(" • If you're starting fresh, run: rhiza init")
|
|
81
|
+
logger.info(" • If you have an existing configuration, run: rhiza migrate")
|
|
82
|
+
logger.info("")
|
|
83
|
+
logger.info("The 'rhiza migrate' command will move your configuration from")
|
|
84
|
+
logger.info(" .github/rhiza/template.yml → .rhiza/template.yml")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
logger.success(f"Template file exists: {template_file.relative_to(target)}")
|
|
64
88
|
|
|
65
89
|
# Validate YAML syntax by attempting to parse the file
|
|
66
90
|
logger.debug(f"Parsing YAML file: {template_file}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhiza
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Reusable configuration templates for modern Python projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/jebel-quant/rhiza-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/jebel-quant/rhiza-cli
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Classifier: Topic :: Software Development :: Build Tools
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: jinja2>=3.1.0
|
|
22
23
|
Requires-Dist: loguru>=0.7.3
|
|
23
24
|
Requires-Dist: pyyaml==6.0.3
|
|
24
25
|
Requires-Dist: typer>=0.20.0
|
|
@@ -60,6 +61,7 @@ Rhiza is a CLI tool that helps you maintain consistent configuration across mult
|
|
|
60
61
|
- [Commands](#commands)
|
|
61
62
|
- [init](#rhiza-init)
|
|
62
63
|
- [materialize](#rhiza-materialize)
|
|
64
|
+
- [migrate](#rhiza-migrate)
|
|
63
65
|
- [validate](#rhiza-validate)
|
|
64
66
|
- [Configuration](#configuration)
|
|
65
67
|
- [Examples](#examples)
|
|
@@ -316,6 +318,86 @@ Re-run this script to update templates explicitly.
|
|
|
316
318
|
|
|
317
319
|
---
|
|
318
320
|
|
|
321
|
+
### `rhiza migrate`
|
|
322
|
+
|
|
323
|
+
Migrate project to the new `.rhiza` folder structure.
|
|
324
|
+
|
|
325
|
+
**Usage:**
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
rhiza migrate [OPTIONS] [TARGET]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Arguments:**
|
|
332
|
+
|
|
333
|
+
- `TARGET` - Target git repository directory (defaults to current directory)
|
|
334
|
+
|
|
335
|
+
**Arguments:**
|
|
336
|
+
|
|
337
|
+
- `TARGET` - Target git repository directory (defaults to current directory)
|
|
338
|
+
|
|
339
|
+
**Description:**
|
|
340
|
+
|
|
341
|
+
Migrates your project to use the new `.rhiza/` folder structure for storing Rhiza state and configuration files. This command helps transition from the old structure where configuration was stored in `.github/rhiza/` and `.rhiza.history` in the project root.
|
|
342
|
+
|
|
343
|
+
The migration performs the following actions:
|
|
344
|
+
|
|
345
|
+
1. Creates the `.rhiza/` directory in the project root
|
|
346
|
+
2. Moves `template.yml` from `.github/rhiza/` or `.github/` to `.rhiza/template.yml`
|
|
347
|
+
3. Moves `.rhiza.history` to `.rhiza/history`
|
|
348
|
+
4. Provides instructions for next steps
|
|
349
|
+
|
|
350
|
+
The new `.rhiza/` folder structure provides better organization by separating Rhiza's state and configuration from the `.github/` directory.
|
|
351
|
+
|
|
352
|
+
**Examples:**
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Migrate current directory
|
|
356
|
+
rhiza migrate
|
|
357
|
+
|
|
358
|
+
# Migrate a specific directory
|
|
359
|
+
rhiza migrate /path/to/project
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Output:**
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
[INFO] Migrating Rhiza structure in: /path/to/project
|
|
366
|
+
[INFO] This will create the .rhiza folder and migrate configuration files
|
|
367
|
+
[INFO] Creating .rhiza directory at: .rhiza
|
|
368
|
+
✓ Created .rhiza
|
|
369
|
+
[INFO] Found template.yml at: .github/rhiza/template.yml
|
|
370
|
+
[INFO] Moving to new location: .rhiza/template.yml
|
|
371
|
+
✓ Moved template.yml to .rhiza/template.yml
|
|
372
|
+
✓ Migration completed successfully
|
|
373
|
+
|
|
374
|
+
Migration Summary:
|
|
375
|
+
- Created .rhiza/ folder
|
|
376
|
+
- Moved template.yml to .rhiza/template.yml
|
|
377
|
+
- Moved history tracking to .rhiza/history
|
|
378
|
+
|
|
379
|
+
Next steps:
|
|
380
|
+
1. Review changes:
|
|
381
|
+
git status
|
|
382
|
+
git diff
|
|
383
|
+
|
|
384
|
+
2. Update other commands to use new .rhiza/ location
|
|
385
|
+
(Future rhiza versions will automatically use .rhiza/)
|
|
386
|
+
|
|
387
|
+
3. Commit the migration:
|
|
388
|
+
git add .
|
|
389
|
+
git commit -m "chore: migrate to .rhiza folder structure"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Notes:**
|
|
393
|
+
|
|
394
|
+
- If files already exist in `.rhiza/`, the migration will skip them and leave the old files in place
|
|
395
|
+
- You can manually remove old files after verifying the migration was successful
|
|
396
|
+
- The old `.rhiza.history` file is removed after successful migration to `.rhiza/history`
|
|
397
|
+
- The original template file in `.github/` is moved (removed from old location)
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
319
401
|
### `rhiza validate`
|
|
320
402
|
|
|
321
403
|
Validate Rhiza template configuration.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
rhiza/__init__.py,sha256=iW3niLBjwRKxcMhIV_1eb78putjUTo2tbZsadofluJk,1939
|
|
2
|
+
rhiza/__main__.py,sha256=Lx0GqVZo6ymm0f18_uYB6E7_SOWwJNYjb73Vr31oLoM,236
|
|
3
|
+
rhiza/cli.py,sha256=I5A5d1-3xrL2gdh5H9Itm9uiQjoPiGEbHYyxXddHEOk,8196
|
|
4
|
+
rhiza/models.py,sha256=fW9lofkkid-bghk2bXEgBdGbZ4scSqG726fMrVfKX_M,3454
|
|
5
|
+
rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
|
|
6
|
+
rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
|
|
7
|
+
rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
|
|
8
|
+
rhiza/commands/__init__.py,sha256=Z5CeMh7ylX27H6dvwqRbEKzYo5pwQq-5TyTxABUSaQg,1848
|
|
9
|
+
rhiza/commands/init.py,sha256=RsYmomOq-00b4bAjseiHR7ljgJQ_XSjDX-ZWKYTPPN8,6787
|
|
10
|
+
rhiza/commands/materialize.py,sha256=AbVXJrR8faa3t7m_Xl5TR8VE3ODmkh2oiAwCYFA36wA,17343
|
|
11
|
+
rhiza/commands/migrate.py,sha256=hhSkj2iafCCxKrrVOfnPWDjK7fTJ5ReAJsWNDGz71s0,6307
|
|
12
|
+
rhiza/commands/uninstall.py,sha256=z95xqamV7wGPr8PBveWzaRmtD5bPhOrTLI0GcOvpnAo,5371
|
|
13
|
+
rhiza/commands/validate.py,sha256=1HMQWF9Syv7JKC31AkaPYMTnqy8HOvAMxMLrYty36FQ,9112
|
|
14
|
+
rhiza/commands/welcome.py,sha256=w3BziR042o6oYincd3EqDsFzF6qqInU7iYhWjF3yJqY,2382
|
|
15
|
+
rhiza-0.8.1.dist-info/METADATA,sha256=hLiWkbsUmak3dR65CSnQokItVSl9X9aYBXFPef0s088,25156
|
|
16
|
+
rhiza-0.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
rhiza-0.8.1.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
|
|
18
|
+
rhiza-0.8.1.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
19
|
+
rhiza-0.8.1.dist-info/RECORD,,
|
rhiza-0.7.1.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
rhiza/__init__.py,sha256=iW3niLBjwRKxcMhIV_1eb78putjUTo2tbZsadofluJk,1939
|
|
2
|
-
rhiza/__main__.py,sha256=Lx0GqVZo6ymm0f18_uYB6E7_SOWwJNYjb73Vr31oLoM,236
|
|
3
|
-
rhiza/cli.py,sha256=faCIOKDzEDRvL4doLZhiIAyHUUGESGrwWLtjLjimCUY,5111
|
|
4
|
-
rhiza/models.py,sha256=fW9lofkkid-bghk2bXEgBdGbZ4scSqG726fMrVfKX_M,3454
|
|
5
|
-
rhiza/commands/__init__.py,sha256=Z5CeMh7ylX27H6dvwqRbEKzYo5pwQq-5TyTxABUSaQg,1848
|
|
6
|
-
rhiza/commands/init.py,sha256=3dAQmFYPVIdRXyJsv0-28RLpPcRh-0urMyd-3IxHnGw,5725
|
|
7
|
-
rhiza/commands/materialize.py,sha256=jmXH9Fb3lkktxgdWtZ2cQk0wyURleWzHvQN6n_DNZ7U,16049
|
|
8
|
-
rhiza/commands/validate.py,sha256=cxStfXbY_ifsc_yRDCg0TOnv8jG05hxE9rteta-X9hQ,8093
|
|
9
|
-
rhiza/commands/welcome.py,sha256=w3BziR042o6oYincd3EqDsFzF6qqInU7iYhWjF3yJqY,2382
|
|
10
|
-
rhiza-0.7.1.dist-info/METADATA,sha256=NUz6Ye9SW_JPrqjGmqSLBPJO8eDHvkE_Z4mtCw70Scc,22742
|
|
11
|
-
rhiza-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
-
rhiza-0.7.1.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
|
|
13
|
-
rhiza-0.7.1.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
14
|
-
rhiza-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|