usecli 0.1.30__tar.gz → 0.1.32__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.30 → usecli-0.1.32}/PKG-INFO +1 -1
  2. {usecli-0.1.30 → usecli-0.1.32}/pyproject.toml +1 -1
  3. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/__init__.py +36 -1
  4. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +26 -3
  5. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/base_command.py +165 -9
  6. usecli-0.1.32/src/usecli/cli/core/ui/list.py +362 -0
  7. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/command.py.j2 +3 -0
  8. usecli-0.1.30/src/usecli/cli/core/ui/list.py +0 -217
  9. {usecli-0.1.30 → usecli-0.1.32}/LICENSE +0 -0
  10. {usecli-0.1.30 → usecli-0.1.32}/README.md +0 -0
  11. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/__init__.py +0 -0
  12. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/README.md +0 -0
  13. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/__init__.py +0 -0
  14. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/custom/README.md +0 -0
  15. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/custom/__init__.py +0 -0
  16. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  17. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  18. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  19. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  20. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  21. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  22. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  23. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  24. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  25. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  26. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  27. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/init_command.py +0 -0
  28. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/config/__init__.py +0 -0
  29. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/config/colors.py +0 -0
  30. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/__init__.py +0 -0
  31. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/__init__.py +0 -0
  32. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/handler.py +0 -0
  33. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/utils.py +0 -0
  34. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  35. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/base.py +0 -0
  36. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/config.py +0 -0
  37. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/usage.py +0 -0
  38. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/validation.py +0 -0
  39. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/skill_generator.py +0 -0
  40. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/ui/__init__.py +0 -0
  41. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/ui/title.py +0 -0
  42. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/__init__.py +0 -0
  43. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/network.py +0 -0
  44. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/numeric.py +0 -0
  45. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/path.py +0 -0
  46. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/string.py +0 -0
  47. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/services/__init__.py +0 -0
  48. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/services/command_service.py +0 -0
  49. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/usecli.toml.j2 +0 -0
  51. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/params.py +0 -0
  66. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/manager.py +0 -0
  70. {usecli-0.1.30 → usecli-0.1.32}/src/usecli/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.30
3
+ Version: 0.1.32
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.30"
3
+ version = "0.1.32"
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" }]
@@ -91,6 +91,20 @@ def _get_cli_help_text() -> str:
91
91
  return f"{display_name} - {display_description}"
92
92
 
93
93
 
94
+ def _get_group_alias_registry(app: typer.Typer) -> dict[str, list[str]]:
95
+ registry = getattr(app, "_usecli_group_aliases", {})
96
+ return registry if isinstance(registry, dict) else {}
97
+
98
+
99
+ def _build_alias_to_primary(alias_registry: dict[str, list[str]]) -> dict[str, str]:
100
+ alias_to_primary: dict[str, str] = {}
101
+ for primary, aliases in alias_registry.items():
102
+ alias_to_primary[primary] = primary
103
+ for alias in aliases:
104
+ alias_to_primary[alias] = primary
105
+ return alias_to_primary
106
+
107
+
94
108
  class PrefixMatchingGroup(TyperGroup):
