cave-cli 3.5.5__tar.gz → 3.6.0b1__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 (51) hide show
  1. {cave_cli-3.5.5/cave_cli.egg-info → cave_cli-3.6.0b1}/PKG-INFO +29 -16
  2. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/README.md +19 -15
  3. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/cli.py +10 -1
  4. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/doctor.py +16 -12
  5. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/list_cmd.py +7 -2
  6. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/list_versions.py +3 -6
  7. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/run.py +7 -3
  8. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/sync_cmd.py +8 -7
  9. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/uninstall.py +7 -10
  10. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/update.py +15 -17
  11. cave_cli-3.6.0b1/cave_cli/commands/upgrade.py +246 -0
  12. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/version.py +21 -15
  13. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/display.py +30 -0
  14. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/logger.py +2 -0
  15. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/subprocess.py +1 -21
  16. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/validate.py +45 -6
  17. cave_cli-3.6.0b1/cave_cli/utils/version.py +133 -0
  18. {cave_cli-3.5.5 → cave_cli-3.6.0b1/cave_cli.egg-info}/PKG-INFO +29 -16
  19. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli.egg-info/SOURCES.txt +8 -2
  20. cave_cli-3.6.0b1/cave_cli.egg-info/requires.txt +10 -0
  21. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/pyproject.toml +16 -1
  22. cave_cli-3.6.0b1/setup.cfg +4 -0
  23. cave_cli-3.6.0b1/test/test_env.py +113 -0
  24. cave_cli-3.6.0b1/test/test_net.py +33 -0
  25. cave_cli-3.6.0b1/test/test_sync.py +42 -0
  26. cave_cli-3.6.0b1/test/test_validate.py +49 -0
  27. cave_cli-3.6.0b1/utils/prettify.py +25 -0
  28. cave_cli-3.5.5/cave_cli/commands/upgrade.py +0 -75
  29. cave_cli-3.5.5/setup.cfg +0 -10
  30. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/LICENSE +0 -0
  31. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/NOTICE.md +0 -0
  32. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/__init__.py +0 -0
  33. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/__init__.py +0 -0
  34. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/create.py +0 -0
  35. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/kill.py +0 -0
  36. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/prettify.py +0 -0
  37. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/purge.py +0 -0
  38. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/reset.py +0 -0
  39. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/test.py +0 -0
  40. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/commands/theme.py +0 -0
  41. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/__init__.py +0 -0
  42. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/cache.py +0 -0
  43. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/constants.py +0 -0
  44. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/docker.py +0 -0
  45. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/env.py +0 -0
  46. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/git.py +0 -0
  47. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/net.py +0 -0
  48. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli/utils/sync.py +0 -0
  49. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli.egg-info/dependency_links.txt +0 -0
  50. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli.egg-info/entry_points.txt +0 -0
  51. {cave_cli-3.5.5 → cave_cli-3.6.0b1}/cave_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cave_cli
3
- Version: 3.5.5
3
+ Version: 3.6.0b1
4
4
  Summary: CLI for creating and managing Docker-based CAVE web applications.
5
5
  Author-email: MIT-CAVE <cave-contact@mit.edu>
6
6
  License: Apache-2.0
@@ -13,6 +13,15 @@ Requires-Python: >=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  License-File: NOTICE.md
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
18
+ Requires-Dist: autoflake>=2.3.1; extra == "dev"
19
+ Requires-Dist: black>=25.1.0; extra == "dev"
20
+ Requires-Dist: build>=1.4.0; extra == "dev"
21
+ Requires-Dist: nox>=2026.4.10; extra == "dev"
22
+ Requires-Dist: nox-uv>=0.8.0; extra == "dev"
23
+ Requires-Dist: pdoc>=15.0.1; extra == "dev"
24
+ Requires-Dist: twine>=6.2.0; extra == "dev"
16
25
  Dynamic: license-file
17
26
 
18
27
  # CAVE CLI
@@ -56,38 +65,42 @@ docker run hello-world
56
65
 
57
66
  ## Installation
