env-module-manager 0.1.2__tar.gz → 0.2.0__tar.gz

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.
Files changed (37) hide show
  1. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/PKG-INFO +25 -5
  2. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/README.md +23 -3
  3. env_module_manager-0.2.0/docs/environment-modules.md +57 -0
  4. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/index.md +3 -1
  5. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/python-tools.md +4 -0
  6. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/reference.md +35 -1
  7. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/rust-tools.md +4 -0
  8. env_module_manager-0.2.0/docs/shell-tools.md +29 -0
  9. env_module_manager-0.2.0/docs/uninstall.md +42 -0
  10. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/mkdocs.yml +3 -1
  11. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/module_manager/cli.py +129 -7
  12. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/module_manager/deploy.py +117 -2
  13. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/module_manager/modulefile.py +15 -0
  14. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/pyproject.toml +2 -2
  15. env_module_manager-0.2.0/tests/test_cli.py +286 -0
  16. env_module_manager-0.2.0/tests/test_modulefile.py +249 -0
  17. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/uv.lock +1 -1
  18. env_module_manager-0.1.2/docs/environment-modules.md +0 -26
  19. env_module_manager-0.1.2/tests/test_cli.py +0 -148
  20. env_module_manager-0.1.2/tests/test_modulefile.py +0 -116
  21. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.github/workflows/publish.yml +0 -0
  22. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.github/workflows/tag.yml +0 -0
  23. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.github/workflows/test.yml +0 -0
  24. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.gitignore +0 -0
  25. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.python-version +0 -0
  26. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/.readthedocs.yaml +0 -0
  27. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/AGENTS.md +0 -0
  28. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/CONTRIBUTING.md +0 -0
  29. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/LICENSE +0 -0
  30. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/configuration.md +0 -0
  31. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/development.md +0 -0
  32. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/installation.md +0 -0
  33. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/docs/stylesheets/extra.css +0 -0
  34. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/example.bash +0 -0
  35. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/module_manager/__init__.py +0 -0
  36. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/module_manager/config.py +0 -0
  37. {env_module_manager-0.1.2 → env_module_manager-0.2.0}/tests/test_config.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: env-module-manager
3
- Version: 0.1.2
4
- Summary: Deploy Python and Rust CLI tools behind GNU environment modulefiles
3
+ Version: 0.2.0
4
+ Summary: Deploy Python, Rust, and shell CLI tools behind GNU environment modulefiles
5
5
  Project-URL: Homepage, https://github.com/ejb90/module-manager
6
6
  Project-URL: Documentation, https://module-manager.readthedocs.io/
