usecli 0.1.78__tar.gz → 0.1.79__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.
- {usecli-0.1.78 → usecli-0.1.79}/PKG-INFO +16 -2
- {usecli-0.1.78 → usecli-0.1.79}/README.md +15 -1
- {usecli-0.1.78 → usecli-0.1.79}/pyproject.toml +1 -1
- {usecli-0.1.78 → usecli-0.1.79}/pyproject.toml.orig +1 -1
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/__init__.py +4 -2
- usecli-0.1.79/src/usecli/cli/commands/defaults/base/upgrade_command.py +316 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/templates/usecli.config.toml.j2 +3 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/entry.py +5 -2
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/shared/config/manager.py +5 -2
- usecli-0.1.79/src/usecli/shared/upgrades/__init__.py +22 -0
- usecli-0.1.79/src/usecli/shared/upgrades/checker.py +276 -0
- usecli-0.1.79/src/usecli/shared/upgrades/discovery.py +287 -0
- usecli-0.1.79/src/usecli/shared/upgrades/installer.py +262 -0
- usecli-0.1.79/src/usecli/shared/upgrades/pyproject.py +619 -0
- usecli-0.1.79/src/usecli/shared/upgrades/service.py +47 -0
- {usecli-0.1.78 → usecli-0.1.79}/LICENSE +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/bundler/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/bundler/__main__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/make_bundle_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/commands/init_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/config/colors.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/base_command.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/runtime.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/ui/list.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/ui/title.txt +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/templates/command.py.j2 +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/menu.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/params.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/progress.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/ui.py +0 -0
- {usecli-0.1.78 → usecli-0.1.79}/src/usecli/usecli.config.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usecli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.79
|
|
4
4
|
Summary: A powerful Python CLI framework for building beautiful, developer-friendly command-line tools.
|
|
5
5
|
Author: Edward Boswell
|
|
6
6
|
Author-email: Edward Boswell <thememium@gmail.com>
|
|
@@ -245,6 +245,7 @@ about Show app info
|
|
|
245
245
|
help Show help
|
|
246
246
|
init Initialize usecli (usecli only)
|
|
247
247
|
inspire Random quote
|
|
248
|
+
upgrade Upgrade the application to the latest version
|
|
248
249
|
make:command Create new command (usecli only)
|
|
249
250
|
make:theme Create new theme (usecli only)
|
|
250
251
|
make:bundle Build a standalone executable with PyInstaller (usecli only)
|
|
@@ -289,9 +290,14 @@ file is always named `usecli.config.toml`):
|
|
|
289
290
|
# main.py
|
|
290
291
|
from usecli import run
|
|
291
292
|
|
|
292
|
-
|
|
293
|
+
|
|
294
|
+
def main():
|
|
293
295
|
run() # auto-detect project config
|
|
294
296
|
# run("path/to/usecli.config.toml") # or point at a specific config
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
main()
|
|
295
301
|
```
|
|
296
302
|
|
|
297
303
|
Run it in development (no PyInstaller required):
|
|
@@ -300,6 +306,14 @@ Run it in development (no PyInstaller required):
|
|
|
300
306
|
uv run python main.py --help
|
|
301
307
|
```
|
|
302
308
|
|
|
309
|
+
To expose the same entry point as a console script, point `[project.scripts]`
|
|
310
|
+
at the `main` function in your `myapp` package (the usual `src/myapp` layout):
|
|
311
|
+
|
|
312
|
+
```toml
|
|
313
|
+
[project.scripts]
|
|
314
|
+
myapp = "myapp:main"
|
|
315
|
+
```
|
|
316
|
+
|
|
303
317
|
Because `run()` injects the located config explicitly, a custom `main.py` works
|
|
304
318
|
regardless of whether your CLI also has a `[project.scripts]` entry.
|
|
305
319
|
|
|
@@ -215,6 +215,7 @@ about Show app info
|
|
|
215
215
|
help Show help
|
|
216
216
|
init Initialize usecli (usecli only)
|
|
217
217
|
inspire Random quote
|
|
218
|
+
upgrade Upgrade the application to the latest version
|
|
218
219
|
make:command Create new command (usecli only)
|
|
219
220
|
make:theme Create new theme (usecli only)
|
|
220
221
|
make:bundle Build a standalone executable with PyInstaller (usecli only)
|
|
@@ -259,9 +260,14 @@ file is always named `usecli.config.toml`):
|
|
|
259
260
|
# main.py
|
|
260
261
|
from usecli import run
|
|
261
262
|
|
|
262
|
-
|
|
263
|
+
|
|
264
|
+
def main():
|
|
263
265
|
run() # auto-detect project config
|
|
264
266
|
# run("path/to/usecli.config.toml") # or point at a specific config
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|
|
265
271
|
```
|
|
266
272
|
|
|
267
273
|
Run it in development (no PyInstaller required):
|
|
@@ -270,6 +276,14 @@ Run it in development (no PyInstaller required):
|
|
|
270
276
|
uv run python main.py --help
|
|
271
277
|
```
|
|
272
278
|
|
|
279
|
+
To expose the same entry point as a console script, point `[project.scripts]`
|
|
280
|
+
at the `main` function in your `myapp` package (the usual `src/myapp` layout):
|
|
281
|
+
|
|
282
|
+
```toml
|
|
283
|
+
[project.scripts]
|
|
284
|
+
myapp = "myapp:main"
|
|
285
|
+
```
|
|
286
|
+
|
|
273
287
|
Because `run()` injects the located config explicitly, a custom `main.py` works
|
|
274
288
|
regardless of whether your CLI also has a `[project.scripts]` entry.
|
|
275
289
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.79"
|
|
4
4
|
description = "A powerful Python CLI framework for building beautiful, developer-friendly command-line tools."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Edward Boswell", email = "thememium@gmail.com" }]
|
|
@@ -274,7 +274,7 @@ def _ensure_cli_initialized() -> None:
|
|
|
274
274
|
fail(error.__class__.__name__, str(error), 1)
|
|
275
275
|
|
|
276
276
|
def invoke(self, ctx):
|
|
277
|
-
from click.exceptions import Exit
|
|
277
|
+
from click.exceptions import ClickException, Exit
|
|
278
278
|
|
|
279
279
|
from usecli.cli.core.runtime import is_json_mode
|
|
280
280
|
|
|
@@ -306,7 +306,9 @@ def _ensure_cli_initialized() -> None:
|
|
|
306
306
|
styled_error = UsecliUsageError(e.message, ctx=e.ctx)
|
|
307
307
|
styled_error.show()
|
|
308
308
|
sys.exit(styled_error.exit_code)
|
|
309
|
-
|
|
309
|
+
# UsecliError extends real-click ClickException, not the vendored
|
|
310
|
+
# typer._click one — catch both, mirroring the JSON path.
|
|
311
|
+
except (_TyperClickException, ClickException) as e:
|
|
310
312
|
if hasattr(e, "show"):
|
|
311
313
|
e.show()
|
|
312
314
|
sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""UpgradeCommand - upgrade the running application to the latest version.
|
|
2
|
+
|
|
3
|
+
A built-in default command available in every generated CLI. Installation
|
|
4
|
+
discovery, checking, and mutation are delegated to
|
|
5
|
+
``usecli.shared.upgrades.UpgradeService``; this command owns only the
|
|
6
|
+
interaction: flag handling, confirmation, and output formatting.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Annotated, Any
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from usecli.cli.config.colors import COLOR
|
|
16
|
+
from usecli.cli.core.base_command import BaseCommand
|
|
17
|
+
from usecli.cli.core.exceptions.base import UsecliError
|
|
18
|
+
from usecli.shared.config.manager import get_config
|
|
19
|
+
from usecli.shared.upgrades import UpgradeService
|
|
20
|
+
from usecli.shared.upgrades.checker import UpgradeStatus
|
|
21
|
+
from usecli.shared.upgrades.discovery import InstallInfo
|
|
22
|
+
from usecli.ui import Confirm
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _LazyConsole:
|
|
26
|
+
_console: Any | None = None
|
|
27
|
+
|
|
28
|
+
def _get_console(self) -> Any:
|
|
29
|
+
if self._console is None:
|
|
30
|
+
from rich.console import Console
|
|
31
|
+
|
|
32
|
+
self._console = Console()
|
|
33
|
+
return self._console
|
|
34
|
+
|
|
35
|
+
def __getattr__(self, name: str) -> Any:
|
|
36
|
+
return getattr(self._get_console(), name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
console = _LazyConsole()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _short_commit(commit: str | None) -> str:
|
|
43
|
+
"""Render a commit id for display; version labels pass through intact."""
|
|
44
|
+
if not commit:
|
|
45
|
+
return "-"
|
|
46
|
+
import re
|
|
47
|
+
|
|
48
|
+
if re.fullmatch(r"[0-9a-fA-F]{8,40}", commit):
|
|
49
|
+
return commit[:7]
|
|
50
|
+
return commit
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class UpgradeCommand(BaseCommand):
|
|
54
|
+
"""Upgrade the running application from its original install source."""
|
|
55
|
+
|
|
56
|
+
def visible(self) -> bool:
|
|
57
|
+
config = get_config()
|
|
58
|
+
return bool(config.get("upgrade.enabled", True))
|
|
59
|
+
|
|
60
|
+
def signature(self) -> str:
|
|
61
|
+
return "upgrade"
|
|
62
|
+
|
|
63
|
+
def description(self) -> str:
|
|
64
|
+
return "Upgrade the application to the latest version"
|
|
65
|
+
|
|
66
|
+
def handle(
|
|
67
|
+
self,
|
|
68
|
+
check: Annotated[
|
|
69
|
+
bool,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--check",
|
|
72
|
+
help="Only check for updates; do not install anything.",
|
|
73
|
+
),
|
|
74
|
+
] = False,
|
|
75
|
+
force: Annotated[
|
|
76
|
+
bool,
|
|
77
|
+
typer.Option(
|
|
78
|
+
"--force",
|
|
79
|
+
help=(
|
|
80
|
+
"Upgrade even when already up to date and skip the "
|
|
81
|
+
"confirmation prompt."
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
] = False,
|
|
85
|
+
) -> dict[str, object]:
|
|
86
|
+
"""Handle the command execution."""
|
|
87
|
+
from usecli.cli.core.runtime import is_json_mode
|
|
88
|
+
|
|
89
|
+
config = get_config()
|
|
90
|
+
install = UpgradeService.discover(config)
|
|
91
|
+
status = UpgradeService.check(install, config)
|
|
92
|
+
|
|
93
|
+
data: dict[str, object] = {
|
|
94
|
+
"mode": "check" if check else "apply",
|
|
95
|
+
"package": install.package,
|
|
96
|
+
"current": install.version,
|
|
97
|
+
"latest": status.latest,
|
|
98
|
+
"source": install.source,
|
|
99
|
+
"source_detail": status.detail,
|
|
100
|
+
"installer": install.installer,
|
|
101
|
+
"url": install.url,
|
|
102
|
+
"revision": install.revision,
|
|
103
|
+
"commit": install.commit,
|
|
104
|
+
"pinned": install.pinned,
|
|
105
|
+
"pinned_reason": install.pinned_reason,
|
|
106
|
+
"frozen": install.frozen,
|
|
107
|
+
"update_available": status.update_available,
|
|
108
|
+
"upgraded": None,
|
|
109
|
+
"error": status.error,
|
|
110
|
+
"latest_tag": status.latest_tag,
|
|
111
|
+
"latest_tag_commit": status.latest_tag_commit,
|
|
112
|
+
"note": status.note,
|
|
113
|
+
"pyproject_path": None,
|
|
114
|
+
"pyproject_previous_version": None,
|
|
115
|
+
"pyproject_new_version": None,
|
|
116
|
+
"pyproject_updated": None,
|
|
117
|
+
"pyproject_reason": None,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if check:
|
|
121
|
+
if status.error:
|
|
122
|
+
raise UsecliError(
|
|
123
|
+
status.error,
|
|
124
|
+
suggestion="Check your network connection and try again.",
|
|
125
|
+
)
|
|
126
|
+
if is_json_mode():
|
|
127
|
+
return data
|
|
128
|
+
self._print_check(status)
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
# Mutations are impossible for these installations; fail loudly.
|
|
132
|
+
if (
|
|
133
|
+
install.frozen
|
|
134
|
+
or install.pinned
|
|
135
|
+
or install.source
|
|
136
|
+
in (
|
|
137
|
+
"editable",
|
|
138
|
+
"unknown",
|
|
139
|
+
)
|
|
140
|
+
):
|
|
141
|
+
raise UsecliError(
|
|
142
|
+
self._blocked_message(install),
|
|
143
|
+
suggestion=self._blocked_suggestion(install),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if status.error is None and not status.update_available and not force:
|
|
147
|
+
data["upgraded"] = False
|
|
148
|
+
data["message"] = "Already up to date."
|
|
149
|
+
if is_json_mode():
|
|
150
|
+
return data
|
|
151
|
+
console.print(
|
|
152
|
+
f"[{COLOR.INFO}]Already up to date ({install.version}).[/{COLOR.INFO}]"
|
|
153
|
+
)
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
if not is_json_mode() and not force:
|
|
157
|
+
confirmed = Confirm.ask(
|
|
158
|
+
f"[{COLOR.WARNING}]Upgrade {install.package} from "
|
|
159
|
+
f"{install.version} to "
|
|
160
|
+
f"{_short_commit(status.latest) if status.latest else 'latest'}?"
|
|
161
|
+
f"[/{COLOR.WARNING}]",
|
|
162
|
+
default=False,
|
|
163
|
+
)
|
|
164
|
+
if not confirmed:
|
|
165
|
+
data["upgraded"] = False
|
|
166
|
+
data["message"] = "Aborted."
|
|
167
|
+
console.print(
|
|
168
|
+
f"[{COLOR.INFO}]Aborted. Re-run with --force to skip "
|
|
169
|
+
f"confirmation.[/{COLOR.INFO}]"
|
|
170
|
+
)
|
|
171
|
+
return data
|
|
172
|
+
|
|
173
|
+
result = self._run_upgrade(install, config, status)
|
|
174
|
+
data["upgraded"] = result.success
|
|
175
|
+
data["message"] = result.message
|
|
176
|
+
|
|
177
|
+
if result.pyproject is not None:
|
|
178
|
+
data["pyproject_path"] = result.pyproject.path
|
|
179
|
+
data["pyproject_previous_version"] = result.pyproject.previous_version
|
|
180
|
+
data["pyproject_new_version"] = result.pyproject.new_version
|
|
181
|
+
data["pyproject_updated"] = result.pyproject.updated
|
|
182
|
+
data["pyproject_reason"] = result.pyproject.reason
|
|
183
|
+
|
|
184
|
+
if not result.success:
|
|
185
|
+
raise UsecliError(
|
|
186
|
+
result.message or "Upgrade failed.",
|
|
187
|
+
suggestion="Run the upgrade command manually.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if is_json_mode():
|
|
191
|
+
return data
|
|
192
|
+
|
|
193
|
+
console.print(f"[{COLOR.SUCCESS}]✓ Upgrade complete.[/{COLOR.SUCCESS}]")
|
|
194
|
+
|
|
195
|
+
if result.pyproject is not None:
|
|
196
|
+
if result.pyproject.updated:
|
|
197
|
+
console.print(result.pyproject.summary)
|
|
198
|
+
elif result.pyproject.reason:
|
|
199
|
+
console.print(
|
|
200
|
+
f"[{COLOR.WARNING}]⚠ {result.pyproject.reason}[/{COLOR.WARNING}]"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
from usecli.cli.core.ui.title import get_project_name
|
|
204
|
+
|
|
205
|
+
console.print(
|
|
206
|
+
f"Run `[bold {COLOR.PRIMARY}]{get_project_name()} --version"
|
|
207
|
+
f"[/bold {COLOR.PRIMARY}]` to verify."
|
|
208
|
+
)
|
|
209
|
+
return data
|
|
210
|
+
|
|
211
|
+
def _run_upgrade(
|
|
212
|
+
self,
|
|
213
|
+
install: InstallInfo,
|
|
214
|
+
config: Any,
|
|
215
|
+
status: UpgradeStatus,
|
|
216
|
+
):
|
|
217
|
+
"""Run the upgrade behind a spinner (auto-disabled in JSON mode)."""
|
|
218
|
+
from usecli import Spinner
|
|
219
|
+
|
|
220
|
+
target = status.latest or "latest"
|
|
221
|
+
with Spinner(f"Upgrading {install.package}") as spinner:
|
|
222
|
+
spinner.update(
|
|
223
|
+
f"Upgrading {install.package} ({install.version} → {target})"
|
|
224
|
+
)
|
|
225
|
+
return UpgradeService.upgrade(
|
|
226
|
+
install,
|
|
227
|
+
config,
|
|
228
|
+
target_revision=status.latest_tag,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _print_check(self, status: UpgradeStatus) -> None:
|
|
232
|
+
"""Render the non-mutating check report."""
|
|
233
|
+
install = status.install
|
|
234
|
+
console.print()
|
|
235
|
+
console.print(f"[bold {COLOR.PRIMARY}]Upgrade Check[/bold {COLOR.PRIMARY}]")
|
|
236
|
+
console.print(f"[{COLOR.PRIMARY}]─" * 78)
|
|
237
|
+
self._print_row("Package", install.package or "unknown")
|
|
238
|
+
self._print_row("Installed", install.version)
|
|
239
|
+
self._print_row("Source", status.detail or install.source)
|
|
240
|
+
if install.source == "git":
|
|
241
|
+
if install.revision:
|
|
242
|
+
self._print_row("Ref", install.revision)
|
|
243
|
+
self._print_row("Commit", _short_commit(install.commit))
|
|
244
|
+
if status.latest:
|
|
245
|
+
self._print_row("Latest", _short_commit(status.latest))
|
|
246
|
+
elif status.latest:
|
|
247
|
+
self._print_row("Latest", status.latest)
|
|
248
|
+
self._print_row("Installer", install.installer or "unknown")
|
|
249
|
+
console.print()
|
|
250
|
+
|
|
251
|
+
if install.pinned:
|
|
252
|
+
console.print(
|
|
253
|
+
f"[{COLOR.WARNING}]This installation is pinned "
|
|
254
|
+
f"({install.pinned_reason}); no automatic upgrade."
|
|
255
|
+
f"[/{COLOR.WARNING}]"
|
|
256
|
+
)
|
|
257
|
+
elif status.update_available:
|
|
258
|
+
from usecli.cli.core.ui.title import get_project_name
|
|
259
|
+
|
|
260
|
+
console.print(
|
|
261
|
+
f"[{COLOR.SUCCESS}]Update available: {install.version} → "
|
|
262
|
+
f"{_short_commit(status.latest)}. Run "
|
|
263
|
+
f"`{get_project_name()} upgrade` to update.[/{COLOR.SUCCESS}]"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
if status.note:
|
|
267
|
+
console.print(f"[{COLOR.INFO}]{status.note}[/{COLOR.INFO}]")
|
|
268
|
+
else:
|
|
269
|
+
console.print(f"[{COLOR.INFO}]You are up to date.[/{COLOR.INFO}]")
|
|
270
|
+
console.print()
|
|
271
|
+
|
|
272
|
+
def _print_row(self, label: str, value: str) -> None:
|
|
273
|
+
visible_value = console.render_str(value).plain
|
|
274
|
+
right_align_column = 76
|
|
275
|
+
indent_width = 2
|
|
276
|
+
padding_spaces = 2
|
|
277
|
+
dots_length = (
|
|
278
|
+
right_align_column
|
|
279
|
+
- indent_width
|
|
280
|
+
- len(label)
|
|
281
|
+
- padding_spaces
|
|
282
|
+
- len(visible_value)
|
|
283
|
+
)
|
|
284
|
+
dots = "." * max(dots_length, 1)
|
|
285
|
+
console.print(
|
|
286
|
+
f" [{COLOR.FOREGROUND}]{label}[/{COLOR.FOREGROUND}] {dots} {value}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def _blocked_message(self, install: InstallInfo) -> str:
|
|
290
|
+
if install.frozen:
|
|
291
|
+
return "This application runs from a frozen bundle and cannot self-upgrade."
|
|
292
|
+
if install.source == "editable":
|
|
293
|
+
return "This is an editable install and cannot self-upgrade."
|
|
294
|
+
if install.pinned:
|
|
295
|
+
return (
|
|
296
|
+
f"This installation is pinned ({install.pinned_reason}) and "
|
|
297
|
+
"will not automatically upgrade."
|
|
298
|
+
)
|
|
299
|
+
return (
|
|
300
|
+
"Could not determine how this application was installed; "
|
|
301
|
+
"automatic upgrade is unavailable."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def _blocked_suggestion(self, install: InstallInfo) -> str:
|
|
305
|
+
if install.frozen:
|
|
306
|
+
return "Download the latest release or rebuild the bundle."
|
|
307
|
+
if install.source == "editable":
|
|
308
|
+
return "Update the source repository and re-sync (e.g. `uv sync`)."
|
|
309
|
+
if install.source == "git" and install.url:
|
|
310
|
+
revision = f"@{install.revision}" if install.revision else ""
|
|
311
|
+
return (
|
|
312
|
+
"Reinstall from the source you want, e.g. "
|
|
313
|
+
f'`uv tool install --force "{install.package} @ '
|
|
314
|
+
f'git+{install.url}{revision}"`.'
|
|
315
|
+
)
|
|
316
|
+
return "Reinstall the package with your package manager to upgrade."
|
|
@@ -9,3 +9,6 @@ templates_dir = "{{ templates_dir | default('cli/templates') }}"
|
|
|
9
9
|
themes_dir = "{{ themes_dir | default('cli/themes') }}"
|
|
10
10
|
theme = "{{ theme | default('default') }}"
|
|
11
11
|
hide_inspire = {{ hide_inspire | default(false) | lower }}
|
|
12
|
+
|
|
13
|
+
[usecli.upgrade]
|
|
14
|
+
enabled = {{ upgrade_enabled | default(true) | lower }}
|
|
@@ -5,9 +5,12 @@ thin ``main.py``::
|
|
|
5
5
|
|
|
6
6
|
from usecli import run
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
def main():
|
|
9
9
|
run() # auto-detect the project config
|
|
10
|
-
run("my/custom/cli.toml")
|
|
10
|
+
# run("my/custom/cli.toml") # or point at a specific config
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
11
14
|
|
|
12
15
|
It works in three contexts:
|
|
13
16
|
|
|
@@ -284,6 +284,8 @@ class ConfigManager:
|
|
|
284
284
|
"environment": "prod",
|
|
285
285
|
"command_name": "usecli",
|
|
286
286
|
"hide_inspire": False,
|
|
287
|
+
# Users disable the built-in `upgrade` command via [usecli.upgrade].
|
|
288
|
+
"upgrade": {"enabled": True},
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
def __init__(
|
|
@@ -320,7 +322,8 @@ class ConfigManager:
|
|
|
320
322
|
# find_project_root discovery (up-walk + bounded rglob tree search) that
|
|
321
323
|
# would otherwise be spent rediscovering it. Derive the project root
|
|
322
324
|
# from the config's own directory instead.
|
|
323
|
-
|
|
325
|
+
# Unreachable: the normalization above guarantees a Path here.
|
|
326
|
+
if usecli_config_path is None: # pragma: no cover
|
|
324
327
|
detected_root = find_project_root(start_dir)
|
|
325
328
|
else:
|
|
326
329
|
detected_root = None
|
|
@@ -328,7 +331,7 @@ class ConfigManager:
|
|
|
328
331
|
config_parent = self.usecli_config_path.parent
|
|
329
332
|
if detected_root is None:
|
|
330
333
|
detected_root = config_parent
|
|
331
|
-
else:
|
|
334
|
+
else: # pragma: no cover - detected_root is always None here
|
|
332
335
|
root_config = detected_root / USECLI_CONFIG_TOML
|
|
333
336
|
if self.usecli_config_path.resolve() != root_config.resolve():
|
|
334
337
|
detected_root = config_parent
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Reusable self-upgrade machinery for usecli applications.
|
|
2
|
+
|
|
3
|
+
Every generated CLI inherits a built-in ``upgrade`` command built on this
|
|
4
|
+
package. Installations are classified from standard packaging metadata
|
|
5
|
+
(``INSTALLER`` and PEP 610 ``direct_url.json``) — no CLI-author
|
|
6
|
+
configuration required.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from usecli.shared.upgrades.checker import UpgradeStatus
|
|
10
|
+
from usecli.shared.upgrades.discovery import InstallInfo, InstallSource
|
|
11
|
+
from usecli.shared.upgrades.installer import UpgradeResult
|
|
12
|
+
from usecli.shared.upgrades.pyproject import PyprojectUpdate
|
|
13
|
+
from usecli.shared.upgrades.service import UpgradeService
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"InstallInfo",
|
|
17
|
+
"InstallSource",
|
|
18
|
+
"PyprojectUpdate",
|
|
19
|
+
"UpgradeResult",
|
|
20
|
+
"UpgradeService",
|
|
21
|
+
"UpgradeStatus",
|
|
22
|
+
]
|