usecli 0.1.61__tar.gz → 0.1.64__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.61 → usecli-0.1.64}/PKG-INFO +1 -1
- {usecli-0.1.61 → usecli-0.1.64}/pyproject.toml +1 -1
- usecli-0.1.64/src/usecli/__init__.py +457 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/about_command.py +2 -16
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/config/colors.py +30 -56
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/manager.py +223 -79
- usecli-0.1.61/src/usecli/__init__.py +0 -404
- {usecli-0.1.61 → usecli-0.1.64}/LICENSE +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/README.md +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/init_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/base_command.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/list.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/title.txt +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/command.py.j2 +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/menu.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/params.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/ui.py +0 -0
- {usecli-0.1.61 → usecli-0.1.64}/src/usecli/usecli.config.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.64"
|
|
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" }]
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Usecli CLI main entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
|
|
8
|
+
# Avoid importing typing at module level (costs ~6ms)
|
|
9
|
+
# TYPE_CHECKING is False at runtime, so the if-block never executes
|
|
10
|
+
TYPE_CHECKING = False
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from usecli.cli.core.base_command import BaseCommand
|
|
19
|
+
from usecli.menu import Menu
|
|
20
|
+
from usecli.params import Argument, Option
|
|
21
|
+
from usecli.ui import Confirm, Prompt
|
|
22
|
+
|
|
23
|
+
console: Console
|
|
24
|
+
|
|
25
|
+
# Lazy module-level placeholders - these are populated on first access
|
|
26
|
+
_app: Any = None
|
|
27
|
+
_service: Any = None
|
|
28
|
+
_help_resolved: bool = False
|
|
29
|
+
|
|
30
|
+
_LAZY_EXPORTS = {
|
|
31
|
+
"Menu": ("usecli.menu", "Menu"),
|
|
32
|
+
"Argument": ("usecli.params", "Argument"),
|
|
33
|
+
"Option": ("usecli.params", "Option"),
|
|
34
|
+
"Prompt": ("usecli.ui", "Prompt"),
|
|
35
|
+
"Confirm": ("usecli.ui", "Confirm"),
|
|
36
|
+
"Console": ("usecli.ui", "Console"),
|
|
37
|
+
"console": ("usecli.ui", "console"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __getattr__(name: str) -> Any:
|
|
42
|
+
# Handle lazy exports
|
|
43
|
+
export = _LAZY_EXPORTS.get(name)
|
|
44
|
+
if export is not None:
|
|
45
|
+
module_name, attr_name = export
|
|
46
|
+
value = getattr(import_module(module_name), attr_name)
|
|
47
|
+
globals()[name] = value
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
# Handle CLI framework components - lazy initialization
|
|
51
|
+
if name in ("app", "service", "BaseCommand", "colors", "theme"):
|
|
52
|
+
_ensure_cli_initialized()
|
|
53
|
+
return globals()[name]
|
|
54
|
+
|
|
55
|
+
# Handle run_app callback
|
|
56
|
+
if name == "run_app":
|
|
57
|
+
_ensure_cli_initialized()
|
|
58
|
+
_get_run_app_callback()
|
|
59
|
+
return globals()[name]
|
|
60
|
+
|
|
61
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _ensure_cli_initialized() -> None:
|
|
65
|
+
"""Lazily initialize the CLI framework components."""
|
|
66
|
+
# Use BaseCommand as the guard - it's set last, so if it's present,
|
|
67
|
+
# initialization completed successfully.
|
|
68
|
+
if "BaseCommand" in globals():
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
global _app, _service
|
|
72
|
+
|
|
73
|
+
import typer
|
|
74
|
+
from click.exceptions import BadParameter, ClickException, Exit, UsageError
|
|
75
|
+
from typer.core import TyperGroup
|
|
76
|
+
|
|
77
|
+
from usecli.cli.config.colors import COLOR
|
|
78
|
+
from usecli.cli.core.base_command import BaseCommand
|
|
79
|
+
from usecli.cli.services.command_service import CommandService
|
|
80
|
+
|
|
81
|
+
# Store exceptions for error handling
|
|
82
|
+
try:
|
|
83
|
+
from typer._click.exceptions import BadParameter as TyperBadParameter # type: ignore[import-untyped]
|
|
84
|
+
from typer._click.exceptions import ClickException as TyperClickException # type: ignore[import-untyped]
|
|
85
|
+
from typer._click.exceptions import UsageError as TyperUsageError # type: ignore[import-untyped]
|
|
86
|
+
except ImportError:
|
|
87
|
+
TyperBadParameter = BadParameter
|
|
88
|
+
TyperClickException = ClickException
|
|
89
|
+
TyperUsageError = UsageError
|
|
90
|
+
|
|
91
|
+
# Store in globals for use by other functions
|
|
92
|
+
globals()["_TyperBadParameter"] = TyperBadParameter
|
|
93
|
+
globals()["_TyperClickException"] = TyperClickException
|
|
94
|
+
globals()["_TyperUsageError"] = TyperUsageError
|
|
95
|
+
globals()["_Exit"] = Exit
|
|
96
|
+
globals()["_BadParameter"] = BadParameter
|
|
97
|
+
globals()["_ClickException"] = ClickException
|
|
98
|
+
globals()["_UsageError"] = UsageError
|
|
99
|
+
globals()["_TyperGroup"] = TyperGroup
|
|
100
|
+
|
|
101
|
+
# Create PrefixMatchingGroup now that TyperGroup is available
|
|
102
|
+
class PrefixMatchingGroup(TyperGroup):
|
|
103
|
+
"""Custom Typer group that supports prefix matching for commands."""
|
|
104
|
+
|
|
105
|
+
def get_command(self, ctx, cmd_name):
|
|
106
|
+
"""Get a command by name, with prefix matching fallback."""
|
|
107
|
+
rv = TyperGroup.get_command(self, ctx, cmd_name)
|
|
108
|
+
if rv is not None:
|
|
109
|
+
return rv
|
|
110
|
+
|
|
111
|
+
app = _get_app()
|
|
112
|
+
group_alias_registry = _get_group_alias_registry(app)
|
|
113
|
+
group_alias_to_primary = _build_alias_to_primary(group_alias_registry)
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
cmd_name in group_alias_to_primary
|
|
117
|
+
and group_alias_to_primary[cmd_name] != cmd_name
|
|
118
|
+
):
|
|
119
|
+
return TyperGroup.get_command(
|
|
120
|
+
self, ctx, group_alias_to_primary[cmd_name]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
|
|
124
|
+
group_aliases = [
|
|
125
|
+
alias for aliases in group_alias_registry.values() for alias in aliases
|
|
126
|
+
]
|
|
127
|
+
matches.extend(
|
|
128
|
+
[alias for alias in group_aliases if alias.startswith(cmd_name)]
|
|
129
|
+
)
|
|
130
|
+
matches = list(dict.fromkeys(matches))
|
|
131
|
+
|
|
132
|
+
if not matches:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if cmd_name in matches:
|
|
136
|
+
if (
|
|
137
|
+
cmd_name in group_alias_to_primary
|
|
138
|
+
and group_alias_to_primary[cmd_name] != cmd_name
|
|
139
|
+
):
|
|
140
|
+
return TyperGroup.get_command(
|
|
141
|
+
self, ctx, group_alias_to_primary[cmd_name]
|
|
142
|
+
)
|
|
143
|
+
return TyperGroup.get_command(self, ctx, cmd_name)
|
|
144
|
+
|
|
145
|
+
return _FilteredListCommand(cmd_name)
|
|
146
|
+
|
|
147
|
+
def main(
|
|
148
|
+
self,
|
|
149
|
+
args=None,
|
|
150
|
+
prog_name=None,
|
|
151
|
+
complete_var=None,
|
|
152
|
+
standalone_mode=True,
|
|
153
|
+
windows_expand_args=True,
|
|
154
|
+
**extra,
|
|
155
|
+
):
|
|
156
|
+
return super().main(
|
|
157
|
+
args=args,
|
|
158
|
+
prog_name=prog_name,
|
|
159
|
+
complete_var=complete_var,
|
|
160
|
+
standalone_mode=False,
|
|
161
|
+
windows_expand_args=windows_expand_args,
|
|
162
|
+
**extra,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def invoke(self, ctx):
|
|
166
|
+
from click.exceptions import BadParameter, ClickException, Exit, UsageError
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
return super().invoke(ctx)
|
|
170
|
+
except Exit:
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
except BadParameter as e:
|
|
173
|
+
from usecli.cli.core.exceptions import UsecliBadParameter
|
|
174
|
+
|
|
175
|
+
styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
|
|
176
|
+
styled_error.show()
|
|
177
|
+
sys.exit(styled_error.exit_code)
|
|
178
|
+
except UsageError as e:
|
|
179
|
+
from usecli.cli.core.exceptions import UsecliUsageError
|
|
180
|
+
|
|
181
|
+
styled_error = UsecliUsageError(e.message, ctx=e.ctx)
|
|
182
|
+
styled_error.show()
|
|
183
|
+
sys.exit(styled_error.exit_code)
|
|
184
|
+
except ClickException as e:
|
|
185
|
+
if hasattr(e, "show"):
|
|
186
|
+
e.show()
|
|
187
|
+
sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
|
|
188
|
+
|
|
189
|
+
globals()["PrefixMatchingGroup"] = PrefixMatchingGroup
|
|
190
|
+
|
|
191
|
+
# Setup module aliasing
|
|
192
|
+
colors = import_module("usecli.cli.config.colors")
|
|
193
|
+
globals()["colors"] = colors
|
|
194
|
+
globals()["theme"] = COLOR
|
|
195
|
+
sys.modules.setdefault(__name__ + ".colors", colors)
|
|
196
|
+
sys.modules.setdefault("colors", colors)
|
|
197
|
+
|
|
198
|
+
# Create app and service
|
|
199
|
+
_app = typer.Typer(
|
|
200
|
+
help="Usecli CLI - An elegant CLI framework for Python",
|
|
201
|
+
invoke_without_command=True,
|
|
202
|
+
no_args_is_help=False,
|
|
203
|
+
cls=PrefixMatchingGroup,
|
|
204
|
+
pretty_exceptions_enable=False,
|
|
205
|
+
)
|
|
206
|
+
globals()["app"] = _app
|
|
207
|
+
|
|
208
|
+
# Set BaseCommand BEFORE load_commands() since commands import it
|
|
209
|
+
globals()["BaseCommand"] = BaseCommand
|
|
210
|
+
|
|
211
|
+
_service = CommandService(_app)
|
|
212
|
+
_service.load_commands()
|
|
213
|
+
globals()["service"] = _service
|
|
214
|
+
|
|
215
|
+
# Register the callback eagerly so --version/--help work
|
|
216
|
+
# even when the app is accessed directly (e.g. smoke tests).
|
|
217
|
+
_get_run_app_callback()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_app():
|
|
221
|
+
_ensure_cli_initialized()
|
|
222
|
+
return _app
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _get_service():
|
|
226
|
+
_ensure_cli_initialized()
|
|
227
|
+
return _service
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = [
|
|
231
|
+
"BaseCommand",
|
|
232
|
+
"console",
|
|
233
|
+
"Console",
|
|
234
|
+
"main",
|
|
235
|
+
"Menu",
|
|
236
|
+
"Argument",
|
|
237
|
+
"Option",
|
|
238
|
+
"Prompt",
|
|
239
|
+
"Confirm",
|
|
240
|
+
"colors",
|
|
241
|
+
"theme",
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _console():
|
|
246
|
+
return __getattr__("console")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_interactive_flag_present() -> bool:
|
|
250
|
+
"""Check if -i/--interactive flag is present in sys.argv.
|
|
251
|
+
|
|
252
|
+
This allows the interactive flag to work regardless of position,
|
|
253
|
+
e.g., both 'usecli -i magic' and 'usecli magic -i' will work.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if -i or --interactive is found in sys.argv, False otherwise.
|
|
257
|
+
"""
|
|
258
|
+
return "-i" in sys.argv or "--interactive" in sys.argv
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _get_cli_help_text() -> str:
|
|
262
|
+
from usecli.shared.config.manager import get_config
|
|
263
|
+
|
|
264
|
+
fallback = "Usecli CLI - An elegant CLI framework for Python"
|
|
265
|
+
fallback_description = "An elegant CLI framework for Python"
|
|
266
|
+
config = get_config()
|
|
267
|
+
|
|
268
|
+
description = config.get("description")
|
|
269
|
+
has_description = (
|
|
270
|
+
config.has_key("description")
|
|
271
|
+
and isinstance(description, str)
|
|
272
|
+
and description.strip()
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
title = config.get("title")
|
|
276
|
+
has_title = config.has_key("title") and isinstance(title, str) and title.strip()
|
|
277
|
+
|
|
278
|
+
command_name = config.get("command_name")
|
|
279
|
+
has_command_name = (
|
|
280
|
+
config.has_key("command_name")
|
|
281
|
+
and isinstance(command_name, str)
|
|
282
|
+
and command_name.strip()
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if not has_description and not has_title and not has_command_name:
|
|
286
|
+
return fallback
|
|
287
|
+
|
|
288
|
+
display_name = (
|
|
289
|
+
title.strip()
|
|
290
|
+
if has_title
|
|
291
|
+
else command_name.strip()
|
|
292
|
+
if has_command_name
|
|
293
|
+
else "Usecli CLI"
|
|
294
|
+
)
|
|
295
|
+
display_description = (
|
|
296
|
+
description.strip() if has_description else fallback_description
|
|
297
|
+
)
|
|
298
|
+
return f"{display_name} - {display_description}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _get_group_alias_registry(app: Any) -> dict[str, list[str]]:
|
|
302
|
+
registry = getattr(app, "_usecli_group_aliases", {})
|
|
303
|
+
return registry if isinstance(registry, dict) else {}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _build_alias_to_primary(alias_registry: dict[str, list[str]]) -> dict[str, str]:
|
|
307
|
+
alias_to_primary: dict[str, str] = {}
|
|
308
|
+
for primary, aliases in alias_registry.items():
|
|
309
|
+
alias_to_primary[primary] = primary
|
|
310
|
+
for alias in aliases:
|
|
311
|
+
alias_to_primary[alias] = primary
|
|
312
|
+
return alias_to_primary
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# PrefixMatchingGroup is created lazily by _ensure_cli_initialized()
|
|
316
|
+
# to avoid importing typer at module level.
|
|
317
|
+
PrefixMatchingGroup = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class _FilteredListCommand:
|
|
321
|
+
"""Command that displays a filtered list of commands."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, prefix_filter: str) -> None:
|
|
324
|
+
self.prefix_filter = prefix_filter
|
|
325
|
+
self.name = "filtered-list"
|
|
326
|
+
|
|
327
|
+
def __call__(self, *args, **kwargs):
|
|
328
|
+
from usecli.cli.core.ui.list import list_commands
|
|
329
|
+
|
|
330
|
+
list_commands(_get_app(), prefix_filter=self.prefix_filter)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _get_default_help() -> str:
|
|
334
|
+
return "Usecli CLI - An elegant CLI framework for Python"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _resolve_help():
|
|
338
|
+
global _help_resolved
|
|
339
|
+
if not _help_resolved:
|
|
340
|
+
app = _get_app()
|
|
341
|
+
app.info.help = _get_cli_help_text()
|
|
342
|
+
_help_resolved = True
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _get_run_app_callback():
|
|
346
|
+
"""Get the run_app callback function."""
|
|
347
|
+
if "run_app" in globals():
|
|
348
|
+
return globals()["run_app"]
|
|
349
|
+
|
|
350
|
+
import typer
|
|
351
|
+
|
|
352
|
+
# Inject typer into module globals so annotation evaluation works
|
|
353
|
+
# (required because `from __future__ import annotations` makes them strings)
|
|
354
|
+
globals()["typer"] = typer
|
|
355
|
+
|
|
356
|
+
@_app.callback()
|
|
357
|
+
def run_app(
|
|
358
|
+
ctx: typer.Context,
|
|
359
|
+
version: bool = typer.Option(
|
|
360
|
+
None, "--version", "-v", help="Show the version and exit.", is_eager=True
|
|
361
|
+
),
|
|
362
|
+
help: bool = typer.Option(None, "--help", "-h", is_eager=True),
|
|
363
|
+
interactive: bool = typer.Option(
|
|
364
|
+
False, "--interactive", "-i", help="Run in interactive mode.", is_eager=True
|
|
365
|
+
),
|
|
366
|
+
) -> None:
|
|
367
|
+
_resolve_help()
|
|
368
|
+
|
|
369
|
+
if help:
|
|
370
|
+
from usecli.cli.core.ui.list import list_commands
|
|
371
|
+
|
|
372
|
+
list_commands(_get_app())
|
|
373
|
+
raise typer.Exit()
|
|
374
|
+
|
|
375
|
+
if version:
|
|
376
|
+
import shutil
|
|
377
|
+
|
|
378
|
+
from usecli.shared.config.manager import get_config
|
|
379
|
+
|
|
380
|
+
config = get_config()
|
|
381
|
+
service = _get_service()
|
|
382
|
+
command_path = shutil.which(sys.argv[0]) or sys.argv[0]
|
|
383
|
+
_console().print(
|
|
384
|
+
f"[bold {globals()['theme'].SECONDARY}]{config.get('title')} {service.version}[/bold {globals()['theme'].SECONDARY}] [{globals()['theme'].INFO}]({command_path})[/{globals()['theme'].INFO}]"
|
|
385
|
+
)
|
|
386
|
+
raise typer.Exit()
|
|
387
|
+
|
|
388
|
+
interactive_requested = interactive or _is_interactive_flag_present()
|
|
389
|
+
|
|
390
|
+
if interactive_requested:
|
|
391
|
+
from usecli.cli.commands.defaults.base.internal.fzf_command import (
|
|
392
|
+
run_interactive,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
cmd_parts = [ctx.invoked_subcommand] if ctx.invoked_subcommand else None
|
|
396
|
+
run_interactive(_get_app(), cmd_parts=cmd_parts)
|
|
397
|
+
raise typer.Exit()
|
|
398
|
+
|
|
399
|
+
if ctx.invoked_subcommand is None:
|
|
400
|
+
prefix_filter: str | None = None
|
|
401
|
+
if ctx.obj and isinstance(ctx.obj, dict):
|
|
402
|
+
prefix_filter = ctx.obj.get("prefix_filter")
|
|
403
|
+
from usecli.cli.core.ui.list import list_commands
|
|
404
|
+
|
|
405
|
+
list_commands(_get_app(), prefix_filter=prefix_filter)
|
|
406
|
+
|
|
407
|
+
globals()["run_app"] = run_app
|
|
408
|
+
return run_app
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def main() -> None:
|
|
412
|
+
"""Run the CLI application with custom error handling."""
|
|
413
|
+
from click.exceptions import BadParameter, ClickException, Exit, UsageError
|
|
414
|
+
|
|
415
|
+
_ensure_cli_initialized()
|
|
416
|
+
_resolve_help()
|
|
417
|
+
|
|
418
|
+
from usecli.shared.config.manager import get_config
|
|
419
|
+
|
|
420
|
+
config = get_config()
|
|
421
|
+
command_name = config._get_command_name()
|
|
422
|
+
if command_name == "usecli" and not config.is_usecli_direct_dependency():
|
|
423
|
+
_console().print(
|
|
424
|
+
"[bold red]Error:[/bold red] usecli is not a direct dependency of this project."
|
|
425
|
+
)
|
|
426
|
+
_console().print(
|
|
427
|
+
"Add it to your [cyan]pyproject.toml[/cyan] dependencies or dependency-groups."
|
|
428
|
+
)
|
|
429
|
+
sys.exit(1)
|
|
430
|
+
|
|
431
|
+
# Setup the callback
|
|
432
|
+
_get_run_app_callback()
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
_get_app()()
|
|
436
|
+
except Exit:
|
|
437
|
+
sys.exit(0)
|
|
438
|
+
except BadParameter as e:
|
|
439
|
+
from usecli.cli.core.exceptions import UsecliBadParameter
|
|
440
|
+
|
|
441
|
+
styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
|
|
442
|
+
styled_error.show()
|
|
443
|
+
sys.exit(styled_error.exit_code)
|
|
444
|
+
except UsageError as e:
|
|
445
|
+
from usecli.cli.core.exceptions import UsecliUsageError
|
|
446
|
+
|
|
447
|
+
styled_error = UsecliUsageError(e.message, ctx=e.ctx)
|
|
448
|
+
styled_error.show()
|
|
449
|
+
sys.exit(styled_error.exit_code)
|
|
450
|
+
except ClickException as e:
|
|
451
|
+
if hasattr(e, "show"):
|
|
452
|
+
e.show()
|
|
453
|
+
sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
main()
|
|
@@ -85,23 +85,9 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
|
|
|
85
85
|
def _get_console_script_distribution(command_name: str | None):
|
|
86
86
|
if not command_name:
|
|
87
87
|
return None
|
|
88
|
-
import
|
|
88
|
+
from usecli.shared.config.manager import _find_distribution_for_console_script
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
distributions = importlib.metadata.distributions()
|
|
92
|
-
except Exception:
|
|
93
|
-
return None
|
|
94
|
-
for dist in distributions:
|
|
95
|
-
try:
|
|
96
|
-
entry_points = dist.entry_points
|
|
97
|
-
except Exception:
|
|
98
|
-
continue
|
|
99
|
-
for entry_point in entry_points:
|
|
100
|
-
if entry_point.group != "console_scripts":
|
|
101
|
-
continue
|
|
102
|
-
if entry_point.name == command_name:
|
|
103
|
-
return dist
|
|
104
|
-
return None
|
|
90
|
+
return _find_distribution_for_console_script(command_name)
|
|
105
91
|
|
|
106
92
|
|
|
107
93
|
def _get_package_dependencies_from_distribution(dist) -> list[tuple[str, str | None]]:
|
|
@@ -175,41 +175,21 @@ def _get_command_name() -> str | None:
|
|
|
175
175
|
return command if command else None
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
_distributions_cache: list[Any] | None = None
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _get_distributions() -> list[Any]:
|
|
182
|
-
global _distributions_cache
|
|
183
|
-
if _distributions_cache is not None:
|
|
184
|
-
return _distributions_cache
|
|
185
|
-
try:
|
|
186
|
-
import importlib.metadata
|
|
187
|
-
|
|
188
|
-
_distributions_cache = list(importlib.metadata.distributions())
|
|
189
|
-
except Exception:
|
|
190
|
-
_distributions_cache = []
|
|
191
|
-
return _distributions_cache
|
|
192
|
-
|
|
193
|
-
|
|
194
178
|
def _get_console_script_aliases(command_name: str | None) -> set[str]:
|
|
195
|
-
|
|
179
|
+
from usecli.shared.config.manager import _find_distribution_for_console_script
|
|
180
|
+
|
|
196
181
|
if not command_name:
|
|
197
182
|
return set()
|
|
198
183
|
aliases: set[str] = {command_name}
|
|
199
|
-
|
|
200
|
-
|
|
184
|
+
dist = _find_distribution_for_console_script(command_name)
|
|
185
|
+
if dist is not None:
|
|
201
186
|
try:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
names = [
|
|
206
|
-
entry_point.name
|
|
207
|
-
for entry_point in entry_points
|
|
208
|
-
if entry_point.group == "console_scripts"
|
|
209
|
-
]
|
|
210
|
-
if command_name in names:
|
|
187
|
+
names = [
|
|
188
|
+
ep.name for ep in dist.entry_points if ep.group == "console_scripts"
|
|
189
|
+
]
|
|
211
190
|
aliases.update(names)
|
|
212
|
-
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
213
193
|
return aliases
|
|
214
194
|
|
|
215
195
|
|
|
@@ -315,36 +295,30 @@ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
|
|
|
315
295
|
|
|
316
296
|
|
|
317
297
|
def _find_usecli_config_for_console_script() -> Path | None:
|
|
298
|
+
from usecli.shared.config.manager import _find_distribution_for_console_script
|
|
299
|
+
|
|
318
300
|
command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
|
|
319
301
|
if not command_name:
|
|
320
302
|
return None
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
candidates.append(dist_name)
|
|
341
|
-
normalized = dist_name.replace("-", "_")
|
|
342
|
-
if normalized not in candidates:
|
|
343
|
-
candidates.append(normalized)
|
|
344
|
-
for package_name in candidates:
|
|
345
|
-
match = _find_usecli_config_in_named_package(package_name)
|
|
346
|
-
if match:
|
|
347
|
-
return match
|
|
303
|
+
dist = _find_distribution_for_console_script(command_name)
|
|
304
|
+
if dist is None:
|
|
305
|
+
return None
|
|
306
|
+
metadata = dist.metadata
|
|
307
|
+
dist_name = ""
|
|
308
|
+
if "Name" in metadata:
|
|
309
|
+
dist_name = metadata["Name"]
|
|
310
|
+
elif "name" in metadata:
|
|
311
|
+
dist_name = metadata["name"]
|
|
312
|
+
candidates: list[str] = []
|
|
313
|
+
if dist_name:
|
|
314
|
+
candidates.append(dist_name)
|
|
315
|
+
normalized = dist_name.replace("-", "_")
|
|
316
|
+
if normalized not in candidates:
|
|
317
|
+
candidates.append(normalized)
|
|
318
|
+
for package_name in candidates:
|
|
319
|
+
match = _find_usecli_config_in_named_package(package_name)
|
|
320
|
+
if match:
|
|
321
|
+
return match
|
|
348
322
|
return None
|
|
349
323
|
|
|
350
324
|
|