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.
Files changed (71) hide show
  1. {usecli-0.1.61 → usecli-0.1.64}/PKG-INFO +1 -1
  2. {usecli-0.1.61 → usecli-0.1.64}/pyproject.toml +1 -1
  3. usecli-0.1.64/src/usecli/__init__.py +457 -0
  4. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/about_command.py +2 -16
  5. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/config/colors.py +30 -56
  6. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/manager.py +223 -79
  7. usecli-0.1.61/src/usecli/__init__.py +0 -404
  8. {usecli-0.1.61 → usecli-0.1.64}/LICENSE +0 -0
  9. {usecli-0.1.61 → usecli-0.1.64}/README.md +0 -0
  10. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/__init__.py +0 -0
  11. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/README.md +0 -0
  12. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/__init__.py +0 -0
  13. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/custom/README.md +0 -0
  14. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/custom/__init__.py +0 -0
  15. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  16. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  17. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  18. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  19. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  20. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  21. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  22. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  23. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  24. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  25. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  26. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/commands/init_command.py +0 -0
  27. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/config/__init__.py +0 -0
  28. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/__init__.py +0 -0
  29. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/base_command.py +0 -0
  30. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/__init__.py +0 -0
  31. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/handler.py +0 -0
  32. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/error/utils.py +0 -0
  33. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  34. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/base.py +0 -0
  35. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/config.py +0 -0
  36. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/usage.py +0 -0
  37. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/exceptions/validation.py +0 -0
  38. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/__init__.py +0 -0
  39. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/list.py +0 -0
  40. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/ui/title.txt +0 -0
  42. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/__init__.py +0 -0
  43. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/network.py +0 -0
  44. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/numeric.py +0 -0
  45. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/path.py +0 -0
  46. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/core/validators/string.py +0 -0
  47. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/services/__init__.py +0 -0
  48. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/services/command_service.py +0 -0
  49. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/command.py.j2 +0 -0
  50. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  51. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  52. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  53. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  54. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  55. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  56. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  57. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/default.toml +0 -0
  58. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/dracula.toml +0 -0
  59. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  60. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/nord.toml +0 -0
  61. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  62. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/__init__.py +0 -0
  63. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  64. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  65. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/menu.py +0 -0
  66. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/params.py +0 -0
  67. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/__init__.py +0 -0
  68. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/__init__.py +0 -0
  69. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/shared/config/globals.py +0 -0
  70. {usecli-0.1.61 → usecli-0.1.64}/src/usecli/ui.py +0 -0
  71. {usecli-0.1.61 → usecli-0.1.64}/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.61
3
+ Version: 0.1.64
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.61"
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 importlib.metadata
88
+ from usecli.shared.config.manager import _find_distribution_for_console_script
89
89
 
90
- try:
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
- """Get all aliases for a console script from package metadata."""
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
- distributions = _get_distributions()
200
- for dist in distributions:
184
+ dist = _find_distribution_for_console_script(command_name)
185
+ if dist is not None:
201
186
  try:
202
- entry_points = dist.entry_points
203
- except Exception:
204
- continue
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
- break
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
- distributions = _get_distributions()
322
- for dist in distributions:
323
- try:
324
- entry_points = dist.entry_points
325
- except Exception:
326
- continue
327
- for entry_point in entry_points:
328
- if entry_point.group != "console_scripts":
329
- continue
330
- if entry_point.name != command_name:
331
- continue
332
- metadata = dist.metadata
333
- dist_name = ""
334
- if "Name" in metadata:
335
- dist_name = metadata["Name"]
336
- elif "name" in metadata:
337
- dist_name = metadata["name"]
338
- candidates: list[str] = []
339
- if dist_name:
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