usecli 0.1.55__tar.gz → 0.1.57__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.55 → usecli-0.1.57}/PKG-INFO +1 -1
- {usecli-0.1.55 → usecli-0.1.57}/pyproject.toml +1 -1
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/__init__.py +4 -1
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/services/command_service.py +7 -3
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/manager.py +154 -6
- {usecli-0.1.55 → usecli-0.1.57}/LICENSE +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/README.md +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/init_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/config/colors.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/base_command.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/list.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/title.txt +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/command.py.j2 +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/menu.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/params.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/ui.py +0 -0
- {usecli-0.1.55 → usecli-0.1.57}/src/usecli/usecli.config.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.57"
|
|
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" }]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shutil
|
|
5
6
|
import sys
|
|
6
7
|
from importlib import import_module
|
|
7
8
|
from typing import Any, Optional, Sequence
|
|
@@ -282,8 +283,10 @@ def run_app(
|
|
|
282
283
|
raise typer.Exit()
|
|
283
284
|
|
|
284
285
|
if version:
|
|
286
|
+
config = get_config()
|
|
287
|
+
command_path = shutil.which(sys.argv[0]) or sys.argv[0]
|
|
285
288
|
console.print(
|
|
286
|
-
f"[bold
|
|
289
|
+
f"[bold {theme.SECONDARY}]{config.get('title')} {service.version}[/bold {theme.SECONDARY}] [{theme.INFO}]({command_path})[/{theme.INFO}]"
|
|
287
290
|
)
|
|
288
291
|
raise typer.Exit()
|
|
289
292
|
|
|
@@ -41,10 +41,14 @@ class CommandService:
|
|
|
41
41
|
def load_commands(self) -> None:
|
|
42
42
|
"""Load all commands from the commands directory and project directories."""
|
|
43
43
|
self._load_version()
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
config = get_config()
|
|
45
|
+
|
|
46
|
+
if config.is_usecli_direct_dependency():
|
|
47
|
+
package_commands_dir = (PACKAGE_ROOT / "cli/commands").resolve()
|
|
48
|
+
self._load_from_dir(package_commands_dir)
|
|
46
49
|
|
|
47
|
-
project_commands_dir =
|
|
50
|
+
project_commands_dir = config.get_project_commands_dir().resolve()
|
|
51
|
+
package_commands_dir = (PACKAGE_ROOT / "cli/commands").resolve()
|
|
48
52
|
if project_commands_dir == package_commands_dir:
|
|
49
53
|
return
|
|
50
54
|
try:
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import importlib.metadata
|
|
9
9
|
import importlib.util
|
|
10
|
+
import json
|
|
10
11
|
import os
|
|
11
12
|
import sys
|
|
12
13
|
from pathlib import Path
|
|
@@ -138,6 +139,13 @@ class ConfigManager:
|
|
|
138
139
|
):
|
|
139
140
|
detected_root = config_parent
|
|
140
141
|
self.project_root: Path = (detected_root or start_dir).resolve()
|
|
142
|
+
# Only override project_root for the framework itself (usecli).
|
|
143
|
+
# Downstream packages (usechange, userun, etc.) legitimately live
|
|
144
|
+
# inside .venv when installed as dependencies — don't break them.
|
|
145
|
+
command_name = self._get_command_name()
|
|
146
|
+
is_framework = command_name == "usecli" if command_name else True
|
|
147
|
+
if is_framework and self._is_in_venv(self.project_root):
|
|
148
|
+
self.project_root = start_dir.resolve()
|
|
141
149
|
self._config: dict[str, Any] = {}
|
|
142
150
|
self._overrides: dict[str, Any] = {}
|
|
143
151
|
self._load_config()
|
|
@@ -211,8 +219,9 @@ class ConfigManager:
|
|
|
211
219
|
current = parent
|
|
212
220
|
|
|
213
221
|
search_root = find_project_root(start_dir) or start_dir.resolve()
|
|
222
|
+
is_framework = command_name == "usecli" if command_name else True
|
|
214
223
|
recursive_match = cls._find_usecli_config_in_tree(
|
|
215
|
-
search_root, start_dir, skip_venv=
|
|
224
|
+
search_root, start_dir, skip_venv=is_framework
|
|
216
225
|
)
|
|
217
226
|
if recursive_match:
|
|
218
227
|
return recursive_match
|
|
@@ -279,9 +288,26 @@ class ConfigManager:
|
|
|
279
288
|
|
|
280
289
|
@staticmethod
|
|
281
290
|
def _find_usecli_config_in_package() -> Path | None:
|
|
282
|
-
|
|
291
|
+
package_name = _get_package_name()
|
|
292
|
+
spec = importlib.util.find_spec(package_name)
|
|
283
293
|
if spec is None or not spec.submodule_search_locations:
|
|
284
294
|
return None
|
|
295
|
+
|
|
296
|
+
command_name = ConfigManager._get_command_name()
|
|
297
|
+
aliases = ConfigManager._get_console_script_aliases(command_name)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
dist = importlib.metadata.distribution(package_name)
|
|
301
|
+
source_root = ConfigManager._resolve_editable_source_root(dist)
|
|
302
|
+
if source_root:
|
|
303
|
+
source_config = ConfigManager._search_source_for_config(
|
|
304
|
+
source_root, command_name, aliases
|
|
305
|
+
)
|
|
306
|
+
if source_config:
|
|
307
|
+
return source_config
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
|
|
285
311
|
for location in spec.submodule_search_locations:
|
|
286
312
|
package_root = Path(location)
|
|
287
313
|
if not package_root.exists() or not package_root.is_dir():
|
|
@@ -289,8 +315,6 @@ class ConfigManager:
|
|
|
289
315
|
candidates = [
|
|
290
316
|
path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
|
|
291
317
|
]
|
|
292
|
-
command_name = ConfigManager._get_command_name()
|
|
293
|
-
aliases = ConfigManager._get_console_script_aliases(command_name)
|
|
294
318
|
if command_name:
|
|
295
319
|
candidates = [
|
|
296
320
|
path
|
|
@@ -311,6 +335,22 @@ class ConfigManager:
|
|
|
311
335
|
spec = importlib.util.find_spec(package_name)
|
|
312
336
|
if spec is None or not spec.submodule_search_locations:
|
|
313
337
|
return None
|
|
338
|
+
|
|
339
|
+
command_name = cls._get_command_name()
|
|
340
|
+
aliases = cls._get_console_script_aliases(command_name)
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
dist = importlib.metadata.distribution(package_name)
|
|
344
|
+
source_root = cls._resolve_editable_source_root(dist)
|
|
345
|
+
if source_root:
|
|
346
|
+
source_config = cls._search_source_for_config(
|
|
347
|
+
source_root, command_name, aliases
|
|
348
|
+
)
|
|
349
|
+
if source_config:
|
|
350
|
+
return source_config
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
|
|
314
354
|
for location in spec.submodule_search_locations:
|
|
315
355
|
package_root = Path(location)
|
|
316
356
|
if not package_root.exists() or not package_root.is_dir():
|
|
@@ -318,8 +358,6 @@ class ConfigManager:
|
|
|
318
358
|
candidates = [
|
|
319
359
|
path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
|
|
320
360
|
]
|
|
321
|
-
command_name = cls._get_command_name()
|
|
322
|
-
aliases = cls._get_console_script_aliases(command_name)
|
|
323
361
|
if command_name:
|
|
324
362
|
candidates = [
|
|
325
363
|
path
|
|
@@ -362,7 +400,15 @@ class ConfigManager:
|
|
|
362
400
|
normalized = dist_name.replace("-", "_")
|
|
363
401
|
if normalized not in candidates:
|
|
364
402
|
candidates.append(normalized)
|
|
403
|
+
aliases = cls._get_console_script_aliases(command_name)
|
|
365
404
|
for package_name in candidates:
|
|
405
|
+
source_root = cls._resolve_editable_source_root(dist)
|
|
406
|
+
if source_root:
|
|
407
|
+
source_config = cls._search_source_for_config(
|
|
408
|
+
source_root, command_name, aliases
|
|
409
|
+
)
|
|
410
|
+
if source_config:
|
|
411
|
+
return source_config
|
|
366
412
|
match = cls._find_usecli_config_in_named_package(package_name)
|
|
367
413
|
if match:
|
|
368
414
|
return match
|
|
@@ -471,6 +517,71 @@ class ConfigManager:
|
|
|
471
517
|
aliases = {command_name}
|
|
472
518
|
return normalized in aliases
|
|
473
519
|
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _is_in_venv(path: Path) -> bool:
|
|
522
|
+
resolved = path.resolve()
|
|
523
|
+
return any(part in ConfigManager._SKIP_DIRS for part in resolved.parts)
|
|
524
|
+
|
|
525
|
+
@staticmethod
|
|
526
|
+
def _resolve_editable_source_root(
|
|
527
|
+
dist: importlib.metadata.Distribution,
|
|
528
|
+
) -> Path | None:
|
|
529
|
+
"""Resolve the source directory for an editable-installed package.
|
|
530
|
+
|
|
531
|
+
Reads ``direct_url.json`` from the distribution's metadata to find the
|
|
532
|
+
local source tree. Returns the source root or ``None`` when the
|
|
533
|
+
distribution is not an editable install or the source no longer exists.
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
text = dist.read_text("direct_url.json")
|
|
537
|
+
except Exception:
|
|
538
|
+
return None
|
|
539
|
+
if not text:
|
|
540
|
+
return None
|
|
541
|
+
try:
|
|
542
|
+
data = json.loads(text)
|
|
543
|
+
except (json.JSONDecodeError, TypeError):
|
|
544
|
+
return None
|
|
545
|
+
if not isinstance(data, dict):
|
|
546
|
+
return None
|
|
547
|
+
if data.get("dir_info", {}).get("editable") is not True:
|
|
548
|
+
return None
|
|
549
|
+
url = data.get("url", "")
|
|
550
|
+
if not url:
|
|
551
|
+
return None
|
|
552
|
+
# ``url`` is a ``file://`` URI.
|
|
553
|
+
if url.startswith("file://"):
|
|
554
|
+
url = url[len("file://") :]
|
|
555
|
+
source = Path(url)
|
|
556
|
+
if source.exists() and source.is_dir():
|
|
557
|
+
return source.resolve()
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _search_source_for_config(
|
|
562
|
+
source_root: Path,
|
|
563
|
+
command_name: str | None,
|
|
564
|
+
aliases: set[str] | None,
|
|
565
|
+
) -> Path | None:
|
|
566
|
+
"""Search a source tree for a ``usecli.config.toml`` that matches."""
|
|
567
|
+
if not source_root.exists() or not source_root.is_dir():
|
|
568
|
+
return None
|
|
569
|
+
candidates = [
|
|
570
|
+
p
|
|
571
|
+
for p in source_root.rglob(USECLI_CONFIG_TOML)
|
|
572
|
+
if not any(part in ConfigManager._SKIP_DIRS for part in p.parts)
|
|
573
|
+
]
|
|
574
|
+
if command_name:
|
|
575
|
+
candidates = [
|
|
576
|
+
p
|
|
577
|
+
for p in candidates
|
|
578
|
+
if ConfigManager._config_matches_command(p, command_name, aliases)
|
|
579
|
+
]
|
|
580
|
+
if not candidates:
|
|
581
|
+
return None
|
|
582
|
+
candidates.sort(key=lambda p: (len(p.parts), str(p)))
|
|
583
|
+
return candidates[0]
|
|
584
|
+
|
|
474
585
|
def get(self, key: str, default: Any = None) -> Any:
|
|
475
586
|
"""Get a configuration value using dot notation.
|
|
476
587
|
|
|
@@ -544,6 +655,43 @@ class ConfigManager:
|
|
|
544
655
|
"""Check if running in production environment."""
|
|
545
656
|
return self.get("environment", "prod") == "prod"
|
|
546
657
|
|
|
658
|
+
def is_usecli_direct_dependency(self) -> bool:
|
|
659
|
+
"""Check if usecli is a direct dependency of the current project.
|
|
660
|
+
|
|
661
|
+
Returns True when:
|
|
662
|
+
- The current command IS usecli (framework mode)
|
|
663
|
+
- usecli appears in pyproject.toml [project.dependencies]
|
|
664
|
+
- usecli appears in pyproject.toml [dependency-groups]
|
|
665
|
+
"""
|
|
666
|
+
command_name = self._get_command_name()
|
|
667
|
+
if command_name == "usecli":
|
|
668
|
+
return True
|
|
669
|
+
|
|
670
|
+
if not self.pyproject_path.exists():
|
|
671
|
+
return False
|
|
672
|
+
|
|
673
|
+
try:
|
|
674
|
+
with open(self.pyproject_path, "rb") as f:
|
|
675
|
+
data = tomllib.load(f)
|
|
676
|
+
except (tomllib.TOMLDecodeError, OSError):
|
|
677
|
+
return False
|
|
678
|
+
|
|
679
|
+
for dep in data.get("project", {}).get("dependencies", []):
|
|
680
|
+
if isinstance(dep, str) and dep.strip().lower().startswith("usecli"):
|
|
681
|
+
return True
|
|
682
|
+
|
|
683
|
+
for group_deps in data.get("dependency-groups", {}).values():
|
|
684
|
+
if not isinstance(group_deps, list):
|
|
685
|
+
continue
|
|
686
|
+
for dep in group_deps:
|
|
687
|
+
dep_str = dep if isinstance(dep, str) else dep.get("dependency", "")
|
|
688
|
+
if isinstance(dep_str, str) and dep_str.strip().lower().startswith(
|
|
689
|
+
"usecli"
|
|
690
|
+
):
|
|
691
|
+
return True
|
|
692
|
+
|
|
693
|
+
return False
|
|
694
|
+
|
|
547
695
|
def reload(self) -> None:
|
|
548
696
|
"""Reload configuration from disk."""
|
|
549
697
|
self.usecli_config_path = self._find_usecli_config(self.start_dir) or (
|
|
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
|
{usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py
RENAMED
|
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
|