95
109
  """Custom Typer group that supports prefix matching for commands.
96
110
 
@@ -111,12 +125,33 @@ class PrefixMatchingGroup(TyperGroup):
111
125
  if rv is not None:
112
126
  return rv
113
127
 
128
+ group_alias_registry = _get_group_alias_registry(app)
129
+ group_alias_to_primary = _build_alias_to_primary(group_alias_registry)
130
+
131
+ if (
132
+ cmd_name in group_alias_to_primary
133
+ and group_alias_to_primary[cmd_name] != cmd_name
134
+ ):
135
+ return TyperGroup.get_command(self, ctx, group_alias_to_primary[cmd_name])
136
+
114
137
  matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)]
138
+ group_aliases = [
139
+ alias for aliases in group_alias_registry.values() for alias in aliases
140
+ ]
141
+ matches.extend([alias for alias in group_aliases if alias.startswith(cmd_name)])
142
+ matches = list(dict.fromkeys(matches))
115
143
 
116
144
  if not matches:
117
145
  return None
118
146
 
119
147
  if cmd_name in matches:
148
+ if (
149
+ cmd_name in group_alias_to_primary
150
+ and group_alias_to_primary[cmd_name] != cmd_name
151
+ ):
152
+ return TyperGroup.get_command(
153
+ self, ctx, group_alias_to_primary[cmd_name]
154
+ )
120
155
  return TyperGroup.get_command(self, ctx, cmd_name)
121
156
 
122
157
  return FilteredListCommand(cmd_name)
@@ -192,7 +227,7 @@ def run_app(
192
227
  ),
193
228
  help: bool = typer.Option(None, "--help", "-h", is_eager=True),
194
229
  interactive: bool = typer.Option(
195
- False, "--interactive", "-i", help="Run in interactive mode", is_eager=True
230
+ False, "--interactive", "-i", help="Run in interactive mode.", is_eager=True
196
231
  ),
197
232
  ) -> None:
198
233
  """Main callback for the CLI application.
@@ -155,11 +155,27 @@ def _run_fzf_menu(
155
155
  )
156
156
 
157
157
 
158
- def _get_group_subcommands(group_name: str) -> list[dict[str, Any]]:
158
+ def _resolve_group_alias(app: typer.Typer, group_name: str) -> str:
159
+ registry = getattr(app, "_usecli_group_aliases", {})
160
+ if not isinstance(registry, dict):
161
+ return group_name
162
+ for primary, aliases in registry.items():
163
+ if group_name == primary:
164
+ return primary
165
+ if group_name in aliases:
166
+ return primary
167
+ return group_name
168
+
169
+
170
+ def _get_group_subcommands(
171
+ app: typer.Typer,
172
+ group_name: str,
173
+ ) -> list[dict[str, Any]]:
159
174
  from usecli.cli.core.base_command import NestedCommandRegistry
160
175
 
161
176
  registry = NestedCommandRegistry()
162
- group_app = registry._groups.get(group_name)
177
+ resolved_group = _resolve_group_alias(app, group_name)
178
+ group_app = registry._groups.get(resolved_group)
163
179
 
164
180
  if not group_app:
165
181
  return []
@@ -260,12 +276,19 @@ def run_interactive(
260
276
  selected_cmd = next(
261
277
  (c for c in ordered_commands if c["name"] == cmd_name), None
262
278
  )
279
+ if not selected_cmd:
280
+ resolved_group = _resolve_group_alias(app, cmd_name)
281
+ if resolved_group != cmd_name:
282
+ cmd_name = resolved_group
283
+ selected_cmd = next(
284
+ (c for c in ordered_commands if c["name"] == cmd_name), None
285
+ )
263
286
  if not selected_cmd:
264
287
  ErrorHandler.display_error(f"Unknown command '{cmd_name}'")
265
288
  raise typer.Exit(code=1)
266
289
 
267
290
  if selected_cmd and selected_cmd.get("is_group"):
268
- subcommands = _get_group_subcommands(cmd_name)
291
+ subcommands = _get_group_subcommands(app, cmd_name)
269
292
  if not subcommands:
270
293
  ErrorHandler.display_error(f"No subcommands found for '{cmd_name}'")
271
294
  raise typer.Exit(code=1)
@@ -89,7 +89,7 @@ class NestedCommandRegistry:
89
89
  False,
90
90
  "--interactive",
91
91
  "-i",
92
- help="Run in interactive mode",
92
+ help="Run in interactive mode.",
93
93
  ),
94
94
  ) -> None:
95
95
  """Callback for the command group."""
@@ -182,7 +182,7 @@ class CustomHelpCommand(TyperCommand):
182
182
  Option(
183
183
  ["--interactive", "-i"],
184
184
  is_flag=True,
185
- help="Run in interactive mode",
185
+ help="Run in interactive mode.",
186
186
  )
