usecli 0.1.31__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 (69) hide show
  1. {usecli-0.1.31 → usecli-0.1.32}/PKG-INFO +1 -1
  2. {usecli-0.1.31 → usecli-0.1.32}/pyproject.toml +1 -1
  3. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/__init__.py +35 -0
  4. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +26 -3
  5. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/base_command.py +76 -1
  6. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/ui/list.py +50 -24
  7. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/templates/command.py.j2 +2 -2
  8. {usecli-0.1.31 → usecli-0.1.32}/LICENSE +0 -0
  9. {usecli-0.1.31 → usecli-0.1.32}/README.md +0 -0
  10. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/__init__.py +0 -0
  11. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/README.md +0 -0
  12. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/__init__.py +0 -0
  13. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/custom/README.md +0 -0
  14. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/custom/__init__.py +0 -0
  15. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  16. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  17. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  18. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  19. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  20. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  21. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  22. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  23. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  24. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  25. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  26. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/commands/init_command.py +0 -0
  27. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/config/__init__.py +0 -0
  28. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/config/colors.py +0 -0
  29. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/__init__.py +0 -0
  30. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/error/__init__.py +0 -0
  31. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/error/handler.py +0 -0
  32. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/error/utils.py +0 -0
  33. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  34. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/exceptions/base.py +0 -0
  35. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/exceptions/config.py +0 -0
  36. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/exceptions/usage.py +0 -0
  37. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/exceptions/validation.py +0 -0
  38. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/skill_generator.py +0 -0
  39. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/ui/__init__.py +0 -0
  40. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  49. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/templates/usecli.toml.j2 +0 -0
  50. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  51. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  52. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  53. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  54. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  55. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/default.toml +0 -0
  56. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/dracula.toml +0 -0
  57. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  58. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/nord.toml +0 -0
  59. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  60. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/utils/__init__.py +0 -0
  61. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  62. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  63. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/menu.py +0 -0
  64. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/params.py +0 -0
  65. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/shared/__init__.py +0 -0
  66. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/shared/config/__init__.py +0 -0
  67. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/shared/config/globals.py +0 -0
  68. {usecli-0.1.31 → usecli-0.1.32}/src/usecli/shared/config/manager.py +0 -0
  69. {usecli-0.1.31 → 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.31
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.31"
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)
@@ -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)
@@ -344,7 +344,7 @@ class BaseCommand(ABC):
344
344
  registry = NestedCommandRegistry()
345
345
  group_app = registry.get_or_create_group(self.app, group_name)
346
346
 
347
- normalized_aliases = self._normalize_aliases(
347
+ normalized_aliases, group_aliases = self._normalize_nested_aliases(
348
348
  primary_name=command_name,
349
349
  aliases=aliases,
350
350
  group_name=group_name,
@@ -355,6 +355,11 @@ class BaseCommand(ABC):
355
355
  description=self.description(),
356
356
  aliases=normalized_aliases,
357
357
  )
358
+ self._register_group_aliases(
359
+ app=self.app,
360
+ group_name=group_name,
361
+ aliases=group_aliases,
362
+ )
358
363
  else:
359
364
  # Single-level command (e.g., "help", "init", "config:set", "make:command")
360
365
  name = signature_parts[0]
@@ -409,6 +414,31 @@ class BaseCommand(ABC):
409
414
  setattr(app, "_usecli_aliases", registry)
410
415
  return registry
411
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
+
412
442
  def _normalize_aliases(
413
443
  self,
414
444
  primary_name: str,
@@ -437,6 +467,51 @@ class BaseCommand(ABC):
437
467
  normalized.append(alias_name)
438
468
  return normalized
439
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
514
+
440
515
  def _is_valid_subcommand_name(self, name: str) -> bool:
441
516
  """Check if a string is a valid subcommand name (not an argument placeholder).
442
517
 
@@ -52,6 +52,8 @@ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
52
52
 
53
53
  alias_registry = _get_alias_registry(app)
54
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)
55
57
 
56
58
  command_name = get_script_command_name(default="usecli")
57
59
 
@@ -84,23 +86,15 @@ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
84
86
  commands.append(command_entry)
85
87
  commands.sort(key=lambda x: x["name"])
86
88
 
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
89
  groups: dict[str, str] = {}
101
90
  if isinstance(click_group, click.Group):
102
91
  for cmd_name, cmd_obj in click_group.commands.items():
103
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
104
98
  groups[cmd_name] = (
105
99
  getattr(cmd_obj, "help", f"Commands for {cmd_name}")
106
100
  or f"Commands for {cmd_name}"
@@ -117,7 +111,41 @@ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
117
111
 
118
112
  help_flags = "--help, -h"
119
113
  all_option_flags = [help_flags] + option_flags
120
- all_display_names = [cmd["display_name"] for cmd in commands] + list(groups.keys())
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
+ ]
121
149
  all_labels = all_display_names + all_option_flags
122
150
  longest_label_length = max((len(label) for label in all_labels), default=0)
123
151
 
@@ -147,8 +175,9 @@ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
147
175
  if not prefix_filter:
148
176
  console.print(f"[bold {COLOR.SECONDARY}]Available commands:")
149
177
 
178
+ group_names = set(groups.keys())
150
179
  top_level: list[CommandEntry] = [
151
- c for c in commands if ":" not in c["name"] and c["name"] not in groups
180
+ c for c in commands if ":" not in c["name"] and c["name"] not in group_names
152
181
  ]
153
182
  with_colon: list[CommandEntry] = [c for c in commands if ":" in c["name"]]
154
183
 
@@ -160,15 +189,7 @@ def list_commands(app: typer.Typer, prefix_filter: str | None = None) -> None:
160
189
  f" [{COLOR.COMMAND} not bold]{display_name}[/{COLOR.COMMAND} not bold]{padding}{cmd['help']}"
161
190
  )
162
191
 
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
- )
192
+ top_level.extend(group_entries)
172
193
 
173
194
  top_level.sort(key=lambda x: x["name"])
174
195
 
@@ -291,6 +312,11 @@ def _get_alias_registry(app: typer.Typer) -> dict[str, list[str]]:
291
312
  return registry if isinstance(registry, dict) else {}
292
313
 
293
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
+
294
320
  def _get_option_description(param: click.Parameter) -> str:
295
321
  if "--show-completion" in param.opts:
296
322
  return "Show completion for the current shell."
@@ -12,8 +12,8 @@ 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] }}]
15
+ # def aliases(self) -> list[str]:
16
+ # return [{{ command_name[:3] }}]
17
17
 
18
18
  def handle(
19
19
  self,
File without changes
File without changes
File without changes
File without changes
File without changes