58
67
 
59
- Install with [pipx](https://pipx.pypa.io/) (recommended keeps the CLI isolated from your system Python):
68
+ Install with [uv](https://docs.astral.sh/uv/) (required to run cave update and cave uninstall):
60
69
 
61
70
  ```sh
62
- pipx install cave_cli
71
+ uv tool install cave_cli
63
72
  ```
64
73
 
65
- If you don't have pipx, install it first:
74
+ If you don't have uv, install it first:
66
75
 
67
76
  <details>
68
77
  <summary>macOS</summary>
69
78
 
70
79
  ```sh
71
80
  # With Homebrew (recommended)
72
- brew install pipx
73
- pipx ensurepath
81
+ brew install uv
74
82
 
75
- # Or with pip
76
- pip3 install --user pipx
77
- pipx ensurepath
83
+ # Or with the official installer
84
+ curl -LsSf https://astral.sh/uv/install.sh | sh
78
85
  ```
79
86
 
80
87
  </details>
81
88
  <details>
82
- <summary>Other Linux / Windows</summary>
89
+ <summary>Linux</summary>
83
90
 
84
91
  ```sh
85
- python3 -m pip install --user pipx
86
- pipx ensurepath
92
+ curl -LsSf https://astral.sh/uv/install.sh | sh
93
+ ```
94
+ </details>
95
+ <details>
96
+ <summary>Windows</summary>
97
+
98
+ ```powershell
99
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
87
100
  ```
88
101
  </details>
89
102
 
90
- For more options see the [pipx installation guide](https://pipx.pypa.io/stable/installation/).
103
+ For more options see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
91
104
 
92
105
  Verify the installation and check your environment health:
93
106
 
@@ -116,7 +129,7 @@ cave --help
116
129
  |---|---|
117
130
  | `cave create <name>` | Create a new CAVE app from the template repository |
118
131
  | `cave run` | Build Docker image and run the app with a live TUI dashboard |
119
- | `cave doctor` | Check the health of your Docker, Git, and Pipx environment |
132
+ | `cave doctor` | Check the health of your Docker, Git, and uv environment |
120
133
 
121
134
  ### Peripheral Commands
122
135
 
@@ -163,10 +176,10 @@ cave --help
163
176
  cave update
164
177
  ```
165
178
 
166
- Or directly via pipx:
179
+ Or directly via uv:
167
180
 
168
181
  ```sh
169
- pipx upgrade cave_cli
182
+ uv tool upgrade cave_cli
170
183
  ```
171
184
 
172
185
  ## License Notice
@@ -39,38 +39,42 @@ docker run hello-world
39
39
 
40
40
  ## Installation
41
41
 
42
- Install with [pipx](https://pipx.pypa.io/) (recommended keeps the CLI isolated from your system Python):
42
+ Install with [uv](https://docs.astral.sh/uv/) (required to run cave update and cave uninstall):
43
43
 
44
44
  ```sh
45
- pipx install cave_cli
45
+ uv tool install cave_cli
46
46
  ```
47
47
 
48
- If you don't have pipx, install it first:
48
+ If you don't have uv, install it first:
49
49
 
50
50
  <details>
51
51
  <summary>macOS</summary>
52
52
 
53
53
  ```sh
54
54
  # With Homebrew (recommended)
55
- brew install pipx
56
- pipx ensurepath
55
+ brew install uv
57
56
 
58
- # Or with pip
59
- pip3 install --user pipx
60
- pipx ensurepath
57
+ # Or with the official installer
58
+ curl -LsSf https://astral.sh/uv/install.sh | sh
61
59
  ```
62
60
 
63
61
  </details>
64
62
  <details>
65
- <summary>Other Linux / Windows</summary>
63
+ <summary>Linux</summary>
66
64
 
67
65
  ```sh
68
- python3 -m pip install --user pipx
69
- pipx ensurepath
66
+ curl -LsSf https://astral.sh/uv/install.sh | sh
67
+ ```
68
+ </details>
69
+ <details>
70
+ <summary>Windows</summary>
71
+
72
+ ```powershell
73
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
70
74
  ```
71
75
  </details>
72
76
 
73
- For more options see the [pipx installation guide](https://pipx.pypa.io/stable/installation/).
77
+ For more options see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
74
78
 
75
79
  Verify the installation and check your environment health:
76
80
 
@@ -99,7 +103,7 @@ cave --help
99
103
  |---|---|
100
104
  | `cave create <name>` | Create a new CAVE app from the template repository |
101
105
  | `cave run` | Build Docker image and run the app with a live TUI dashboard |
102
- | `cave doctor` | Check the health of your Docker, Git, and Pipx environment |
106
+ | `cave doctor` | Check the health of your Docker, Git, and uv environment |
103
107
 
104
108
  ### Peripheral Commands
105
109
 
@@ -146,10 +150,10 @@ cave --help
146
150
  cave update
147
151
  ```
148
152
 
149
- Or directly via pipx:
153
+ Or directly via uv:
150
154
 
151
155
  ```sh
152
- pipx upgrade cave_cli
156
+ uv tool upgrade cave_cli
153
157
  ```
154
158
 
155
159
  ## License Notice
@@ -38,6 +38,13 @@ def add_global_args(parser: argparse.ArgumentParser) -> None:
38
38
  default=False,
39
39
  help="Enable verbose logging output (shorthand for --loglevel DEBUG)",
40
40
  )
41
+ parser.add_argument(
42
+ "-q",
43
+ "--quiet",
44
+ action="store_true",
45
+ default=False,
46
+ help="Enable quiet mode (suppress most output, shorthand for --loglevel SILENT)",
47
+ )
41
48
  parser.add_argument(
42
49
  "--loglevel",
43
50
  "--ll",
@@ -419,7 +426,9 @@ def main():
419
426
  # Configure logging
420
427
  from cave_cli.utils.logger import logger
421
428
 
422
- if args.verbose:
429
+ if getattr(args, "quiet", False):
430
+ logger.set_level("SILENT")
431
+ elif args.verbose:
423
432
  logger.set_level("DEBUG")
424
433
  else:
425
434
  logger.set_level(args.loglevel)
@@ -34,31 +34,35 @@ def check_git() -> tuple[bool, str]:
34
34
  )
35
35
 
36
36
 
37
- def check_pipx() -> tuple[bool, str]:
37
+ def check_uv(return_path: bool = True) -> tuple[bool, str, str | None]:
38
38
  """
39
39
  Usage:
40
40
 
41
- - Validates that pipx is installed
41
+ - Validates that uv is installed
42
42
 
43
43
  Returns:
44
44
 
45
45
  - ``success``: bool
46
46
  - ``message``: str remediation or version
47
+ - ``path``: str|None path to uv executable if found
47
48
  """
48
49
  import shutil
49
50
 
50
- pipx = shutil.which("pipx")
51
- if pipx:
51
+ uv_path = shutil.which("uv")
52
+ if uv_path:
52
53
  try:
53
- result = run([pipx, "--version"])
54
+ result = run([uv_path, "--version"])
54
55
  if result.returncode == 0:
55
- return True, f"pipx version {result.stdout.strip()}"
56
+ output = True, f"uv version {result.stdout.strip()}"
57
+ if return_path:
58
+ return output + (uv_path,)
59
+ return output
56
60
  except Exception:
57
61
  pass
58
- return (
59
- False,
60
- "pipx is not installed. Please install pipx: https://pipx.pypa.io/",
61
- )
62
+ output = False, "uv is not installed. Please install uv: https://github.com/astral-sh/uv"
63
+ if return_path:
64
+ return output + (None,)
65
+ return output
62
66
 
63
67
 
64
68
  def check_docker() -> tuple[bool, str]:
@@ -126,7 +130,7 @@ def check_all(exit_on_fail: bool = True) -> dict[str, tuple[bool, str]]:
126
130
  results = {
127
131
  "Docker": check_docker(),
128
132
  "Git": check_git(),
129
- "Pipx": check_pipx(),
133
+ "uv": check_uv(return_path=False),
130
134
  }
131
135
 
132
136
  if exit_on_fail:
@@ -145,7 +149,7 @@ def doctor(args: argparse.Namespace) -> None:
145
149
  """
146
150
  Usage:
147
151
 
148
- - Checks the health of the CAVE environment (Docker, Git, Pipx)
152
+ - Checks the health of the CAVE environment (Docker, Git, uv)
149
153
  """
150
154
  print_section("CAVE Doctor")
151
155
 
@@ -20,9 +20,11 @@ def list_cmd(args: argparse.Namespace) -> None:
20
20
 
21
21
  if show_all:
22
22
  print_section("CAVE App Containers (All)")
23
+ from cave_cli.utils.display import _QUIET
23
24
  for suffix in ("_django", "_db_host", "_redis_host", "_nginx_host"):
24
25
  for name in get_all_containers(suffix):
25
- print(f" ● {name}")
26
+ if not _QUIET:
27
+ print(f" ● {name}")
26
28
  else:
27
29
  apps = get_running_apps()
28
30
  if not apps:
@@ -40,4 +42,7 @@ def list_cmd(args: argparse.Namespace) -> None:
40
42
  else:
41
43
  port = get_container_host_port(f"{app_name}_django")
42
44
  print_key_value(app_name, f"http://localhost:{port}")
43
- print("")
45
+
46
+ from cave_cli.utils.display import _QUIET
47
+ if not _QUIET:
48
+ print("")
@@ -14,14 +14,11 @@ from cave_cli.utils.display import (
14
14
  )
15
15
  from cave_cli.utils.git import ls_remote_tags
16
16
  from cave_cli.utils.logger import logger
17
+ from cave_cli.utils.version import version_tuple
17
18
 
18
19
  RECENT_LIMIT = 5
19
20
 
20
21
 
21
- def _version_sort_key(v: str) -> list[int]:
22
- return [int(x) for x in re.findall(r"\d+", v)]
23
-
24
-
25
22
  def list_versions(args: argparse.Namespace) -> None:
26
23
  """
27
24
  Usage:
@@ -54,7 +51,7 @@ def list_versions(args: argparse.Namespace) -> None:
54
51
  v for v in set().union(*repo_tags.values())
55
52
  if not pattern or fnmatch.fnmatch(v, pattern)
56
53
  ),
57
- key=_version_sort_key,
54
+ key=version_tuple,
58
55
  reverse=True,
59
56
  )
60
57
 
@@ -64,7 +61,7 @@ def list_versions(args: argparse.Namespace) -> None:
64
61
 
65
62
  by_major: dict[int, list[str]] = defaultdict(list)
66
63
  for v in all_versions:
67
- major = _version_sort_key(v)[0]
64
+ major = version_tuple(v)[0]
68
65
  by_major[major].append(v)
69
66
 
70
67
  repos = list(VALID_REPOS)
@@ -56,6 +56,7 @@ def run_cave(
56
56
  ip_port_arg = getattr(args, "ip_port", None)
57
57
  command_args = getattr(args, "command_args", []) or []
58
58
  extra_env = getattr(args, "extra_env", {}) or {}
59
+ quiet = getattr(args, "quiet", False) or getattr(args, "loglevel", "INFO").upper() == "SILENT"
59
60
 
60
61
  is_server_run = entrypoint == "./utils/run_server.sh" and not interactive
61
62
  use_tui = is_server_run and not show_all
@@ -224,7 +225,7 @@ def run_cave(
224
225
  url = f"https://{ip}:{port}"
225
226
 
226
227
  if interactive:
227
- logger.header("CAVE App: (Interactive)")
228
+ logger.header("CAVE App (Interactive)")
228
229
  run_interactive(
229
230
  name=django_container,
230
231
  image=f"cave-app:{app_name}",
@@ -236,8 +237,8 @@ def run_cave(
236
237
  command=server_command,
237
238
  )
238
239
  remove_containers(app_name)
239
-
240
240
  elif use_tui:
241
+ logger.header("CAVE App (TUI)")
241
242
  _run_tui(
242
243
  app_name=app_name,
243
244
  django_container=django_container,
@@ -252,6 +253,7 @@ def run_cave(
252
253
  )
253
254
 
254
255
  else:
256
+ logger.header("CAVE App (CLI)")
255
257
  if is_server_run:
256
258
  logger.info(
257
259
  f"Your Cave App can be accessed from Chrome at:\n{url}"
@@ -273,7 +275,7 @@ def run_cave(
273
275
  url = f"http://localhost:{port}"
274
276
 
275
277
  if interactive:
276
- logger.header("CAVE App: (Interactive)")
278
+ logger.header("CAVE App (Interactive)")
277
279
  run_interactive(
278
280
  name=django_container,
279
281
  image=f"cave-app:{app_name}",
@@ -287,6 +289,7 @@ def run_cave(
287
289
  remove_containers(app_name)
288
290
 
289
291
  elif use_tui:
292
+ logger.header("CAVE App (TUI)")
290
293
  _run_tui(
291
294
  app_name=app_name,
292
295
  django_container=django_container,
@@ -301,6 +304,7 @@ def run_cave(
301
304
  )
302
305
 
303
306
  else:
307
+ logger.header("CAVE App (CLI)")
304
308
  if is_server_run:
305
309
  logger.info(
306
310
  f"Your Cave App can be accessed from Chrome at:\n{url}"
@@ -12,7 +12,7 @@ from cave_cli.utils.sync import sync_files
12
12
  from cave_cli.utils.validate import confirm_action, find_app_dir
13
13
 
14
14
 
15
- def sync_cmd(args: argparse.Namespace) -> None:
15
+ def sync_cmd(args: argparse.Namespace, do_reset: bool=True) -> None:
16
16
  """
17
17
  Usage:
18
18
 
@@ -69,11 +69,12 @@ def sync_cmd(args: argparse.Namespace) -> None:
69
69
 
70
70
  shutil.rmtree(temp_dir, ignore_errors=True)
71
71
 
72
- reset_args = argparse.Namespace(
73
- yes=True,
74
- verbose=getattr(args, "verbose", False),
75
- loglevel=getattr(args, "loglevel", "INFO"),
76
- )
77
- reset(reset_args)
72
+ if do_reset:
73
+ reset_args = argparse.Namespace(
74
+ yes=True,
75
+ verbose=getattr(args, "verbose", False),
76
+ loglevel=getattr(args, "loglevel", "INFO"),
77
+ )
78
+ reset(reset_args)
78
79
 
79
80
  logger.success("Sync complete.")
@@ -5,15 +5,14 @@ import sys
5
5
 
6
6
  from cave_cli.utils.display import RED, RESET, step_done, step_start
7
7
  from cave_cli.utils.logger import logger
8
-
9
- PIPX_DOCS_URL = "https://pipx.pypa.io/stable/installation/"
8
+ from cave_cli.commands.doctor import check_uv
10
9
 
11
10
 
12
11
  def uninstall(args: argparse.Namespace) -> None:
13
12
  """
14
13
  Usage:
15
14
 
16
- - Removes the CAVE CLI package via pipx.
15
+ - Removes the CAVE CLI package via uv.
17
16
  """
18
17
  try:
19
18
  response = input(
@@ -28,17 +27,15 @@ def uninstall(args: argparse.Namespace) -> None:
28
27
  logger.error("Uninstall canceled")
29
28
  return
30
29
 
31
- pipx = shutil.which("pipx")
32
- if not pipx:
33
- logger.error(
34
- "pipx not found. Please uninstall cave_cli manually."
35
- )
36
- logger.error(f"See: {PIPX_DOCS_URL}")
30
+ has_uv, uv_message, uv_path = check_uv()
31
+ if not has_uv:
32
+ logger.error(uv_message)
33
+ logger.error(f"You may need to uninstall CAVE CLI manually based on how it was installed.")
37
34
  sys.exit(1)
38
35
 
39
36
  step_start("Removing installation")
40
37
  result = subprocess.run(
41
- [pipx, "uninstall", "cave_cli"],
38
+ [uv_path, "tool", "uninstall", "cave_cli"],
42
39
  stdout=subprocess.PIPE,
43
40
  stderr=subprocess.PIPE,
44
41
  text=True,
@@ -1,13 +1,13 @@
1
1
  import argparse
2
- import shutil
3
2
  import subprocess
4
3
  import sys
5
4
  import tempfile
6
5
 
7
6
  from cave_cli.utils.display import step_done, step_start
8
7
  from cave_cli.utils.logger import logger
8
+ from cave_cli.commands.doctor import check_uv
9
9
 
10
- PIPX_DOCS_URL = "https://pipx.pypa.io/stable/installation/"
10
+ UV_DOCS_URL = "https://docs.astral.sh/uv/"
11
11
  CLI_REPO_URL = "https://github.com/MIT-CAVE/cave_cli.git"
12
12
 
13
13
 
@@ -15,32 +15,30 @@ def update(args: argparse.Namespace) -> None:
15
15
  """
16
16
  Usage:
17
17
 
18
- - Updates the CAVE CLI via pipx.
18
+ - Updates the CAVE CLI via uv.
19
19
 
20
20
  Notes:
21
21
 
22
- - Without ``--version``, installs the latest version from PyPI via
23
- ``pipx install --force cave_cli``.
24
- - With ``--version``, reinstalls via ``pipx install --force`` from the
25
- specified git tag or branch.
22
+ - Without ``--version``, upgrades to the latest version from PyPI via
23
+ ``uv tool upgrade cave_cli``.
24
+ - With ``--version``, reinstalls via ``uv tool install --reinstall`` from
25
+ the specified git tag or branch.
26
26
  - On Windows, the update runs in a new console window because Windows
27
27
  locks running executables and cave.exe cannot replace itself.
28
28
  """
29
- pipx = shutil.which("pipx")
30
- if not pipx:
31
- logger.error(
32
- "pipx not found. Please install pipx and reinstall cave_cli."
33
- )
34
- logger.error(f"See: {PIPX_DOCS_URL}")
29
+ has_uv, uv_message, uv_path= check_uv()
30
+ if not has_uv:
31
+ logger.error(uv_message)
32
+ logger.error(f"The cave_cli package may need to be updated manually based on how it was installed.")
35
33
  sys.exit(1)
36
34
 
37
35
  version = getattr(args, "version", None)
38
36
  if version:
39
37
  label = f"Reinstalling CAVE CLI ({version})"
40
- spec = f"cave_cli @ git+{CLI_REPO_URL}@{version}"
38
+ cmd = [uv_path, "tool", "install", "--reinstall", f"cave_cli @ git+{CLI_REPO_URL}@{version}"]
41
39
  else:
42
40
  label = "Updating CAVE CLI"
43
- spec = "cave_cli"
41
+ cmd = [uv_path, "tool", "upgrade", "cave_cli"]
44
42
 
45
43
  step_start(label)
46
44
 
@@ -54,7 +52,7 @@ def update(args: argparse.Namespace) -> None:
54
52
  ) as f:
55
53
  f.write("@echo off\r\n")
56
54
  f.write("timeout /t 1 /nobreak >nul\r\n")
57
- f.write(f'"{pipx}" install --force "{spec}"\r\n')
55
+ f.write(" ".join(f'"{c}"' for c in cmd) + "\r\n")
58
56
  f.write('del "%~f0"\r\n')
59
57
  bat_path = f.name
60
58
  subprocess.Popen(
@@ -66,7 +64,7 @@ def update(args: argparse.Namespace) -> None:
66
64
  return
67
65
 
68
66
  result = subprocess.run(
69
- [pipx, "install", "--force", spec],
67
+ cmd,
70
68
  stdout=subprocess.PIPE,
71
69
  stderr=subprocess.PIPE,
72
70
  text=True,