187
187
  )
188
188
 
@@ -314,6 +314,9 @@ class BaseCommand(ABC):
314
314
  """
315
315
  pass
316
316
 
317
+ def aliases(self) -> list[str]:
318
+ return []
319
+
317
320
  def visible(self) -> bool:
318
321
  return True
319
322
 
@@ -323,6 +326,7 @@ class BaseCommand(ABC):
323
326
 
324
327
  signature = self.signature()
325
328
  signature_parts = signature.split()
329
+ aliases = self.aliases()
326
330
 
327
331
  # Check if this is a space-separated nested command signature (e.g., "spec show")
328
332
  # vs a command with argument placeholders (e.g., "test-cmd <name>")
@@ -340,21 +344,173 @@ class BaseCommand(ABC):
340
344
  registry = NestedCommandRegistry()
341
345
  group_app = registry.get_or_create_group(self.app, group_name)
342
346
 
343
- cmd_decorator = group_app.command(
347
+ normalized_aliases, group_aliases = self._normalize_nested_aliases(
348
+ primary_name=command_name,
349
+ aliases=aliases,
350
+ group_name=group_name,
351
+ )
352
+ self._register_with_aliases(
353
+ app=group_app,
344
354
  name=command_name,
345
- help=self.description(),
346
- cls=CustomHelpCommand,
355
+ description=self.description(),
356
+ aliases=normalized_aliases,
357
+ )
358
+ self._register_group_aliases(
359
+ app=self.app,
360
+ group_name=group_name,
361
+ aliases=group_aliases,
347
362
  )
348
- cmd_decorator(self.handle)
349
363
  else:
350
364
  # Single-level command (e.g., "help", "init", "config:set", "make:command")
351
365
  name = signature_parts[0]
352
- cmd_decorator = self.app.command(
366
+ normalized_aliases = self._normalize_aliases(
367
+ primary_name=name,
368
+ aliases=aliases,
369
+ group_name=None,
370
+ )
371
+ self._register_with_aliases(
372
+ app=self.app,
353
373
  name=name,
354
- help=self.description(),
374
+ description=self.description(),
375
+ aliases=normalized_aliases,
376
+ )
377
+
378
+ def _register_with_aliases(
379
+ self,
380
+ app: typer.Typer,
381
+ name: str,
382
+ description: str,
383
+ aliases: list[str],
384
+ ) -> None:
385
+ cmd_decorator = app.command(
386
+ name=name,
387
+ help=description,
388
+ cls=CustomHelpCommand,
389
+ )
390
+ cmd_decorator(self.handle)
391
+
392
+ if not aliases:
393
+ return
394
+
395
+ alias_registry = self._get_alias_registry(app)
396
+ alias_registry.setdefault(name, [])
397
+ for alias in aliases:
398
+ if alias == name or alias in alias_registry[name]:
399
+ continue
400
+ alias_registry[name].append(alias)
401
+ alias_decorator = app.command(
402
+ name=alias,
403
+ help=description,
355
404
  cls=CustomHelpCommand,
356
405
  )
357
- cmd_decorator(self.handle)
406
+ alias_decorator(self.handle)
407
+
408
+ def _get_alias_registry(self, app: typer.Typer) -> dict[str, list[str]]:
409
+ if not hasattr(app, "_usecli_aliases"):
410
+ setattr(app, "_usecli_aliases", {})
411
+ registry = getattr(app, "_usecli_aliases")
412
+ if not isinstance(registry, dict):
413
+ registry = {}
414
+ setattr(app, "_usecli_aliases", registry)
415
+ return registry
416
+
417
+ def _get_group_alias_registry(self, app: typer.Typer) -> dict[str, list[str]]:
418
+ if not hasattr(app, "_usecli_group_aliases"):
419
+ setattr(app, "_usecli_group_aliases", {})
420
+ registry = getattr(app, "_usecli_group_aliases")
421
+ if not isinstance(registry, dict):
422
+ registry = {}
423
+ setattr(app, "_usecli_group_aliases", registry)
424
+ return registry
425
+
426
+ def _register_group_aliases(
427
+ self,
428
+ app: typer.Typer,
429
+ group_name: str,
430
+ aliases: list[str],
431
+ ) -> None:
432
+ if not aliases:
433
+ return
434
+
435
+ registry = self._get_group_alias_registry(app)
436
+ registry.setdefault(group_name, [])
437
+ for alias in aliases:
438
+ if alias == group_name or alias in registry[group_name]:
439
+ continue
440
+ registry[group_name].append(alias)
441
+
442
+ def _normalize_aliases(
443
+ self,
444
+ primary_name: str,
445
+ aliases: list[str],
446
+ group_name: str | None,
447
+ ) -> list[str]:
448
+ normalized: list[str] = []
449
+ for alias in aliases:
450
+ alias_parts = alias.split()
451
+ if group_name:
452
+ if len(alias_parts) == 2 and alias_parts[0] == group_name:
453
+ alias_name = alias_parts[1]
454
+ elif len(alias_parts) == 1:
455
+ alias_name = alias_parts[0]
456
+ else:
457
+ continue
458
+ if not self._is_valid_subcommand_name(alias_name):
459
+ continue
460
+ else:
461
+ if len(alias_parts) != 1:
462
+ continue
463
+ alias_name = alias_parts[0]
464
+
465
+ if alias_name == primary_name or alias_name in normalized:
466
+ continue
467
+ normalized.append(alias_name)
468
+ return normalized
469
+
470
+ def _normalize_nested_aliases(
471
+ self,
472
+ primary_name: str,
473
+ aliases: list[str],
474
+ group_name: str,
475
+ ) -> tuple[list[str], list[str]]:
476
+ normalized: list[str] = []
477
+ group_aliases: list[str] = []
478
+
479
+ for alias in aliases:
480
+ alias_parts = alias.split()
481
+ if len(alias_parts) == 1:
482
+ alias_name = alias_parts[0]
483
+ if not self._is_valid_subcommand_name(alias_name):
484
+ continue
485
+ if alias_name == primary_name or alias_name in normalized:
486
+ continue
487
+ normalized.append(alias_name)
488
+ continue
489
+
490
+ if len(alias_parts) != 2:
491
+ continue
492
+
493
+ group_alias, alias_name = alias_parts
494
+
495
+ if not self._is_valid_subcommand_name(group_alias):
496
+ continue
497
+ if not self._is_valid_subcommand_name(alias_name):
498
+ continue
499
+
500
+ if group_alias == group_name:
501
+ if alias_name == primary_name or alias_name in normalized:
502
+ continue
503
+ normalized.append(alias_name)
504
+ continue
505
+
506
+ if group_alias not in group_aliases:
507
+ group_aliases.append(group_alias)
508
+
509
+ if alias_name == primary_name or alias_name in normalized:
510
+ continue
511
+ normalized.append(alias_name)
512
+
513
+ return normalized, group_aliases
358
514
 
359
515
  def _is_valid_subcommand_name(self, name: str) -> bool:
360
516
  """Check if a string is a valid subcommand name (not an argument placeholder).