7
7
  Project-URL: Tests, https://app.codecov.io/gh/ejb90/module-manager
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
  [![PyPI](https://img.shields.io/pypi/v/env-module-manager)](https://pypi.org/project/env-module-manager/)
41
41
  [![Python versions](https://img.shields.io/pypi/pyversions/env-module-manager)](https://pypi.org/project/env-module-manager/)
42
42
 
43
- `module-manager` deploys versioned Python and Rust CLI tools behind GNU
43
+ `module-manager` deploys versioned Python, Rust, and shell CLI tools behind GNU
44
44
  environment modulefiles for Linux/HPC systems.
45
45
 
46
46
  It writes tools and modulefiles into predictable versioned locations:
@@ -71,14 +71,34 @@ module-manager deploy-rust ripgrep 14.1.1 \
71
71
  --module-root /prod/modulefiles
72
72
  ```
73
73
 
74
+ Deploy a shell script:
75
+
76
+ ```sh
77
+ module-manager deploy-script my-tool 1.0.0 \
78
+ --script ./scripts/my-tool \
79
+ --prefix /prod/tools \
80
+ --module-root /prod/modulefiles
81
+ ```
82
+
74
83
  Use the generated module:
75
84
 
76
85
  ```sh
77
86
  module use /prod/modulefiles
78
- module load ruff/0.8.0
87
+ module load ruff
79
88
  ruff --version
80
89
  ```
81
90
 
91
+ Deployments make the deployed version the module default. Add `--no-default`
92
+ when you want to write the versioned modulefile without changing the default.
93
+
94
+ Remove a deployed version:
95
+
96
+ ```sh
97
+ module-manager uninstall ruff 0.8.0 \
98
+ --prefix /prod/tools \
99
+ --module-root /prod/modulefiles
100
+ ```
101
+
82
102
  ## Documentation
83
103
 
84
104
  Full documentation lives in `docs/` and can be served with MkDocs:
@@ -88,4 +108,4 @@ uv run mkdocs serve
88
108
  ```
89
109
 
90
110
  Start with `docs/index.md` for installation, configuration, Python tools, Rust
91
- tools, and local development notes.
111
+ tools, shell scripts, uninstall, and local development notes.
@@ -8,7 +8,7 @@
8
8
  [![PyPI](https://img.shields.io/pypi/v/env-module-manager)](https://pypi.org/project/env-module-manager/)
9
9
  [![Python versions](https://img.shields.io/pypi/pyversions/env-module-manager)](https://pypi.org/project/env-module-manager/)
10
10
 
11
- `module-manager` deploys versioned Python and Rust CLI tools behind GNU
11
+ `module-manager` deploys versioned Python, Rust, and shell CLI tools behind GNU
12
12
  environment modulefiles for Linux/HPC systems.
13
13
 
14
14
  It writes tools and modulefiles into predictable versioned locations:
@@ -39,14 +39,34 @@ module-manager deploy-rust ripgrep 14.1.1 \
39
39
  --module-root /prod/modulefiles
40
40
  ```
41
41
 
42
+ Deploy a shell script:
43
+
44
+ ```sh
45
+ module-manager deploy-script my-tool 1.0.0 \
46
+ --script ./scripts/my-tool \
47
+ --prefix /prod/tools \
48
+ --module-root /prod/modulefiles
49
+ ```
50
+
42
51
  Use the generated module:
43
52
 
44
53
  ```sh
45
54
  module use /prod/modulefiles
46
- module load ruff/0.8.0
55
+ module load ruff
47
56
  ruff --version
48
57
  ```
49
58
 
59
+ Deployments make the deployed version the module default. Add `--no-default`
60
+ when you want to write the versioned modulefile without changing the default.
61
+
62
+ Remove a deployed version:
63
+
64
+ ```sh
65
+ module-manager uninstall ruff 0.8.0 \
66
+ --prefix /prod/tools \
67
+ --module-root /prod/modulefiles
68
+ ```
69
+
50
70
  ## Documentation
51
71
 
52
72
  Full documentation lives in `docs/` and can be served with MkDocs:
@@ -56,4 +76,4 @@ uv run mkdocs serve
56
76
  ```
57
77
 
58
78
  Start with `docs/index.md` for installation, configuration, Python tools, Rust
59
- tools, and local development notes.
79
+ tools, shell scripts, uninstall, and local development notes.
@@ -0,0 +1,57 @@
1
+ # Environment Modules
2
+
3
+ Generated modulefiles are Tcl modulefiles for Environment Modules.
4
+
5
+ Use a generated module tree:
6
+
7
+ ```sh
8
+ module use /prod/modulefiles
9
+ module load ruff
10
+ ruff --version
11
+ ```
12
+
13
+ You can also load an explicit version:
14
+
15
+ ```sh
16
+ module load ruff/0.8.0
17
+ ruff --version
18
+ ```
19
+
20
+ ## Generated Modulefile Behavior
21
+
22
+ Each modulefile:
23
+
24
+ - prepends the tool's versioned `bin` directory to `PATH`
25
+ - sets `<TOOL>_ROOT`
26
+ - includes `module-whatis`
27
+ - includes install guidance in `module help`
28
+
29
+ For `ruff/0.8.0`, the root variable is:
30
+
31
+ ```sh
32
+ RUFF_ROOT=/prod/tools/ruff/0.8.0
33
+ ```
34
+
35
+ ## Default Versions
36
+
37
+ Deploy commands write a default-version selector by default:
38
+
39
+ ```text
40
+ /prod/modulefiles/<name>/.version
41
+ ```
42
+
43
+ For `ruff/0.8.0`, this makes `module load ruff` resolve to `ruff/0.8.0`.
44
+
45
+ Use `--no-default` when a deployment should not update the default version:
46
+
47
+ ```sh
48
+ module-manager deploy-python ruff 0.8.0 \
49
+ --package 'ruff==0.8.0' \
50
+ --prefix /prod/tools \
51
+ --module-root /prod/modulefiles \
52
+ --no-default
53
+ ```
54
+
55
+ Uninstall removes the default selector only when it still points at the version
56
+ being removed. If another deployment has already made a newer version the
57
+ default, that selector is left alone.
@@ -1,6 +1,6 @@
1
1
  # module-manager
2
2
 
3
- `module-manager` deploys versioned Python and Rust CLI tools behind GNU
3
+ `module-manager` deploys versioned Python, Rust, and shell CLI tools behind GNU
4
4
  environment modulefiles for Linux/HPC systems.
5
5
 
6
6
  It is designed for administrators who want reproducible command-line tool
@@ -47,3 +47,5 @@ ruff --version
47
47
  - Set shared defaults in [Configuration](configuration.md).
48
48
  - Deploy Python packages with [Python Tools](python-tools.md).
49
49
  - Deploy Rust binaries with [Rust Tools](rust-tools.md).
50
+ - Deploy shell scripts with [Shell Scripts](shell-tools.md).
51
+ - Remove deployed versions with [Uninstall](uninstall.md).
@@ -21,6 +21,10 @@ Without `--execute-install`, the command creates the versioned directories and
21
21
  modulefile only. The module help includes the `uv tool install` command that an
22
22
  administrator can run later.
23
23
 
24
+ Deployments also make the deployed version the module default, so `module load
25
+ ruff` resolves to `ruff/0.8.0`. Add `--no-default` to leave the current default
26
+ unchanged.
27
+
24
28
  ## Deploy from a Private Index
25
29
 
26
30
  ```sh
@@ -18,6 +18,18 @@ Show Rust deployment options:
18
18
  module-manager deploy-rust --help
19
19
  ```
20
20
 
21
+ Show shell script deployment options:
22
+
23
+ ```sh
24
+ module-manager deploy-script --help
25
+ ```
26
+
27
+ Show uninstall options:
28
+
29
+ ```sh
30
+ module-manager uninstall --help
31
+ ```
32
+
21
33
  ## Python Command
22
34
 
23
35
  ```sh
@@ -28,6 +40,7 @@ module-manager deploy-python NAME VERSION \
28
40
  [--find-links PATH_OR_URL] \
29
41
  [--prefix PATH] \
30
42
  [--module-root PATH] \
43
+ [--no-default] \
31
44
  [--execute-install]
32
45
  ```
33
46
 
@@ -37,5 +50,26 @@ module-manager deploy-python NAME VERSION \
37
50
  module-manager deploy-rust NAME VERSION \
38
51
  [--binary PATH] \
39
52
  [--prefix PATH] \
40
- [--module-root PATH]
53
+ [--module-root PATH] \
54
+ [--no-default]
55
+ ```
56
+
57
+ ## Shell Script Command
58
+
59
+ ```sh
60
+ module-manager deploy-script NAME VERSION \
61
+ [--script PATH] \
62
+ [--prefix PATH] \
63
+ [--module-root PATH] \
64
+ [--no-default]
65
+ ```
66
+
67
+ ## Uninstall Command
68
+
69
+ ```sh
70
+ module-manager uninstall NAME VERSION \
71
+ [--prefix PATH] \
72
+ [--module-root PATH] \
73
+ [--keep-default] \
74
+ [--dry-run]
41
75
  ```
@@ -22,3 +22,7 @@ and creates:
22
22
  ```
23
23
 
24
24
  The generated modulefile prepends the deployed `bin` directory to `PATH`.
25
+
26
+ Deployments make the deployed version the module default, so `module load
27
+ ripgrep` resolves to `ripgrep/14.1.1`. Add `--no-default` to leave the current
28
+ default unchanged.
@@ -0,0 +1,29 @@
1
+ # Shell Scripts
2
+
3
+ Shell scripts can be copied directly into the versioned prefix.
4
+
5
+ ```sh
6
+ module-manager deploy-script my-tool 1.0.0 \
7
+ --script ./scripts/my-tool \
8
+ --prefix /prod/tools \
9
+ --module-root /prod/modulefiles
10
+ ```
11
+
12
+ This writes the script to:
13
+
14
+ ```text
15
+ /prod/tools/my-tool/1.0.0/bin/my-tool
16
+ ```
17
+
18
+ and creates:
19
+
20
+ ```text
21
+ /prod/modulefiles/my-tool/1.0.0
22
+ ```
23
+
24
+ The deployed script is marked executable, and the generated modulefile prepends
25
+ the deployed `bin` directory to `PATH`.
26
+
27
+ Deployments make the deployed version the module default, so `module load
28
+ my-tool` resolves to `my-tool/1.0.0`. Add `--no-default` to leave the current
29
+ default unchanged.
@@ -0,0 +1,42 @@
1
+ # Uninstall
2
+
3
+ Remove one deployed tool version with the `uninstall` command:
4
+
5
+ ```sh
6
+ module-manager uninstall ruff 0.8.0 \
7
+ --prefix /prod/tools \
8
+ --module-root /prod/modulefiles
9
+ ```
10
+
11
+ This removes:
12
+
13
+ ```text
14
+ /prod/tools/ruff/0.8.0
15
+ /prod/modulefiles/ruff/0.8.0
16
+ ```
17
+
18
+ If the default selector points at the same version, it is removed too:
19
+
20
+ ```text
21
+ /prod/modulefiles/ruff/.version
22
+ ```
23
+
24
+ If the default selector points at a different version, it is left in place.
25
+
26
+ Preview an uninstall without deleting files:
27
+
28
+ ```sh
29
+ module-manager uninstall ruff 0.8.0 \
30
+ --prefix /prod/tools \
31
+ --module-root /prod/modulefiles \
32
+ --dry-run
33
+ ```
34
+
35
+ Leave the default selector untouched even when it points at the removed version:
36
+
37
+ ```sh
38
+ module-manager uninstall ruff 0.8.0 \
39
+ --prefix /prod/tools \
40
+ --module-root /prod/modulefiles \
41
+ --keep-default
42
+ ```
@@ -1,5 +1,5 @@
1
1
  site_name: module-manager
2
- site_description: Deploy Python and Rust CLI tools behind GNU environment modulefiles.
2
+ site_description: Deploy Python, Rust, and shell CLI tools behind GNU environment modulefiles.
3
3
  repo_url: https://github.com/ellis/module-manager
4
4
 
5
5
  theme:
@@ -45,6 +45,8 @@ nav:
45
45
  - Configuration: configuration.md
46
46
  - Python Tools: python-tools.md
47
47
  - Rust Tools: rust-tools.md
48
+ - Shell Scripts: shell-tools.md
49
+ - Uninstall: uninstall.md
48
50
  - Environment Modules: environment-modules.md
49
51
  - Local Development: development.md
50
52
  - CLI Reference: reference.md
@@ -10,7 +10,7 @@ from click import Command
10
10
 
11
11
  from . import __version__
12
12
  from .config import AppConfig, load_config
13
- from .deploy import deploy_python_tool, deploy_rust_tool
13
+ from .deploy import UninstallResult, deploy_python_tool, deploy_rust_tool, deploy_script_tool, uninstall_tool
14
14
 
15
15
  click.rich_click.TEXT_MARKUP = "rich"
16
16
  click.rich_click.STYLE_COMMAND = "bold cyan"
@@ -29,7 +29,14 @@ ClickCommand = TypeVar("ClickCommand", bound=Command)
29
29
 
30
30
 
31
31
  def common_options(command: ClickCommand) -> ClickCommand:
32
- """Attach options shared by Python and Rust deployment commands."""
32
+ """Attach options shared by deployment commands."""
33
+ command = click.option(
34
+ "--default/--no-default",
35
+ "make_default",
36
+ default=True,
37
+ show_default=True,
38
+ help="Write the module default selector for this version.",
39
+ )(command)
33
40
  command = click.option(
34
41
  "--homepage",
35
42
  help="Optional upstream homepage shown in module help.",
@@ -53,6 +60,23 @@ def common_options(command: ClickCommand) -> ClickCommand:
53
60
  return command
54
61
 
55
62
 
63
+ def location_options(command: ClickCommand) -> ClickCommand:
64
+ """Attach options for commands that need deployment roots."""
65
+ command = click.option(
66
+ "--prefix",
67
+ type=PATH,
68
+ help="Root installation prefix, for example /prod/tools.",
69
+ )(command)
70
+ command = click.option(
71
+ "--module-root",
72
+ type=PATH,
73
+ help="Root of the module tree, for example /prod/modulefiles.",
74
+ )(command)
75
+ command = click.argument("version")(command)
76
+ command = click.argument("name")(command)
77
+ return command
78
+
79
+
56
80
  @click.group(context_settings={"help_option_names": ["-h", "--help"]})
57
81
  @click.version_option(__version__)
58
82
  @click.option(
@@ -66,8 +90,8 @@ def main(ctx: click.Context, config: Path | None) -> None:
66
90
  """[bold]Deploy CLI tools behind GNU environment modulefiles.[/bold].
67
91
 
68
92
  Build versioned modulefiles for Python tools installed with
69
- [cyan]uv tool install[/cyan] and Rust binaries copied into a production
70
- prefix.
93
+ [cyan]uv tool install[/cyan], Rust binaries, and shell scripts copied into
94
+ a production prefix.
71
95
  """
72
96
  ctx.obj = load_config(config)
73
97
 
@@ -102,6 +126,7 @@ def deploy_python(
102
126
  prefix: Path | None,
103
127
  description: str | None,
104
128
  homepage: str | None,
129
+ make_default: bool,
105
130
  package: str,
106
131
  python: str | None,
107
132
  indexes: tuple[str, ...],
@@ -123,8 +148,11 @@ def deploy_python(
123
148
  indexes=indexes or config.indexes,
124
149
  find_links=find_links or config.find_links,
125
150
  execute_install=execute_install,
151
+ make_default=make_default,
152
+ )
153
+ print_result(
154
+ paths.modulefile, paths.install_root, paths.bin_dir, paths.default_version_file if make_default else None
126
155
  )
127
- print_result(paths.modulefile, paths.install_root, paths.bin_dir)
128
156
 
129
157
 
130
158
  @main.command("deploy-rust")
@@ -143,6 +171,7 @@ def deploy_rust(
143
171
  prefix: Path | None,
144
172
  description: str | None,
145
173
  homepage: str | None,
174
+ make_default: bool,
146
175
  binary: Path | None,
147
176
  ) -> None:
148
177
  """Copy a Rust CLI binary and write a modulefile for it."""
@@ -156,8 +185,84 @@ def deploy_rust(
156
185
  binary=binary.expanduser() if binary else None,
157
186
  description=description,
158
187
  homepage=homepage,
188
+ make_default=make_default,
189
+ )
190
+ print_result(
191
+ paths.modulefile, paths.install_root, paths.bin_dir, paths.default_version_file if make_default else None
159
192
  )
160
- print_result(paths.modulefile, paths.install_root, paths.bin_dir)
193
+
194
+
195
+ @main.command("deploy-script")
196
+ @common_options
197
+ @click.option(
198
+ "--script",
199
+ type=PATH,
200
+ help="Shell script to copy into the versioned prefix.",
201
+ )
202
+ @click.pass_obj
203
+ def deploy_script(
204
+ config: AppConfig,
205
+ name: str,
206
+ version: str,
207
+ module_root: Path | None,
208
+ prefix: Path | None,
209
+ description: str | None,
210
+ homepage: str | None,
211
+ make_default: bool,
212
+ script: Path | None,
213
+ ) -> None:
214
+ """Copy a shell script and write a modulefile for it."""
215
+ resolved_module_root = require_path(module_root or config.module_root, "module root", "--module-root")
216
+ resolved_prefix = require_path(prefix or config.prefix, "install prefix", "--prefix")
217
+ paths = deploy_script_tool(
218
+ name=name,
219
+ version=version,
220
+ module_root=resolved_module_root,
221
+ prefix=resolved_prefix,
222
+ script=script.expanduser() if script else None,
223
+ description=description,
224
+ homepage=homepage,
225
+ make_default=make_default,
226
+ )
227
+ print_result(
228
+ paths.modulefile, paths.install_root, paths.bin_dir, paths.default_version_file if make_default else None
229
+ )
230
+
231
+
232
+ @main.command("uninstall")
233
+ @location_options
234
+ @click.option(
235
+ "--keep-default",
236
+ is_flag=True,
237
+ help="Leave the default selector in place, even if it points at this version.",
238
+ )
239
+ @click.option(
240
+ "--dry-run",
241
+ is_flag=True,
242
+ help="Print paths that would be removed without deleting them.",
243
+ )
244
+ @click.pass_obj
245
+ def uninstall(
246
+ config: AppConfig,
247
+ name: str,
248
+ version: str,
249
+ module_root: Path | None,
250
+ prefix: Path | None,
251
+ keep_default: bool,
252
+ dry_run: bool,
253
+ ) -> None:
254
+ """Remove a deployed tool version and its modulefile."""
255
+ resolved_module_root = require_path(module_root or config.module_root, "module root", "--module-root")
256
+ resolved_prefix = require_path(prefix or config.prefix, "install prefix", "--prefix")
257
+ result = uninstall_tool(
258
+ name=name,
259
+ version=version,
260
+ module_root=resolved_module_root,
261
+ prefix=resolved_prefix,
262
+ remove_default=not keep_default,
263
+ dry_run=dry_run,
264
+ )
265
+ print_uninstall_result(result, dry_run)
161
266
 
162
267
 
163
268
  def require_path(value: Path | None, label: str, option: str) -> Path:
@@ -171,8 +276,25 @@ def require_path(value: Path | None, label: str, option: str) -> Path:
171
276
  return value.expanduser()
172
277
 
173
278
 
174
- def print_result(modulefile: Path, install_root: Path, bin_dir: Path) -> None:
279
+ def print_result(
280
+ modulefile: Path,
281
+ install_root: Path,
282
+ bin_dir: Path,
283
+ default_version_file: Path | None = None,
284
+ ) -> None:
175
285
  """Print the paths produced by a deployment command."""
176
286
  click.echo(f"modulefile: {modulefile}")
177
287
  click.echo(f"install root: {install_root}")
178
288
  click.echo(f"bin dir: {bin_dir}")
289
+ if default_version_file:
290
+ click.echo(f"default version: {default_version_file}")
291
+
292
+
293
+ def print_uninstall_result(result: UninstallResult, dry_run: bool = False) -> None:
294
+ """Print the paths removed by an uninstall command."""
295
+ action = "would remove" if dry_run else "removed"
296
+ if not result.removed:
297
+ click.echo("nothing to remove")
298
+ return
299
+ for path in result.removed:
300
+ click.echo(f"{action}: {path}")