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.
Files changed (70) hide show
  1. {usecli-0.1.55 → usecli-0.1.57}/PKG-INFO +1 -1
  2. {usecli-0.1.55 → usecli-0.1.57}/pyproject.toml +1 -1
  3. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/__init__.py +4 -1
  4. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/services/command_service.py +7 -3
  5. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/manager.py +154 -6
  6. {usecli-0.1.55 → usecli-0.1.57}/LICENSE +0 -0
  7. {usecli-0.1.55 → usecli-0.1.57}/README.md +0 -0
  8. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/__init__.py +0 -0
  9. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/README.md +0 -0
  10. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/__init__.py +0 -0
  11. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/custom/README.md +0 -0
  12. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/custom/__init__.py +0 -0
  13. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  14. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  15. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  16. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/config/colors.py +0 -0
  28. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/__init__.py +0 -0
  29. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/base_command.py +0 -0
  30. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/__init__.py +0 -0
  31. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/handler.py +0 -0
  32. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/error/utils.py +0 -0
  33. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  34. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/base.py +0 -0
  35. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/config.py +0 -0
  36. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/usage.py +0 -0
  37. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/exceptions/validation.py +0 -0
  38. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/__init__.py +0 -0
  39. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/list.py +0 -0
  40. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/ui/title.txt +0 -0
  42. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/__init__.py +0 -0
  43. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/network.py +0 -0
  44. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/numeric.py +0 -0
  45. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/path.py +0 -0
  46. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/core/validators/string.py +0 -0
  47. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/services/__init__.py +0 -0
  48. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/params.py +0 -0
  66. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/ui.py +0 -0
  70. {usecli-0.1.55 → usecli-0.1.57}/src/usecli/usecli.config.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.55
3
+ Version: 0.1.57
4
4
  Summary: A powerful Python CLI framework for building beautiful, developer-friendly command-line tools.
5
5
  Author: Edward Boswell
6
6
  Author-email: Edward Boswell <thememium@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.55"
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 blue]CLI Version:[/bold blue] [green]{service.version}[/green]"
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
- package_commands_dir = (PACKAGE_ROOT / "cli/commands").resolve()
45
- self._load_from_dir(package_commands_dir)
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 = get_config().get_project_commands_dir().resolve()
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=True
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
- spec = importlib.util.find_spec(_get_package_name())
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