@@ -0,0 +1,362 @@
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
+ group_alias_registry = _get_group_alias_registry(app)
56
+ group_alias_to_primary = _build_alias_to_primary(group_alias_registry)
57
+
58
+ command_name = get_script_command_name(default="usecli")
59
+
60
+ commands_by_name: dict[str, CommandMeta] = {}
61
+ for command in app.registered_commands:
62
+ callback = command.callback
63
+ name = command.name or (
64
+ getattr(callback, "__name__", "unknown") if callback else "unknown"
65
+ )
66
+ if name in alias_to_primary and alias_to_primary[name] != name:
67
+ continue
68
+ help_text = command.help or ""
69
+ if name not in commands_by_name or (
70
+ not commands_by_name[name]["help"] and help_text
71
+ ):
72
+ commands_by_name[name] = {
73
+ "help": help_text,
74
+ "aliases": alias_registry.get(name, list[str]()),
75
+ }
76
+
77
+ commands: list[CommandEntry] = []
78
+ for name in commands_by_name:
79
+ aliases = commands_by_name[name]["aliases"]
80
+ command_entry: CommandEntry = {
81
+ "name": name,
82
+ "display_name": _format_display_name(name, aliases),
83
+ "help": commands_by_name[name]["help"],
84
+ "aliases": aliases,
85
+ }
86
+ commands.append(command_entry)
87
+ commands.sort(key=lambda x: x["name"])
88
+
89
+ groups: dict[str, str] = {}
90
+ if isinstance(click_group, click.Group):
91
+ for cmd_name, cmd_obj in click_group.commands.items():
92
+ if isinstance(cmd_obj, click.Group):
93
+ if (
94
+ cmd_name in group_alias_to_primary
95
+ and group_alias_to_primary[cmd_name] != cmd_name
96
+ ):
97
+ continue
98
+ groups[cmd_name] = (
99
+ getattr(cmd_obj, "help", f"Commands for {cmd_name}")
100
+ or f"Commands for {cmd_name}"
101
+ )
102
+
103
+ option_flags = []
104
+ display_params = _order_completion_params(click_group.params or [])
105
+ if display_params:
106
+ for param in display_params:
107
+ flags = ", ".join(param.opts)
108
+ if "--help" in flags:
109
+ continue
110
+ option_flags.append(flags)
111
+
112
+ help_flags = "--help, -h"
113
+ all_option_flags = [help_flags] + option_flags
114
+ group_entries: list[CommandEntry] = []
115
+ for group_name, group_help in groups.items():
116
+ aliases = group_alias_registry.get(group_name, list[str]())
117
+ group_entries.append(
118
+ {
119
+ "name": group_name,
120
+ "display_name": _format_display_name(group_name, aliases),
121
+ "help": group_help,
122
+ "aliases": aliases,
123
+ }
124
+ )
125
+
126
+ if prefix_filter:
127
+ filtered = [
128
+ c
129
+ for c in commands
130
+ if c["name"].startswith(prefix_filter)
131
+ or any(alias.startswith(prefix_filter) for alias in c["aliases"])
132
+ ]
133
+ filtered_groups = [
134
+ g
135
+ for g in group_entries
136
+ if g["name"].startswith(prefix_filter)
137
+ or any(alias.startswith(prefix_filter) for alias in g["aliases"])
138
+ ]
139
+ if not filtered and not filtered_groups:
140
+ console.print(f" [dim]No commands found for '{prefix_filter}'[/dim]")
141
+ console.print()
142
+ return
143
+ commands = filtered
144
+ group_entries = filtered_groups
145
+
146
+ all_display_names = [cmd["display_name"] for cmd in commands] + [
147
+ entry["display_name"] for entry in group_entries
148
+ ]
149
+ all_labels = all_display_names + all_option_flags
150
+ longest_label_length = max((len(label) for label in all_labels), default=0)
151
+
152
+ console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
153
+ console.print(f" [{COLOR.PRIMARY}]{command_name} [OPTIONS] [ARGUMENTS]")
154
+ console.print()
155
+
156
+ console.print(f"[bold {COLOR.SECONDARY}]Options:")
157
+
158
+ help_padding = " " * (longest_label_length - len(help_flags) + SPACER_LENGTH)
159
+ console.print(
160
+ f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
161
+ )
162
+
163
+ if display_params:
164
+ for param in display_params:
165
+ flags = ", ".join(param.opts)
166
+ if "--help" in flags:
167
+ continue
168
+ description = _get_option_description(param)
169
+ padding = " " * (longest_label_length - len(flags) + SPACER_LENGTH)
170
+ console.print(
171
+ f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
172
+ )
173
+ console.print()
174
+
175
+ if not prefix_filter:
176
+ console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
177
+
178
+ group_names = set(groups.keys())
179
+ top_level: list[CommandEntry] = [
180
+ c for c in commands if ":" not in c["name"] and c["name"] not in group_names
181
+ ]
182
+ with_colon: list[CommandEntry] = [c for c in commands if ":" in c["name"]]
183
+
184
+ def print_command(cmd: CommandEntry) -> None:
185
+ """Print a single command with proper formatting."""
186
+ display_name = cmd["display_name"]
187
+ padding = " " * (longest_label_length - len(display_name) + SPACER_LENGTH)
188
+ console.print(
189
+ f" [{COLOR.COMMAND} not bold]{display_name}[/{COLOR.COMMAND} not bold]{padding}{cmd['help']}"
190
+ )
191
+
192
+ top_level.extend(group_entries)
193
+
194
+ top_level.sort(key=lambda x: x["name"])
195
+
196
+ if top_level:
197
+ for cmd in top_level:
198
+ print_command(cmd)
199
+ console.print()
200
+
201
+ sections: dict[str, list[CommandEntry]] = {}
202
+ for cmd in with_colon:
203
+ section_prefix = cmd["name"].split(":")[0]
204
+ if section_prefix not in sections:
205
+ sections[section_prefix] = []
206
+ sections[section_prefix].append(cmd)
207
+
208
+ for section_prefix, section_cmds in sections.items():
209
+ console.print(f"[bold {COLOR.SECONDARY}]{section_prefix}:")
210
+ for cmd in section_cmds:
211
+ print_command(cmd)
212
+ console.print()
213
+
214
+
215
+ def list_group_commands(group_app: typer.Typer, group_name: str) -> None:
216
+ """List all commands within a specific command group.
217
+
218
+ Displays commands in a formatted list for a nested command group,
219
+ similar to how list_commands works for the main app.
220
+
221
+ Args:
222
+ group_app: The Typer sub-app for the command group.
223
+ group_name: The name of the command group.
224
+ """
225
+ alias_registry = _get_alias_registry(group_app)
226
+ alias_to_primary = _build_alias_to_primary(alias_registry)
227
+
228
+ command_name = get_script_command_name(default="usecli")
229
+
230
+ console.print(f"[bold {COLOR.SECONDARY}]Usage:[/bold {COLOR.SECONDARY}]")
231
+ console.print(
232
+ f" [{COLOR.PRIMARY}]{command_name} {group_name} [COMMAND] [OPTIONS][/]"
233
+ )
234
+ console.print()
235
+ commands_by_name: dict[str, CommandMeta] = {}
236
+ for command in group_app.registered_commands:
237
+ callback = command.callback
238
+ name = command.name or (
239
+ getattr(callback, "__name__", "unknown") if callback else "unknown"
240
+ )
241
+ if name in alias_to_primary and alias_to_primary[name] != name:
242
+ continue
243
+ help_text = command.help or ""
244
+ if name not in commands_by_name or (
245
+ not commands_by_name[name]["help"] and help_text
246
+ ):
247
+ commands_by_name[name] = {
248
+ "help": help_text,
249
+ "aliases": alias_registry.get(name, list[str]()),
250
+ }
251
+
252
+ commands: list[CommandEntry] = []
253
+ for name in commands_by_name:
254
+ aliases = commands_by_name[name]["aliases"]
255
+ command_entry: CommandEntry = {
256
+ "name": name,
257
+ "display_name": _format_display_name(name, aliases),
258
+ "help": commands_by_name[name]["help"],
259
+ "aliases": aliases,
260
+ }
261
+ commands.append(command_entry)
262
+ commands.sort(key=lambda x: x["name"])
263
+
264
+ click_group = typer.main.get_command(group_app)
265
+ option_flags = []
266
+ display_params = _order_completion_params(click_group.params or [])
267
+ if display_params:
268
+ for param in display_params:
269
+ flags = ", ".join(param.opts)
270
+ if "--help" in flags:
271
+ continue
272
+ option_flags.append(flags)
273
+
274
+ help_flags = "--help, -h"
275
+ all_option_flags = [help_flags] + option_flags
276
+ all_display_names = [cmd["display_name"] for cmd in commands]
277
+ all_labels = all_display_names + all_option_flags
278
+ longest_label_length = max((len(label) for label in all_labels), default=0)
279
+
280
+ console.print(f"[bold {COLOR.SECONDARY}]Options:")
281
+ help_padding = " " * (longest_label_length - len(help_flags) + SPACER_LENGTH)
282
+ console.print(
283
+ f" [{COLOR.OPTION}]{help_flags}[/{COLOR.OPTION}]{help_padding}Show this message and exit."
284
+ )
285
+
286
+ if display_params:
287
+ for param in display_params:
288
+ flags = ", ".join(param.opts)
289
+ if "--help" in flags:
290
+ continue
291
+ description = _get_option_description(param)
292
+ padding = " " * (longest_label_length - len(flags) + SPACER_LENGTH)
293
+ console.print(
294
+ f" [{COLOR.OPTION}]{flags}[/{COLOR.OPTION}]{padding}{description}"
295
+ )
296
+ console.print()
297
+
298
+ console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
299
+
300
+ for cmd in commands:
301
+ display_name = cmd["display_name"]
302
+ padding = " " * (longest_label_length - len(display_name) + SPACER_LENGTH)
303
+ console.print(
304
+ f" [{COLOR.COMMAND}]{display_name}[/{COLOR.COMMAND}]{padding}{cmd['help']}"
305
+ )
306
+
307
+ console.print()
308
+
309
+
310
+ def _get_alias_registry(app: typer.Typer) -> dict[str, list[str]]:
311
+ registry = getattr(app, "_usecli_aliases", {})
312
+ return registry if isinstance(registry, dict) else {}
313
+
314
+
315
+ def _get_group_alias_registry(app: typer.Typer) -> dict[str, list[str]]:
316
+ registry = getattr(app, "_usecli_group_aliases", {})
317
+ return registry if isinstance(registry, dict) else {}
318
+
319
+
320
+ def _get_option_description(param: click.Parameter) -> str:
321
+ if "--show-completion" in param.opts:
322
+ return "Show completion for the current shell."
323
+ return getattr(param, "help", "") or ""
324
+
325
+
326
+ def _order_completion_params(
327
+ params: list[click.Parameter],
328
+ ) -> list[click.Parameter]:
329
+ ordered = list(params)
330
+ install_index = None
331
+ show_index = None
332
+ for index, param in enumerate(ordered):
333
+ opts = set(param.opts)
334
+ if "--install-completion" in opts:
335
+ install_index = index
336
+ if "--show-completion" in opts:
337
+ show_index = index
338
+ if install_index is None or show_index is None:
339
+ return ordered
340
+ if install_index < show_index:
341
+ show_param = ordered.pop(show_index)
342
+ ordered.insert(install_index, show_param)
343
+ return ordered
344
+ if show_index < install_index:
345
+ install_param = ordered.pop(install_index)
346
+ ordered.insert(show_index, install_param)
347
+ return ordered
348
+
349
+
350
+ def _build_alias_to_primary(alias_registry: dict[str, list[str]]) -> dict[str, str]:
351
+ alias_to_primary: dict[str, str] = {}
352
+ for primary, aliases in alias_registry.items():
353
+ alias_to_primary[primary] = primary
354
+ for alias in aliases:
355
+ alias_to_primary[alias] = primary
356
+ return alias_to_primary
357
+
358
+
359
+ def _format_display_name(name: str, aliases: list[str]) -> str:
360
+ if not aliases:
361
+ return name
362
+ 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
File without changes