usecli 0.1.29__tar.gz → 0.1.31__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 (70) hide show
  1. {usecli-0.1.29 → usecli-0.1.31}/PKG-INFO +8 -2
  2. {usecli-0.1.29 → usecli-0.1.31}/README.md +7 -1
  3. {usecli-0.1.29 → usecli-0.1.31}/pyproject.toml +1 -1
  4. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/__init__.py +1 -1
  5. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/base_command.py +93 -10
  6. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/exceptions/usage.py +17 -2
  7. usecli-0.1.31/src/usecli/cli/core/ui/list.py +336 -0
  8. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/templates/command.py.j2 +3 -0
  9. usecli-0.1.29/src/usecli/cli/core/ui/list.py +0 -217
  10. {usecli-0.1.29 → usecli-0.1.31}/LICENSE +0 -0
  11. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/__init__.py +0 -0
  12. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/README.md +0 -0
  13. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/__init__.py +0 -0
  14. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/custom/README.md +0 -0
  15. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/custom/__init__.py +0 -0
  16. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  17. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  18. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  19. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  20. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  21. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  22. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  23. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  24. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  25. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  26. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  27. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  28. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/commands/init_command.py +0 -0
  29. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/config/__init__.py +0 -0
  30. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/config/colors.py +0 -0
  31. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/__init__.py +0 -0
  32. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/error/__init__.py +0 -0
  33. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/error/handler.py +0 -0
  34. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/error/utils.py +0 -0
  35. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  36. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/exceptions/base.py +0 -0
  37. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/exceptions/config.py +0 -0
  38. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/exceptions/validation.py +0 -0
  39. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/skill_generator.py +0 -0
  40. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/ui/__init__.py +0 -0
  41. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/ui/title.py +0 -0
  42. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/validators/__init__.py +0 -0
  43. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/validators/network.py +0 -0
  44. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/validators/numeric.py +0 -0
  45. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/validators/path.py +0 -0
  46. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/core/validators/string.py +0 -0
  47. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/services/__init__.py +0 -0
  48. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/services/command_service.py +0 -0
  49. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/templates/usecli.toml.j2 +0 -0
  51. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/params.py +0 -0
  66. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/shared/config/manager.py +0 -0
  70. {usecli-0.1.29 → usecli-0.1.31}/src/usecli/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.29
3
+ Version: 0.1.31
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>
@@ -77,12 +77,18 @@ useCli A powerful Python CLI framework for building beautiful, developer-friendl
77
77
 
78
78
  ## Quick Start
79
79
 
80
- ### Install usecli:
80
+ ### Install usecli with uv (recommended)
81
81
 
82
82
  ```sh
83
83
  uv add usecli
84
84
  ```
85
85
 
86
+ ### Install with pip (alternative)
87
+
88
+ ```sh
89
+ pip install usecli
90
+ ```
91
+
86
92
  ### Initialize
87
93
 
88
94
  ```sh
@@ -48,12 +48,18 @@ useCli A powerful Python CLI framework for building beautiful, developer-friendl
48
48
 
49
49
  ## Quick Start
50
50
 
51
- ### Install usecli:
51
+ ### Install usecli with uv (recommended)
52
52
 
53
53
  ```sh
54
54
  uv add usecli
55
55
  ```
56
56
 
57
+ ### Install with pip (alternative)
58
+
59
+ ```sh
60
+ pip install usecli
61
+ ```
62
+
57
63
  ### Initialize
58
64
 
59
65
  ```sh
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.29"
3
+ version = "0.1.31"
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" }]
@@ -192,7 +192,7 @@ def run_app(
192
192
  ),
193
193
  help: bool = typer.Option(None, "--help", "-h", is_eager=True),
194
194
  interactive: bool = typer.Option(
195
- False, "--interactive", "-i", help="Run in interactive mode", is_eager=True
195
+ False, "--interactive", "-i", help="Run in interactive mode.", is_eager=True
196
196
  ),
197
197
  ) -> None:
198
198
  """Main callback for the CLI application.
@@ -12,6 +12,7 @@ from rich.console import Console
12
12
  from typer.core import TyperCommand
13
13
 
14
14
  from usecli.cli.config.colors import COLOR
15
+ from usecli.cli.core.ui.title import get_script_command_name
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from click.core import Context as ClickContext
@@ -88,7 +89,7 @@ class NestedCommandRegistry:
88
89
  False,
89
90
  "--interactive",
90
91
  "-i",
91
- help="Run in interactive mode",
92
+ help="Run in interactive mode.",
92
93
  ),
93
94
  ) -> None:
94
95
  """Callback for the command group."""
@@ -181,7 +182,7 @@ class CustomHelpCommand(TyperCommand):
181
182
  Option(
182
183
  ["--interactive", "-i"],
183
184
  is_flag=True,
184
- help="Run in interactive mode",
185
+ help="Run in interactive mode.",
185
186
  )
186
187
  )
187
188
 
@@ -223,7 +224,8 @@ class CustomHelpCommand(TyperCommand):
223
224
  ]
224
225
 
225
226
  arg_usage = " ".join(rf"\[{name.upper()}]" for name in argument_names)
226
- usage = f" [bold {COLOR.WARNING}]usecli {self.name}[/bold {COLOR.WARNING}] [bold {COLOR.PRIMARY}][OPTIONS]{f' {arg_usage}' if arg_usage else ''}[/bold {COLOR.PRIMARY}]"
227
+ command_name = get_script_command_name(default="usecli") or "usecli"
228
+ usage = f" [bold {COLOR.WARNING}]{command_name} {self.name}[/bold {COLOR.WARNING}] [bold {COLOR.PRIMARY}][OPTIONS]{f' {arg_usage}' if arg_usage else ''}[/bold {COLOR.PRIMARY}]"
227
229
 
228
230
  console.print()
229
231
  console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
@@ -312,6 +314,9 @@ class BaseCommand(ABC):
312
314
  """
313
315
  pass
314
316
 
317
+ def aliases(self) -> list[str]:
318
+ return []
319
+
315
320
  def visible(self) -> bool:
316
321
  return True
317
322
 
@@ -321,6 +326,7 @@ class BaseCommand(ABC):
321
326
 
322
327
  signature = self.signature()
323
328
  signature_parts = signature.split()
329
+ aliases = self.aliases()
324
330
 
325
331
  # Check if this is a space-separated nested command signature (e.g., "spec show")
326
332
  # vs a command with argument placeholders (e.g., "test-cmd <name>")
@@ -338,21 +344,98 @@ class BaseCommand(ABC):
338
344
  registry = NestedCommandRegistry()
339
345
  group_app = registry.get_or_create_group(self.app, group_name)
340
346
 
