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.
- {usecli-0.1.30 → usecli-0.1.32}/PKG-INFO +1 -1
- {usecli-0.1.30 → usecli-0.1.32}/pyproject.toml +1 -1
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/__init__.py +36 -1
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +26 -3
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/base_command.py +165 -9
- usecli-0.1.32/src/usecli/cli/core/ui/list.py +362 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/command.py.j2 +3 -0
- usecli-0.1.30/src/usecli/cli/core/ui/list.py +0 -217
- {usecli-0.1.30 → usecli-0.1.32}/LICENSE +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/README.md +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/init_command.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/config/colors.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/skill_generator.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/templates/usecli.toml.j2 +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/menu.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/params.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/shared/config/manager.py +0 -0
- {usecli-0.1.30 → usecli-0.1.32}/src/usecli/ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
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.
|
{usecli-0.1.30 → usecli-0.1.32}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py
RENAMED
|
@@ -155,11 +155,27 @@ def _run_fzf_menu(
|
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|