usecli 0.1.31__tar.gz → 0.1.33__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.31 → usecli-0.1.33}/PKG-INFO +1 -1
- {usecli-0.1.31 → usecli-0.1.33}/pyproject.toml +1 -1
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/__init__.py +35 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +26 -3
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/init_command.py +35 -1
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/config/colors.py +8 -2
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/base_command.py +76 -1
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/ui/list.py +50 -24
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/templates/command.py.j2 +2 -2
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/shared/config/globals.py +1 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/shared/config/manager.py +89 -7
- {usecli-0.1.31 → usecli-0.1.33}/LICENSE +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/README.md +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/skill_generator.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/templates/usecli.toml.j2 +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/menu.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/params.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.31 → usecli-0.1.33}/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.33"
|
|
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)
|
{usecli-0.1.31 → usecli-0.1.33}/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)
|
|
@@ -27,7 +27,7 @@ from usecli.cli.core.base_command import BaseCommand
|
|
|
27
27
|
from usecli.cli.core.exceptions import UsecliBadParameter
|
|
28
28
|
from usecli.cli.core.validators import validate_command_name
|
|
29
29
|
from usecli.cli.utils.interactive.terminal_menu import terminal_menu
|
|
30
|
-
from usecli.shared.config.globals import TEMPLATES_DIR, THEMES_DIR
|
|
30
|
+
from usecli.shared.config.globals import TEMPLATES_DIR, THEMES_DIR, USECLI_TOML
|
|
31
31
|
from usecli.shared.config.manager import ConfigManager, get_config
|
|
32
32
|
|
|
33
33
|
console = Console()
|
|
@@ -113,6 +113,23 @@ class InitCommand(BaseCommand):
|
|
|
113
113
|
pyproject_path.write_text(content)
|
|
114
114
|
return True
|
|
115
115
|
|
|
116
|
+
def _write_usecli_toml(
|
|
117
|
+
self, project_root: Path, config_content: str, force: bool
|
|
118
|
+
) -> str:
|
|
119
|
+
config_path = project_root / USECLI_TOML
|
|
120
|
+
existed = config_path.exists()
|
|
121
|
+
if existed and not force:
|
|
122
|
+
should_overwrite = Confirm.ask(
|
|
123
|
+
f"[{COLOR.WARNING}]usecli.toml already exists at {config_path}.[/{COLOR.WARNING}]\n"
|
|
124
|
+
"Overwrite it with the new settings from this init run?",
|
|
125
|
+
default=False,
|
|
126
|
+
)
|
|
127
|
+
if not should_overwrite:
|
|
128
|
+
return "skipped"
|
|
129
|
+
|
|
130
|
+
config_path.write_text(config_content.rstrip() + "\n")
|
|
131
|
+
return "updated" if existed else "created"
|
|
132
|
+
|
|
116
133
|
def _ensure_project_scripts(
|
|
117
134
|
self, pyproject_path: Path, command_name: str, force: bool
|
|
118
135
|
) -> str:
|
|
@@ -685,6 +702,7 @@ include = ["{root_package}*"]
|
|
|
685
702
|
)
|
|
686
703
|
|
|
687
704
|
scripts_status: str | None = None
|
|
705
|
+
usecli_toml_status: str | None = None
|
|
688
706
|
|
|
689
707
|
# Check if pyproject.toml exists
|
|
690
708
|
if pyproject_path.exists():
|
|
@@ -744,6 +762,22 @@ include = ["{root_package}*"]
|
|
|
744
762
|
|
|
745
763
|
self._sync_environment(project_root, command_name)
|
|
746
764
|
|
|
765
|
+
usecli_toml_status = self._write_usecli_toml(
|
|
766
|
+
project_root, config_content, force
|
|
767
|
+
)
|
|
768
|
+
if usecli_toml_status == "created":
|
|
769
|
+
console.print(
|
|
770
|
+
f"[{COLOR.SUCCESS}]Created {USECLI_TOML} for runtime config fallback[/{COLOR.SUCCESS}]"
|
|
771
|
+
)
|
|
772
|
+
elif usecli_toml_status == "updated":
|
|
773
|
+
console.print(
|
|
774
|
+
f"[{COLOR.SUCCESS}]Updated {USECLI_TOML} for runtime config fallback[/{COLOR.SUCCESS}]"
|
|
775
|
+
)
|
|
776
|
+
elif usecli_toml_status == "skipped":
|
|
777
|
+
console.print(
|
|
778
|
+
f"[{COLOR.WARNING}]Skipped updating {USECLI_TOML}.[/{COLOR.WARNING}]"
|
|
779
|
+
)
|
|
780
|
+
|
|
747
781
|
# Show summary
|
|
748
782
|
summary_command = (
|
|
749
783
|
command_name
|
|
@@ -21,6 +21,8 @@ else:
|
|
|
21
21
|
import tomli as tomllib
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
PYPROJECT_TOML = "pyproject.toml"
|
|
25
|
+
USECLI_TOML = "usecli.toml"
|
|
24
26
|
DEFAULT_THEME_NAME = "default"
|
|
25
27
|
THEMES_DIR = Path(__file__).resolve().parent.parent / "themes"
|
|
26
28
|
DEFAULT_THEME_COLORS: dict[str, str] = {
|
|
@@ -53,10 +55,14 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
|
|
|
53
55
|
current = start_dir.resolve()
|
|
54
56
|
|
|
55
57
|
while True:
|
|
56
|
-
pyproject_path = current /
|
|
58
|
+
pyproject_path = current / PYPROJECT_TOML
|
|
57
59
|
if pyproject_path.exists():
|
|
58
60
|
return current
|
|
59
61
|
|
|
62
|
+
usecli_path = current / USECLI_TOML
|
|
63
|
+
if usecli_path.exists():
|
|
64
|
+
return current
|
|
65
|
+
|
|
60
66
|
git_dir = current / ".git"
|
|
61
67
|
if git_dir.exists():
|
|
62
68
|
return current
|
|
@@ -73,7 +79,7 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
|
|
|
73
79
|
if project_root is None:
|
|
74
80
|
return {}
|
|
75
81
|
|
|
76
|
-
pyproject_path = project_root /
|
|
82
|
+
pyproject_path = project_root / PYPROJECT_TOML
|
|
77
83
|
if not pyproject_path.exists():
|
|
78
84
|
return {}
|
|
79
85
|
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
# def aliases(self) -> list[str]:
|
|
16
|
+
# return [{{ command_name[:3] }}]
|
|
17
17
|
|
|
18
18
|
def handle(
|
|
19
19
|
self,
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
from usecli.cli.core.exceptions.config import UsecliConfigError
|
|
15
|
+
from usecli.shared.config.globals import PYPROJECT_TOML, USECLI_TOML
|
|
15
16
|
|
|
16
17
|
if sys.version_info >= (3, 11):
|
|
17
18
|
import tomllib
|
|
@@ -87,6 +88,7 @@ class ConfigManager:
|
|
|
87
88
|
def __init__(
|
|
88
89
|
self,
|
|
89
90
|
pyproject_path: Path | None = None,
|
|
91
|
+
usecli_toml_path: Path | None = None,
|
|
90
92
|
start_dir: Path | None = None,
|
|
91
93
|
) -> None:
|
|
92
94
|
"""Initialize the configuration manager.
|
|
@@ -102,12 +104,21 @@ class ConfigManager:
|
|
|
102
104
|
|
|
103
105
|
if pyproject_path is None:
|
|
104
106
|
pyproject_path = self._find_pyproject_toml(start_dir) or (
|
|
105
|
-
start_dir /
|
|
107
|
+
start_dir / PYPROJECT_TOML
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if usecli_toml_path is None:
|
|
111
|
+
usecli_toml_path = self._find_usecli_toml(start_dir) or (
|
|
112
|
+
start_dir / USECLI_TOML
|
|
106
113
|
)
|
|
107
114
|
|
|
108
115
|
self.pyproject_path: Path = pyproject_path
|
|
116
|
+
self.usecli_toml_path: Path = usecli_toml_path
|
|
109
117
|
self.start_dir: Path = start_dir
|
|
110
|
-
|
|
118
|
+
detected_root = find_project_root(start_dir)
|
|
119
|
+
if detected_root is None and self.usecli_toml_path.exists():
|
|
120
|
+
detected_root = self.usecli_toml_path.parent
|
|
121
|
+
self.project_root: Path = (detected_root or start_dir).resolve()
|
|
111
122
|
self._config: dict[str, Any] = {}
|
|
112
123
|
self._overrides: dict[str, Any] = {}
|
|
113
124
|
self._load_config()
|
|
@@ -117,18 +128,32 @@ class ConfigManager:
|
|
|
117
128
|
self._config = self.DEFAULT_CONFIG.copy()
|
|
118
129
|
self._overrides = {}
|
|
119
130
|
|
|
131
|
+
loaded = False
|
|
120
132
|
if self.pyproject_path.exists():
|
|
121
133
|
try:
|
|
122
134
|
pyproject_config = self._load_pyproject_toml(self.pyproject_path)
|
|
123
135
|
if pyproject_config:
|
|
124
136
|
self._config = _deep_merge(self._config, pyproject_config)
|
|
125
137
|
self._overrides = _deep_merge(self._overrides, pyproject_config)
|
|
138
|
+
loaded = True
|
|
126
139
|
except (tomllib.TOMLDecodeError, OSError) as e:
|
|
127
140
|
raise UsecliConfigError(
|
|
128
141
|
f"Failed to load pyproject.toml: {e}",
|
|
129
142
|
config_file=str(self.pyproject_path),
|
|
130
143
|
) from e
|
|
131
144
|
|
|
145
|
+
if not loaded and self.usecli_toml_path.exists():
|
|
146
|
+
try:
|
|
147
|
+
usecli_config = self._load_usecli_toml(self.usecli_toml_path)
|
|
148
|
+
if usecli_config:
|
|
149
|
+
self._config = _deep_merge(self._config, usecli_config)
|
|
150
|
+
self._overrides = _deep_merge(self._overrides, usecli_config)
|
|
151
|
+
except (tomllib.TOMLDecodeError, OSError) as e:
|
|
152
|
+
raise UsecliConfigError(
|
|
153
|
+
f"Failed to load usecli.toml: {e}",
|
|
154
|
+
config_file=str(self.usecli_toml_path),
|
|
155
|
+
) from e
|
|
156
|
+
|
|
132
157
|
default_themes = _normalize_themes_dir(self.DEFAULT_CONFIG.get("themes_dir"))
|
|
133
158
|
override_themes = _normalize_themes_dir(self._overrides.get("themes_dir"))
|
|
134
159
|
merged_themes = _dedupe_items(default_themes + override_themes)
|
|
@@ -151,7 +176,7 @@ class ConfigManager:
|
|
|
151
176
|
current = start_dir.resolve()
|
|
152
177
|
|
|
153
178
|
while True:
|
|
154
|
-
pyproject_path = current /
|
|
179
|
+
pyproject_path = current / PYPROJECT_TOML
|
|
155
180
|
if pyproject_path.exists():
|
|
156
181
|
return pyproject_path
|
|
157
182
|
|
|
@@ -162,6 +187,38 @@ class ConfigManager:
|
|
|
162
187
|
|
|
163
188
|
return None
|
|
164
189
|
|
|
190
|
+
@classmethod
|
|
191
|
+
def _find_usecli_toml(cls, start_dir: Path) -> Path | None:
|
|
192
|
+
current = start_dir.resolve()
|
|
193
|
+
|
|
194
|
+
while True:
|
|
195
|
+
config_path = current / USECLI_TOML
|
|
196
|
+
if config_path.exists():
|
|
197
|
+
return config_path
|
|
198
|
+
|
|
199
|
+
parent = current.parent
|
|
200
|
+
if parent == current:
|
|
201
|
+
break
|
|
202
|
+
current = parent
|
|
203
|
+
|
|
204
|
+
return cls._find_usecli_toml_on_sys_path()
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _find_usecli_toml_on_sys_path() -> Path | None:
|
|
208
|
+
for entry in sys.path:
|
|
209
|
+
if not entry:
|
|
210
|
+
continue
|
|
211
|
+
path = Path(entry)
|
|
212
|
+
if not path.exists() or not path.is_dir():
|
|
213
|
+
continue
|
|
214
|
+
candidate = path / USECLI_TOML
|
|
215
|
+
if candidate.exists():
|
|
216
|
+
return candidate
|
|
217
|
+
for child in path.glob(f"*/{USECLI_TOML}"):
|
|
218
|
+
if child.exists():
|
|
219
|
+
return child
|
|
220
|
+
return None
|
|
221
|
+
|
|
165
222
|
@staticmethod
|
|
166
223
|
def _load_pyproject_toml(path: Path) -> dict[str, Any]:
|
|
167
224
|
"""Load pyproject.toml and return [tool.usecli] section.
|
|
@@ -176,6 +233,23 @@ class ConfigManager:
|
|
|
176
233
|
data = tomllib.load(f)
|
|
177
234
|
return data.get("tool", {}).get("usecli", {})
|
|
178
235
|
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _load_usecli_toml(path: Path) -> dict[str, Any]:
|
|
238
|
+
with open(path, "rb") as f:
|
|
239
|
+
data = tomllib.load(f)
|
|
240
|
+
|
|
241
|
+
tool_section = data.get("tool", {})
|
|
242
|
+
if isinstance(tool_section, dict):
|
|
243
|
+
usecli_section = tool_section.get("usecli", {})
|
|
244
|
+
if isinstance(usecli_section, dict):
|
|
245
|
+
return usecli_section
|
|
246
|
+
|
|
247
|
+
usecli_section = data.get("usecli", {})
|
|
248
|
+
if isinstance(usecli_section, dict):
|
|
249
|
+
return usecli_section
|
|
250
|
+
|
|
251
|
+
return {}
|
|
252
|
+
|
|
179
253
|
def get(self, key: str, default: Any = None) -> Any:
|
|
180
254
|
"""Get a configuration value using dot notation.
|
|
181
255
|
|
|
@@ -256,9 +330,13 @@ class ConfigManager:
|
|
|
256
330
|
@property
|
|
257
331
|
def pyproject_exists(self) -> bool:
|
|
258
332
|
"""Check if pyproject.toml with [tool.usecli] exists."""
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
|
|
333
|
+
if self.pyproject_path.exists() and self._pyproject_has_usecli(
|
|
334
|
+
self.pyproject_path
|
|
335
|
+
):
|
|
336
|
+
return True
|
|
337
|
+
if self.usecli_toml_path.exists():
|
|
338
|
+
return True
|
|
339
|
+
return False
|
|
262
340
|
|
|
263
341
|
@staticmethod
|
|
264
342
|
def _load_project_version(path: Path) -> str | None:
|
|
@@ -305,10 +383,14 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
|
|
|
305
383
|
current = start_dir.resolve()
|
|
306
384
|
|
|
307
385
|
while True:
|
|
308
|
-
pyproject_path = current /
|
|
386
|
+
pyproject_path = current / PYPROJECT_TOML
|
|
309
387
|
if pyproject_path.exists():
|
|
310
388
|
return current
|
|
311
389
|
|
|
390
|
+
usecli_path = current / USECLI_TOML
|
|
391
|
+
if usecli_path.exists():
|
|
392
|
+
return current
|
|
393
|
+
|
|
312
394
|
git_dir = current / ".git"
|
|
313
395
|
if git_dir.exists():
|
|
314
396
|
return current
|
|
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
|