rhiza 0.8.3__py3-none-any.whl → 0.8.5__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/__main__.py +22 -0
- rhiza/commands/init.py +213 -162
- rhiza/commands/materialize.py +328 -267
- rhiza/commands/migrate.py +99 -46
- rhiza/commands/uninstall.py +136 -57
- rhiza/commands/validate.py +182 -71
- rhiza/subprocess_utils.py +26 -0
- {rhiza-0.8.3.dist-info → rhiza-0.8.5.dist-info}/METADATA +8 -10
- rhiza-0.8.5.dist-info/RECORD +20 -0
- rhiza-0.8.3.dist-info/RECORD +0 -19
- {rhiza-0.8.3.dist-info → rhiza-0.8.5.dist-info}/WHEEL +0 -0
- {rhiza-0.8.3.dist-info → rhiza-0.8.5.dist-info}/entry_points.txt +0 -0
- {rhiza-0.8.3.dist-info → rhiza-0.8.5.dist-info}/licenses/LICENSE +0 -0
rhiza/commands/migrate.py
CHANGED
|
@@ -13,30 +13,15 @@ from loguru import logger
|
|
|
13
13
|
from rhiza.models import RhizaTemplate
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
"""
|
|
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
|
|
16
|
+
def _create_rhiza_directory(target: Path) -> Path:
|
|
17
|
+
"""Create .rhiza directory if it doesn't exist.
|
|
29
18
|
|
|
30
19
|
Args:
|
|
31
|
-
target
|
|
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")
|
|
20
|
+
target: Target repository path.
|
|
38
21
|
|
|
39
|
-
|
|
22
|
+
Returns:
|
|
23
|
+
Path to .rhiza directory.
|
|
24
|
+
"""
|
|
40
25
|
rhiza_dir = target / ".rhiza"
|
|
41
26
|
if not rhiza_dir.exists():
|
|
42
27
|
logger.info(f"Creating .rhiza directory at: {rhiza_dir.relative_to(target)}")
|
|
@@ -44,21 +29,30 @@ def migrate(target: Path) -> None:
|
|
|
44
29
|
logger.success(f"✓ Created {rhiza_dir.relative_to(target)}")
|
|
45
30
|
else:
|
|
46
31
|
logger.debug(f".rhiza directory already exists at: {rhiza_dir.relative_to(target)}")
|
|
32
|
+
return rhiza_dir
|
|
47
33
|
|
|
48
|
-
# Track what was migrated for summary
|
|
49
|
-
migrations_performed = []
|
|
50
34
|
|
|
51
|
-
|
|
35
|
+
def _migrate_template_file(target: Path, rhiza_dir: Path) -> tuple[bool, list[str]]:
|
|
36
|
+
"""Migrate template.yml from .github to .rhiza.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
target: Target repository path.
|
|
40
|
+
rhiza_dir: Path to .rhiza directory.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple of (migration_performed, migrations_list).
|
|
44
|
+
"""
|
|
52
45
|
github_dir = target / ".github"
|
|
53
46
|
new_template_file = rhiza_dir / "template.yml"
|
|
54
47
|
|
|
55
|
-
# Check possible locations for template.yml in .github
|
|
56
48
|
possible_template_locations = [
|
|
57
49
|
github_dir / "rhiza" / "template.yml",
|
|
58
50
|
github_dir / "template.yml",
|
|
59
51
|
]
|
|
60
52
|
|
|
53
|
+
migrations_performed = []
|
|
61
54
|
template_migrated = False
|
|
55
|
+
|
|
62
56
|
for old_template_file in possible_template_locations:
|
|
63
57
|
if old_template_file.exists():
|
|
64
58
|
if new_template_file.exists():
|
|
@@ -68,8 +62,6 @@ def migrate(target: Path) -> None:
|
|
|
68
62
|
else:
|
|
69
63
|
logger.info(f"Found template.yml at: {old_template_file.relative_to(target)}")
|
|
70
64
|
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
65
|
shutil.move(str(old_template_file), str(new_template_file))
|
|
74
66
|
logger.success("✓ Moved template.yml to .rhiza/template.yml")
|
|
75
67
|
migrations_performed.append("Moved template.yml to .rhiza/template.yml")
|
|
@@ -83,26 +75,42 @@ def migrate(target: Path) -> None:
|
|
|
83
75
|
logger.warning("No existing template.yml file found in .github")
|
|
84
76
|
logger.info("You may need to run 'rhiza init' to create a template configuration")
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# Save the updated template.yml
|
|
98
|
-
template.include = template_include
|
|
99
|
-
template.to_yaml(template_file)
|
|
100
|
-
else:
|
|
78
|
+
return template_migrated or new_template_file.exists(), migrations_performed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ensure_rhiza_in_include(template_file: Path) -> None:
|
|
82
|
+
"""Ensure .rhiza folder is in template.yml include list.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
template_file: Path to template.yml file.
|
|
86
|
+
"""
|
|
87
|
+
if not template_file.exists():
|
|
101
88
|
logger.debug("No template.yml present in .rhiza; skipping include update")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
template = RhizaTemplate.from_yaml(template_file)
|
|
92
|
+
template_include = template.include or []
|
|
93
|
+
if ".rhiza" not in template_include:
|
|
94
|
+
logger.warning("The .rhiza folder is not included in your template.yml")
|
|
95
|
+
template_include.append(".rhiza")
|
|
96
|
+
logger.info("The .rhiza folder is added to your template.yml to ensure it's included in your repository")
|
|
97
|
+
template.include = template_include
|
|
98
|
+
template.to_yaml(template_file)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _migrate_history_file(target: Path, rhiza_dir: Path) -> list[str]:
|
|
102
|
+
"""Migrate .rhiza.history to .rhiza/history.
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
Args:
|
|
105
|
+
target: Target repository path.
|
|
106
|
+
rhiza_dir: Path to .rhiza directory.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of migrations performed.
|
|
110
|
+
"""
|
|
104
111
|
old_history_file = target / ".rhiza.history"
|
|
105
112
|
new_history_file = rhiza_dir / "history"
|
|
113
|
+
migrations_performed = []
|
|
106
114
|
|
|
107
115
|
if old_history_file.exists():
|
|
108
116
|
if new_history_file.exists():
|
|
@@ -112,8 +120,6 @@ def migrate(target: Path) -> None:
|
|
|
112
120
|
else:
|
|
113
121
|
logger.info("Found existing .rhiza.history file")
|
|
114
122
|
logger.info(f"Moving to new location: {new_history_file.relative_to(target)}")
|
|
115
|
-
|
|
116
|
-
# Move the history file to new location
|
|
117
123
|
shutil.move(str(old_history_file), str(new_history_file))
|
|
118
124
|
logger.success("✓ Moved history file to .rhiza/history")
|
|
119
125
|
migrations_performed.append("Moved history tracking to .rhiza/history")
|
|
@@ -123,7 +129,15 @@ def migrate(target: Path) -> None:
|
|
|
123
129
|
else:
|
|
124
130
|
logger.debug("No existing .rhiza.history file to migrate")
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
return migrations_performed
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print_migration_summary(migrations_performed: list[str]) -> None:
|
|
136
|
+
"""Print migration summary.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
migrations_performed: List of migrations performed.
|
|
140
|
+
"""
|
|
127
141
|
logger.success("✓ Migration completed successfully")
|
|
128
142
|
|
|
129
143
|
if migrations_performed:
|
|
@@ -145,3 +159,42 @@ def migrate(target: Path) -> None:
|
|
|
145
159
|
" git add .\n"
|
|
146
160
|
' git commit -m "chore: migrate to .rhiza folder structure"\n'
|
|
147
161
|
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def migrate(target: Path) -> None:
|
|
165
|
+
"""Migrate project to use the new .rhiza folder structure.
|
|
166
|
+
|
|
167
|
+
This command performs the following actions:
|
|
168
|
+
1. Creates the `.rhiza/` directory in the project root
|
|
169
|
+
2. Moves template.yml from `.github/rhiza/` or `.github/` to `.rhiza/template.yml`
|
|
170
|
+
3. Moves `.rhiza.history` to `.rhiza/history` if it exists
|
|
171
|
+
4. Provides instructions for next steps
|
|
172
|
+
|
|
173
|
+
The `.rhiza/` folder will contain:
|
|
174
|
+
- `template.yml` - Template configuration (replaces `.github/rhiza/template.yml`)
|
|
175
|
+
- `history` - List of files managed by Rhiza templates (replaces `.rhiza.history`)
|
|
176
|
+
- Future: Additional state, cache, or metadata files
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
target (Path): Path to the target repository.
|
|
180
|
+
"""
|
|
181
|
+
target = target.resolve()
|
|
182
|
+
logger.info(f"Migrating Rhiza structure in: {target}")
|
|
183
|
+
logger.info("This will create the .rhiza folder and migrate configuration files")
|
|
184
|
+
|
|
185
|
+
# Create .rhiza directory
|
|
186
|
+
rhiza_dir = _create_rhiza_directory(target)
|
|
187
|
+
|
|
188
|
+
# Migrate template file
|
|
189
|
+
template_exists, template_migrations = _migrate_template_file(target, rhiza_dir)
|
|
190
|
+
|
|
191
|
+
# Ensure .rhiza is in include list
|
|
192
|
+
if template_exists:
|
|
193
|
+
_ensure_rhiza_in_include(rhiza_dir / "template.yml")
|
|
194
|
+
|
|
195
|
+
# Migrate history file
|
|
196
|
+
history_migrations = _migrate_history_file(target, rhiza_dir)
|
|
197
|
+
|
|
198
|
+
# Print summary
|
|
199
|
+
all_migrations = template_migrations + history_migrations
|
|
200
|
+
_print_migration_summary(all_migrations)
|
rhiza/commands/uninstall.py
CHANGED
|
@@ -11,70 +11,69 @@ from pathlib import Path
|
|
|
11
11
|
from loguru import logger
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
"""
|
|
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.
|
|
14
|
+
def _read_history_file(history_file: Path, target: Path) -> list[Path]:
|
|
15
|
+
"""Read history file and return list of files to remove.
|
|
19
16
|
|
|
20
17
|
Args:
|
|
21
|
-
|
|
22
|
-
|
|
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"
|
|
18
|
+
history_file: Path to history file.
|
|
19
|
+
target: Target repository path.
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
logger.info("If you haven't migrated yet, run 'rhiza migrate' first.")
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
# Read the history file
|
|
21
|
+
Returns:
|
|
22
|
+
List of file paths to remove.
|
|
23
|
+
"""
|
|
39
24
|
logger.debug(f"Reading history file: {history_file.relative_to(target)}")
|
|
40
25
|
files_to_remove: list[Path] = []
|
|
41
26
|
|
|
42
27
|
with history_file.open("r", encoding="utf-8") as f:
|
|
43
28
|
for line in f:
|
|
44
29
|
line = line.strip()
|
|
45
|
-
# Skip comments and empty lines
|
|
46
30
|
if line and not line.startswith("#"):
|
|
47
31
|
file_path = Path(line)
|
|
48
32
|
files_to_remove.append(file_path)
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
logger.warning("History file is empty (only contains comments)")
|
|
52
|
-
logger.info("Nothing to uninstall.")
|
|
53
|
-
return
|
|
34
|
+
return files_to_remove
|
|
54
35
|
|
|
55
|
-
logger.info(f"Found {len(files_to_remove)} file(s) to remove")
|
|
56
36
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
37
|
+
def _confirm_uninstall(files_to_remove: list[Path], target: Path) -> bool:
|
|
38
|
+
"""Show confirmation prompt and get user response.
|
|
76
39
|
|
|
77
|
-
|
|
40
|
+
Args:
|
|
41
|
+
files_to_remove: List of files to remove.
|
|
42
|
+
target: Target repository path.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if user confirmed, False otherwise.
|
|
46
|
+
"""
|
|
47
|
+
logger.warning("This will remove the following files from your repository:")
|
|
48
|
+
for file_path in sorted(files_to_remove):
|
|
49
|
+
full_path = target / file_path
|
|
50
|
+
if full_path.exists():
|
|
51
|
+
logger.warning(f" - {file_path}")
|
|
52
|
+
else:
|
|
53
|
+
logger.debug(f" - {file_path} (already deleted)")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
response = input("\nAre you sure you want to proceed? [y/N]: ").strip().lower()
|
|
57
|
+
if response not in ("y", "yes"):
|
|
58
|
+
logger.info("Uninstall cancelled by user")
|
|
59
|
+
return False
|
|
60
|
+
except (KeyboardInterrupt, EOFError):
|
|
61
|
+
logger.info("\nUninstall cancelled by user")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _remove_files(files_to_remove: list[Path], target: Path) -> tuple[int, int, int]:
|
|
68
|
+
"""Remove files from repository.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
files_to_remove: List of files to remove.
|
|
72
|
+
target: Target repository path.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple of (removed_count, skipped_count, error_count).
|
|
76
|
+
"""
|
|
78
77
|
logger.info("Removing files...")
|
|
79
78
|
removed_count = 0
|
|
80
79
|
skipped_count = 0
|
|
@@ -96,18 +95,28 @@ def uninstall(target: Path, force: bool) -> None:
|
|
|
96
95
|
logger.error(f"Failed to delete {file_path}: {e}")
|
|
97
96
|
error_count += 1
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
return removed_count, skipped_count, error_count
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cleanup_empty_directories(files_to_remove: list[Path], target: Path) -> int:
|
|
102
|
+
"""Clean up empty directories after file removal.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
files_to_remove: List of files that were removed.
|
|
106
|
+
target: Target repository path.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Number of empty directories removed.
|
|
110
|
+
"""
|
|
100
111
|
logger.debug("Cleaning up empty directories...")
|
|
101
112
|
empty_dirs_removed = 0
|
|
113
|
+
|
|
102
114
|
for file_path in sorted(files_to_remove, reverse=True):
|
|
103
115
|
full_path = target / file_path
|
|
104
116
|
parent = full_path.parent
|
|
105
117
|
|
|
106
|
-
# Try to remove parent directories if they're empty
|
|
107
|
-
# Walk up the directory tree
|
|
108
118
|
while parent != target and parent.exists():
|
|
109
119
|
try:
|
|
110
|
-
# Only remove if directory is empty
|
|
111
120
|
if parent.is_dir() and not any(parent.iterdir()):
|
|
112
121
|
parent.rmdir()
|
|
113
122
|
logger.debug(f"[DEL] {parent.relative_to(target)}/ (empty directory)")
|
|
@@ -116,19 +125,39 @@ def uninstall(target: Path, force: bool) -> None:
|
|
|
116
125
|
else:
|
|
117
126
|
break
|
|
118
127
|
except Exception:
|
|
119
|
-
# Directory not empty or other error, stop walking up
|
|
120
128
|
break
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
return empty_dirs_removed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _remove_history_file(history_file: Path, target: Path) -> tuple[int, int]:
|
|
134
|
+
"""Remove the history file itself.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
history_file: Path to history file.
|
|
138
|
+
target: Target repository path.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Tuple of (removed_count, error_count).
|
|
142
|
+
"""
|
|
123
143
|
try:
|
|
124
144
|
history_file.unlink()
|
|
125
145
|
logger.success(f"[DEL] {history_file.relative_to(target)}")
|
|
126
|
-
|
|
146
|
+
return 1, 0
|
|
127
147
|
except Exception as e:
|
|
128
148
|
logger.error(f"Failed to delete {history_file.relative_to(target)}: {e}")
|
|
129
|
-
|
|
149
|
+
return 0, 1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _print_summary(removed_count: int, skipped_count: int, empty_dirs_removed: int, error_count: int) -> None:
|
|
153
|
+
"""Print uninstall summary.
|
|
130
154
|
|
|
131
|
-
|
|
155
|
+
Args:
|
|
156
|
+
removed_count: Number of files removed.
|
|
157
|
+
skipped_count: Number of files skipped.
|
|
158
|
+
empty_dirs_removed: Number of empty directories removed.
|
|
159
|
+
error_count: Number of errors encountered.
|
|
160
|
+
"""
|
|
132
161
|
logger.info("\nUninstall summary:")
|
|
133
162
|
logger.info(f" Files removed: {removed_count}")
|
|
134
163
|
if skipped_count > 0:
|
|
@@ -139,6 +168,56 @@ def uninstall(target: Path, force: bool) -> None:
|
|
|
139
168
|
logger.error(f" Errors encountered: {error_count}")
|
|
140
169
|
sys.exit(1)
|
|
141
170
|
|
|
171
|
+
|
|
172
|
+
def uninstall(target: Path, force: bool) -> None:
|
|
173
|
+
"""Uninstall Rhiza templates from the target repository.
|
|
174
|
+
|
|
175
|
+
Reads the `.rhiza/history` file and removes all files listed in it.
|
|
176
|
+
This effectively removes all files that were materialized by Rhiza.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
target (Path): Path to the target repository.
|
|
180
|
+
force (bool): If True, skip confirmation prompt and proceed with deletion.
|
|
181
|
+
"""
|
|
182
|
+
target = target.resolve()
|
|
183
|
+
logger.info(f"Target repository: {target}")
|
|
184
|
+
|
|
185
|
+
# Check for history file
|
|
186
|
+
history_file = target / ".rhiza" / "history"
|
|
187
|
+
if not history_file.exists():
|
|
188
|
+
logger.warning(f"No history file found at: {history_file.relative_to(target)}")
|
|
189
|
+
logger.info("Nothing to uninstall. This repository may not have Rhiza templates materialized.")
|
|
190
|
+
logger.info("If you haven't migrated yet, run 'rhiza migrate' first.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Read history file
|
|
194
|
+
files_to_remove = _read_history_file(history_file, target)
|
|
195
|
+
if not files_to_remove:
|
|
196
|
+
logger.warning("History file is empty (only contains comments)")
|
|
197
|
+
logger.info("Nothing to uninstall.")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
logger.info(f"Found {len(files_to_remove)} file(s) to remove")
|
|
201
|
+
|
|
202
|
+
# Confirm uninstall unless force is used
|
|
203
|
+
if not force:
|
|
204
|
+
if not _confirm_uninstall(files_to_remove, target):
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# Remove files
|
|
208
|
+
removed_count, skipped_count, error_count = _remove_files(files_to_remove, target)
|
|
209
|
+
|
|
210
|
+
# Clean up empty directories
|
|
211
|
+
empty_dirs_removed = _cleanup_empty_directories(files_to_remove, target)
|
|
212
|
+
|
|
213
|
+
# Remove history file
|
|
214
|
+
history_removed, history_error = _remove_history_file(history_file, target)
|
|
215
|
+
removed_count += history_removed
|
|
216
|
+
error_count += history_error
|
|
217
|
+
|
|
218
|
+
# Print summary
|
|
219
|
+
_print_summary(removed_count, skipped_count, empty_dirs_removed, error_count)
|
|
220
|
+
|
|
142
221
|
logger.success("Rhiza templates uninstalled successfully")
|
|
143
222
|
logger.info(
|
|
144
223
|
"Next steps:\n"
|