341
- cmd_decorator = group_app.command(
347
+ normalized_aliases = self._normalize_aliases(
348
+ primary_name=command_name,
349
+ aliases=aliases,
350
+ group_name=group_name,
351
+ )
352
+ self._register_with_aliases(
353
+ app=group_app,
342
354
  name=command_name,
343
- help=self.description(),
344
- cls=CustomHelpCommand,
355
+ description=self.description(),
356
+ aliases=normalized_aliases,
345
357
  )
346
- cmd_decorator(self.handle)
347
358
  else:
348
359
  # Single-level command (e.g., "help", "init", "config:set", "make:command")
349
360
  name = signature_parts[0]
350
- cmd_decorator = self.app.command(
361
+ normalized_aliases = self._normalize_aliases(
362
+ primary_name=name,
363
+ aliases=aliases,
364
+ group_name=None,
365
+ )
366
+ self._register_with_aliases(
367
+ app=self.app,
351
368
  name=name,
352
- help=self.description(),
369
+ description=self.description(),
370
+ aliases=normalized_aliases,
371
+ )
372
+
373
+ def _register_with_aliases(
374
+ self,
375
+ app: typer.Typer,
376
+ name: str,
377
+ description: str,
378
+ aliases: list[str],
379
+ ) -> None:
380
+ cmd_decorator = app.command(
381
+ name=name,
382
+ help=description,
383
+ cls=CustomHelpCommand,
384
+ )
385
+ cmd_decorator(self.handle)
386
+
387
+ if not aliases:
388
+ return
389
+
390
+ alias_registry = self._get_alias_registry(app)
391
+ alias_registry.setdefault(name, [])
392
+ for alias in aliases:
393
+ if alias == name or alias in alias_registry[name]:
394
+ continue
395
+ alias_registry[name].append(alias)
396
+ alias_decorator = app.command(
397
+ name=alias,
398
+ help=description,
353
399
  cls=CustomHelpCommand,
354
400
  )
355
- cmd_decorator(self.handle)
401
+ alias_decorator(self.handle)
402
+
403
+ def _get_alias_registry(self, app: typer.Typer) -> dict[str, list[str]]:
404
+ if not hasattr(app, "_usecli_aliases"):
405
+ setattr(app, "_usecli_aliases", {})
406
+ registry = getattr(app, "_usecli_aliases")
407
+ if not isinstance(registry, dict):
408
+ registry = {}
409
+ setattr(app, "_usecli_aliases", registry)
410
+ return registry
411
+
412
+ def _normalize_aliases(
413
+ self,
414
+ primary_name: str,
415
+ aliases: list[str],
416
+ group_name: str | None,
417
+ ) -> list[str]:
418
+ normalized: list[str] = []
419
+ for alias in aliases:
420
+ alias_parts = alias.split()
421
+ if group_name:
422
+ if len(alias_parts) == 2 and alias_parts[0] == group_name:
423
+ alias_name = alias_parts[1]
424
+ elif len(alias_parts) == 1:
425
+ alias_name = alias_parts[0]
426
+ else:
427
+ continue
428
+ if not self._is_valid_subcommand_name(alias_name):
429
+ continue
430
+ else:
431
+ if len(alias_parts) != 1:
432
+ continue
433
+ alias_name = alias_parts[0]
434
+
435
+ if alias_name == primary_name or alias_name in normalized:
436
+ continue
437
+ normalized.append(alias_name)
438
+ return normalized
356
439
 
357
440
  def _is_valid_subcommand_name(self, name: str) -> bool:
358
441
  """Check if a string is a valid subcommand name (not an argument placeholder).
@@ -5,14 +5,29 @@ from __future__ import annotations
5
5
  import sys
6
6
  from typing import IO
7
7
 
8
+ from click.core import Context as ClickContext
8
9
  from click.exceptions import BadParameter, UsageError
9
10
  from rich.console import Console
10
11
 
11
12
  from usecli.cli.config.colors import COLOR
13
+ from usecli.cli.core.ui.title import get_script_command_name
12
14
 
13
15
  console = Console(stderr=True)
14
16
 
15
17
 
18
+ def _get_help_text_with_command_name(ctx: ClickContext) -> str:
19
+ command_name = get_script_command_name(default=getattr(ctx, "info_name", None))
20
+ if not command_name:
21
+ return ctx.get_help()
22
+
23
+ original_info_name = getattr(ctx, "info_name", None)
24
+ try:
25
+ ctx.info_name = command_name
26
+ return ctx.get_help()
27
+ finally:
28
+ ctx.info_name = original_info_name
29
+
30
+
16
31
  class UsecliUsageError(UsageError):
17
32
  """Usage error with styled output and full command help.
18
33
 
@@ -35,7 +50,7 @@ class UsecliUsageError(UsageError):
35
50
 
36
51
  if self.ctx:
37
52
  console.print()
38
- help_text = self.ctx.get_help()
53
+ help_text = _get_help_text_with_command_name(self.ctx)
39
54
  console.print(help_text)
40
55
 
41
56
 
@@ -64,5 +79,5 @@ class UsecliBadParameter(BadParameter):
64
79
  console.print(f"{error_prefix} {error_msg}")
65
80
 
66
81
  if self.ctx:
67
- help_text = self.ctx.get_help()
82
+ help_text = _get_help_text_with_command_name(self.ctx)
68
83
  console.print(help_text)
@@ -0,0 +1,336 @@
1
+ """Command listing utilities for usecli CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, TypedDict
6
+
7
+ import click
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from usecli.cli.config.colors import COLOR
12
+ from usecli.cli.core.ui.title import (
13
+ get_project_name,
14
+ get_script_command_name,
15
+ print_title,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ pass
20
+
21
+ console = Console()
22
+
23
+ SPACER_LENGTH = 6
24
+
25
+
26
+ class CommandEntry(TypedDict):
27
+ name: str
28
+ display_name: str
29
+ help: str
30
+ aliases: list[str]
31
+
32
+
33
+ class CommandMeta(TypedDict):
34
+ help: str
35
+ aliases: list[str]
36
+
37
+
38
+ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
39
+ """List all available commands with optional filtering.
40
+
41
+ Displays commands in a formatted list with sections for grouped commands
42
+ (those with colons in their names).
43
+
44
+ Args:
45
+ app: The Typer application instance.
46
+ prefix_filter: Optional prefix to filter commands by name.
47
+ """
48
+ project_name = get_project_name()
49
+ print_title(title=project_name)
50
+
51
+ click_group = typer.main.get_command(app)
52
+
53
+ alias_registry = _get_alias_registry(app)
54
+ alias_to_primary = _build_alias_to_primary(alias_registry)
55
+
56
+ command_name = get_script_command_name(default="usecli")
57
+
58
+ commands_by_name: dict[str, CommandMeta] = {}
59
+ for command in app.registered_commands:
60
+ callback = command.callback
61
+ name = command.name or (
62
+ getattr(callback, "__name__", "unknown") if callback else "unknown"
63
+ )
64
+ if name in alias_to_primary and alias_to_primary[name] != name:
65
+ continue
66
+ help_text = command.help or ""
67
+ if name not in commands_by_name or (
68
+ not commands_by_name[name]["help"] and help_text
69
+ ):
70
+ commands_by_name[name] = {
71
+ "help": help_text,
72
+ "aliases": alias_registry.get(name, list[str]()),
73
+ }
74
+
75
+ commands: list[CommandEntry] = []
76
+ for name in commands_by_name:
77
+ aliases = commands_by_name[name]["aliases"]
78
+ command_entry: CommandEntry = {
79
+ "name": name,
80
+ "display_name": _format_display_name(name, aliases),
81
+ "help": commands_by_name[name]["help"],
82
+ "aliases": aliases,
83
+ }
84
+ commands.append(command_entry)
85
+ commands.sort(key=lambda x: x["name"])
86
+
87
+ if prefix_filter:
88
+ filtered = [
89
+ c
90
+ for c in commands
91
+ if c["name"].startswith(prefix_filter)
92
+ or any(alias.startswith(prefix_filter) for alias in c["aliases"])
93
+ ]
94
+ if not filtered:
95
+ console.print(f" [dim]No commands found for '{prefix_filter}'[/dim]")
96
+ console.print()
97
+ return
98
+ commands = filtered
99
+
100
+ groups: dict[str, str] = {}
101
+ if isinstance(click_group, click.Group):
102
+ for cmd_name, cmd_obj in click_group.commands.items():
103
+ if isinstance(cmd_obj, click.Group):
104
+ groups[cmd_name] = (
105
+ getattr(cmd_obj, "help", f"Commands for {cmd_name}")
106
+ or f"Commands for {cmd_name}"
107
+ )
108
+
109
+ option_flags = []
110
+ display_params = _order_completion_params(click_group.params or [])
111
+ if display_params:
112
+ for param in display_params:
113
+ flags = ", ".join(param.opts)
114
+ if "--help" in flags:
115
+ continue
116
+ option_flags.append(flags)
117
+
118
+ help_flags = "--help, -h"
119
+ all_option_flags = [help_flags] + option_flags
120
+ all_display_names = [cmd["display_name"] for cmd in commands] + list(groups.keys())
121
+ all_labels = all_display_names + all_option_flags
122
+ longest_label_length = max((len(label) for label in all_labels), default=0)
123
+
124
+ console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
125
+ console.print(f" [{COLOR.PRIMARY}]{command_name} [OPTIONS] [ARGUMENTS]")
126
+ console.print()
127
+
128
+ console.print(f"[bold {COLOR.SECONDARY}]Options:")
129
+
130
+ help_padding = " " * (longest_label_length - len(help_flags) + SPACER_LENGTH)
131
+ console.print(
132
+ f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
133
+ )
134
+
135
+ if display_params:
136
+ for param in display_params:
137
+ flags = ", ".join(param.opts)
138
+ if "--help" in flags:
139
+ continue
140
+ description = _get_option_description(param)
141
+ padding = " " * (longest_label_length - len(flags) + SPACER_LENGTH)
142
+ console.print(
143
+ f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
144
+ )
145
+ console.print()
146
+
147
+ if not prefix_filter:
148
+ console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
149
+
150
+ top_level: list[CommandEntry] = [
151
+ c for c in commands if ":" not in c["name"] and c["name"] not in groups
152
+ ]
153
+ with_colon: list[CommandEntry] = [c for c in commands if ":" in c["name"]]
154
+
155
+ def print_command(cmd: CommandEntry) -> None:
156
+ """Print a single command with proper formatting."""
157
+ display_name = cmd["display_name"]
158
+ padding = " " * (longest_label_length - len(display_name) + SPACER_LENGTH)
159
+ console.print(
160
+ f" [{COLOR.COMMAND} not bold]{display_name}[/{COLOR.COMMAND} not bold]{padding}{cmd['help']}"
161
+ )
162
+
163
+ for group_name, group_help in groups.items():
164
+ top_level.append(
165
+ {
166
+ "name": group_name,
167
+ "display_name": group_name,
168
+ "help": group_help,
169
+ "aliases": [],
170
+ }
171
+ )
172
+
173
+ top_level.sort(key=lambda x: x["name"])
174
+
175
+ if top_level:
176
+ for cmd in top_level:
177
+ print_command(cmd)
178
+ console.print()
179
+
180
+ sections: dict[str, list[CommandEntry]] = {}
181
+ for cmd in with_colon:
182
+ section_prefix = cmd["name"].split(":")[0]
183
+ if section_prefix not in sections:
184
+ sections[section_prefix] = []
185
+ sections[section_prefix].append(cmd)
186
+
187
+ for section_prefix, section_cmds in sections.items():
188
+ console.print(f"[bold {COLOR.SECONDARY}]{section_prefix}:")
189
+ for cmd in section_cmds:
190
+ print_command(cmd)
191
+ console.print()
192
+
193
+
194
+ def list_group_commands(group_app: typer.Typer, group_name: str) -> None:
195
+ """List all commands within a specific command group.
196
+
197
+ Displays commands in a formatted list for a nested command group,
198
+ similar to how list_commands works for the main app.
199
+
200
+ Args:
201
+ group_app: The Typer sub-app for the command group.
202
+ group_name: The name of the command group.
203
+ """
204
+ alias_registry = _get_alias_registry(group_app)
205
+ alias_to_primary = _build_alias_to_primary(alias_registry)
206
+
207
+ command_name = get_script_command_name(default="usecli")
208
+
209
+ console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
210
+ console.print(
211
+ f" [{COLOR.PRIMARY}]{command_name} {group_name} [COMMAND] [OPTIONS][/]"
212
+ )
213
+ console.print()
214
+ commands_by_name: dict[str, CommandMeta] = {}
215
+ for command in group_app.registered_commands:
216
+ callback = command.callback
217
+ name = command.name or (
218
+ getattr(callback, "__name__", "unknown") if callback else "unknown"
219
+ )
220
+ if name in alias_to_primary and alias_to_primary[name] != name:
221
+ continue
222
+ help_text = command.help or ""
223
+ if name not in commands_by_name or (
224
+ not commands_by_name[name]["help"] and help_text
225
+ ):
226
+ commands_by_name[name] = {
227
+ "help": help_text,
228
+ "aliases": alias_registry.get(name, list[str]()),
229
+ }
230
+
231
+ commands: list[CommandEntry] = []
232
+ for name in commands_by_name:
233
+ aliases = commands_by_name[name]["aliases"]
234
+ command_entry: CommandEntry = {
235
+ "name": name,
236
+ "display_name": _format_display_name(name, aliases),
237
+ "help": commands_by_name[name]["help"],
238
+ "aliases": aliases,
239
+ }
240
+ commands.append(command_entry)
241
+ commands.sort(key=lambda x: x["name"])
242
+
243
+ click_group = typer.main.get_command(group_app)
244
+ option_flags = []
245
+ display_params = _order_completion_params(click_group.params or [])
246
+ if display_params:
247
+ for param in display_params:
248
+ flags = ", ".join(param.opts)
249
+ if "--help" in flags:
250
+ continue
251
+ option_flags.append(flags)
252
+
253
+ help_flags = "--help, -h"
254
+ all_option_flags = [help_flags] + option_flags
255
+ all_display_names = [cmd["display_name"] for cmd in commands]
256
+ all_labels = all_display_names + all_option_flags
257
+ longest_label_length = max((len(label) for label in all_labels), default=0)
258
+
259
+ console.print(f"[bold {COLOR.SECONDARY}]Options:")
260
+ help_padding = " " * (longest_label_length - len(help_flags) + SPACER_LENGTH)
261
+ console.print(
262
+ f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
263
+ )
264
+
265
+ if display_params:
266
+ for param in display_params:
267
+ flags = ", ".join(param.opts)
268
+ if "--help" in flags:
269
+ continue
270
+ description = _get_option_description(param)
271
+ padding = " " * (longest_label_length - len(flags) + SPACER_LENGTH)
272
+ console.print(
273
+ f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
274
+ )
275
+ console.print()
276
+
277
+ console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
278
+
279
+ for cmd in commands:
280
+ display_name = cmd["display_name"]
281
+ padding = " " * (longest_label_length - len(display_name) + SPACER_LENGTH)
282
+ console.print(
283
+ f" [{COLOR.COMMAND}]{display_name}[/{COLOR.COMMAND}]{padding}{cmd['help']}"
284
+ )
285
+
286
+ console.print()
287
+
288
+
289
+ def _get_alias_registry(app: typer.Typer) -> dict[str, list[str]]:
290
+ registry = getattr(app, "_usecli_aliases", {})
291
+ return registry if isinstance(registry, dict) else {}
292
+
293
+
294
+ def _get_option_description(param: click.Parameter) -> str:
295
+ if "--show-completion" in param.opts:
296
+ return "Show completion for the current shell."
297
+ return getattr(param, "help", "") or ""
298
+
299
+
300
+ def _order_completion_params(
301
+ params: list[click.Parameter],
302
+ ) -> list[click.Parameter]:
303
+ ordered = list(params)
304
+ install_index = None
305
+ show_index = None
306
+ for index, param in enumerate(ordered):
307
+ opts = set(param.opts)
308
+ if "--install-completion" in opts:
309
+ install_index = index
310
+ if "--show-completion" in opts:
311
+ show_index = index
312
+ if install_index is None or show_index is None:
313
+ return ordered
314
+ if install_index < show_index:
315
+ show_param = ordered.pop(show_index)
316
+ ordered.insert(install_index, show_param)
317
+ return ordered
318
+ if show_index < install_index:
319
+ install_param = ordered.pop(install_index)
320
+ ordered.insert(show_index, install_param)
321
+ return ordered
322
+
323
+
324
+ def _build_alias_to_primary(alias_registry: dict[str, list[str]]) -> dict[str, str]:
325
+ alias_to_primary: dict[str, str] = {}
326
+ for primary, aliases in alias_registry.items():
327
+ alias_to_primary[primary] = primary
328
+ for alias in aliases:
329
+ alias_to_primary[alias] = primary
330
+ return alias_to_primary
331
+
332
+
333
+ def _format_display_name(name: str, aliases: list[str]) -> str:
334
+ if not aliases:
335
+ return name
336
+ return f"{name}, {', '.join(aliases)}"
@@ -12,6 +12,9 @@ class {{ class_name }}(BaseCommand):
12
12
  def description(self) -> str:
13
13
  return "Description for {{ command_name }} command"
14
14
 
15
+ def aliases(self) -> list[str]:
16
+ return [{{ command_name[:3] }}]
17
+
15
18
  def handle(
16
19
  self,
17
20
  name: str = Argument(..., help="An example argument"),
@@ -1,217 +0,0 @@
1
- """Command listing utilities for usecli CLI."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- import click
8
- import typer
9
- from rich.console import Console
10
-
11
- from usecli.cli.config.colors import COLOR
12
- from usecli.cli.core.ui.title import (
13
- get_project_name,
14
- get_script_command_name,
15
- print_title,
16
- )
17
-
18
- if TYPE_CHECKING:
19
- pass
20
-
21
- console = Console()
22
-
23
- SPACER_LENGTH = 18
24
-
25
-
26
- def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
27
- """List all available commands with optional filtering.
28
-
29
- Displays commands in a formatted list with sections for grouped commands
30
- (those with colons in their names).
31
-
32
- Args:
33
- app: The Typer application instance.
34
- prefix_filter: Optional prefix to filter commands by name.
35
- """
36
- project_name = get_project_name()
37
- print_title(title=project_name)
38
-
39
- click_group = typer.main.get_command(app)
40
-
41
- all_command_names = list({cmd.name for cmd in app.registered_commands if cmd.name})
42
- longest_name_length = (
43
- max(len(name) for name in all_command_names) if all_command_names else 0
44
- )
45
-
46
- command_name = get_script_command_name(default="usecli")
47
-
48
- console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
49
- console.print(f" [{COLOR.PRIMARY}]{command_name} [OPTIONS] [ARGUMENTS]")
50
- console.print()
51
-
52
- console.print(f"[bold {COLOR.SECONDARY}]Options:")
53
-
54
- help_flags = "--help, -h"
55
- help_padding = " " * (longest_name_length - len(help_flags) + SPACER_LENGTH)
56
- console.print(
57
- f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
58
- )
59
-
60
- if click_group.params:
61
- for param in click_group.params:
62
- flags = ", ".join(param.opts)
63
- if "--help" in flags:
64
- continue
65
- description = getattr(param, "help", "") or ""
66
- padding = " " * (longest_name_length - len(flags) + SPACER_LENGTH)
67
- console.print(
68
- f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
69
- )
70
- console.print()
71
-
72
- commands_by_name: dict[str, str] = {}
73
- for command in app.registered_commands:
74
- callback = command.callback
75
- name = command.name or (
76
- getattr(callback, "__name__", "unknown") if callback else "unknown"
77
- )
78
- help_text = command.help or ""
79
- if name not in commands_by_name or (not commands_by_name[name] and help_text):
80
- commands_by_name[name] = help_text
81
-
82
- commands = [
83
- {"name": name, "help": help_text}
84
- for name, help_text in commands_by_name.items()
85
- ]
86
- commands.sort(key=lambda x: x["name"])
87
-
88
- if prefix_filter:
89
- filtered = [c for c in commands if c["name"].startswith(prefix_filter)]
90
- if not filtered:
91
- console.print(f" [dim]No commands found for '{prefix_filter}'[/dim]")
92
- console.print()
93
- return
94
- commands = filtered
95
-
96
- if not prefix_filter:
97
- console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
98
-
99
- groups: dict[str, str] = {}
100
- if isinstance(click_group, click.Group):
101
- for cmd_name, cmd_obj in click_group.commands.items():
102
- if isinstance(cmd_obj, click.Group):
103
- groups[cmd_name] = (
104
- getattr(cmd_obj, "help", f"Commands for {cmd_name}")
105
- or f"Commands for {cmd_name}"
106
- )
107
-
108
- top_level = [
109
- c for c in commands if ":" not in c["name"] and c["name"] not in groups
110
- ]
111
- with_colon = [c for c in commands if ":" in c["name"]]
112
-
113
- all_names = all_command_names + list(groups.keys())
114
- if all_names:
115
- longest_name_length = max(len(name) for name in all_names)
116
-
117
- def print_command(cmd: dict[str, str]) -> None:
118
- """Print a single command with proper formatting."""
119
- padding = " " * (longest_name_length - len(cmd["name"]) + SPACER_LENGTH)
120
- console.print(
121
- f" [{COLOR.COMMAND} not bold]{cmd['name']}[/{COLOR.COMMAND} not bold]{padding}{cmd['help']}"
122
- )
123
-
124
- for group_name, group_help in groups.items():
125
- top_level.append({"name": group_name, "help": group_help})
126
-
127
- top_level.sort(key=lambda x: x["name"])
128
-
129
- if top_level:
130
- for cmd in top_level:
131
- print_command(cmd)
132
- console.print()
133
-
134
- sections: dict[str, list[dict[str, str]]] = {}
135
- for cmd in with_colon:
136
- section_prefix = cmd["name"].split(":")[0]
137
- if section_prefix not in sections:
138
- sections[section_prefix] = []
139
- sections[section_prefix].append(cmd)
140
-
141
- for section_prefix, section_cmds in sections.items():
142
- console.print(f"[bold {COLOR.SECONDARY}]{section_prefix}:")
143
- for cmd in section_cmds:
144
- print_command(cmd)
145
- console.print()
146
-
147
-
148
- def list_group_commands(group_app: typer.Typer, group_name: str) -> None:
149
- """List all commands within a specific command group.
150
-
151
- Displays commands in a formatted list for a nested command group,
152
- similar to how list_commands works for the main app.
153
-
154
- Args:
155
- group_app: The Typer sub-app for the command group.
156
- group_name: The name of the command group.
157
- """
158
- all_command_names = list(
159
- {cmd.name for cmd in group_app.registered_commands if cmd.name}
160
- )
161
- longest_name_length = (
162
- max(len(name) for name in all_command_names) if all_command_names else 0
163
- )
164
-
165
- command_name = get_script_command_name(default="usecli")
166
-
167
- console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
168
- console.print(
169
- f" [{COLOR.PRIMARY}]{command_name} {group_name} [COMMAND] [OPTIONS][/]"
170
- )
171
- console.print()
172
-
173
- console.print(f"[bold {COLOR.SECONDARY}]Options:")
174
- help_flags = "--help, -h"
175
- help_padding = " " * (longest_name_length - len(help_flags) + SPACER_LENGTH)
176
- console.print(
177
- f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
178
- )
179
-
180
- click_group = typer.main.get_command(group_app)
181
- if click_group.params:
182
- for param in click_group.params:
183
- flags = ", ".join(param.opts)
184
- if "--help" in flags:
185
- continue
186
- description = getattr(param, "help", "") or ""
187
- padding = " " * (longest_name_length - len(flags) + SPACER_LENGTH)
188
- console.print(
189
- f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
190
- )
191
- console.print()
192
-
193
- commands_by_name: dict[str, str] = {}
194
- for command in group_app.registered_commands:
195
- callback = command.callback
196
- name = command.name or (
197
- getattr(callback, "__name__", "unknown") if callback else "unknown"
198
- )
199
- help_text = command.help or ""
200
- if name not in commands_by_name or (not commands_by_name[name] and help_text):
201
- commands_by_name[name] = help_text
202
-
203
- commands = [
204
- {"name": name, "help": help_text}
205
- for name, help_text in commands_by_name.items()
206
- ]
207
- commands.sort(key=lambda x: x["name"])
208
-
209
- console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
210
-
211
- for cmd in commands:
212
- padding = " " * (longest_name_length - len(cmd["name"]) + SPACER_LENGTH)
213
- console.print(
214
- f" [{COLOR.COMMAND}]{cmd['name']}[/{COLOR.COMMAND}]{padding}{cmd['help']}"
215
- )
216
-
217
- console.print()
File without changes
File without changes
File without changes
File without changes