usecli 0.1.34__tar.gz → 0.1.36__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.34 → usecli-0.1.36}/PKG-INFO +1 -1
  2. {usecli-0.1.34 → usecli-0.1.36}/pyproject.toml +1 -1
  3. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/init_command.py +38 -10
  4. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/config/colors.py +82 -3
  5. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/shared/config/manager.py +79 -3
  6. {usecli-0.1.34 → usecli-0.1.36}/LICENSE +0 -0
  7. {usecli-0.1.34 → usecli-0.1.36}/README.md +0 -0
  8. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/__init__.py +0 -0
  9. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/__init__.py +0 -0
  10. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/README.md +0 -0
  11. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/__init__.py +0 -0
  12. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/custom/README.md +0 -0
  13. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/custom/__init__.py +0 -0
  14. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  15. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  16. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  17. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  18. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  19. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  20. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  21. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  22. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  23. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  24. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  25. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  26. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/__init__.py +0 -0
  28. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/base_command.py +0 -0
  29. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/skill_generator.py +0 -0
  38. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/ui/__init__.py +0 -0
  39. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/ui/list.py +0 -0
  40. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/ui/title.py +0 -0
  41. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/usecli.config.toml +0 -0
  62. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/utils/__init__.py +0 -0
  63. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  64. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  65. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/menu.py +0 -0
  66. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/params.py +0 -0
  67. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/shared/__init__.py +0 -0
  68. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/shared/config/__init__.py +0 -0
  69. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/shared/config/globals.py +0 -0
  70. {usecli-0.1.34 → usecli-0.1.36}/src/usecli/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usecli
3
- Version: 0.1.34
3
+ Version: 0.1.36
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.34"
3
+ version = "0.1.36"
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" }]
@@ -88,18 +88,10 @@ class InitCommand(BaseCommand):
88
88
 
89
89
  def _write_usecli_config(
90
90
  self,
91
- project_root: Path,
91
+ config_path: Path,
92
92
  config_content: str,
93
93
  force: bool,
94
- commands_path: Path,
95
94
  ) -> str:
96
- config_root = project_root
97
- if commands_path.parent != project_root:
98
- config_root = commands_path.parent
99
- config_path = config_root / USECLI_CONFIG_TOML
100
- discovered_config = ConfigManager(start_dir=config_root).usecli_config_path
101
- if discovered_config.exists():
102
- config_path = discovered_config
103
95
  existed = config_path.exists()
104
96
  if existed and not force:
105
97
  should_overwrite = Confirm.ask(
@@ -113,6 +105,14 @@ class InitCommand(BaseCommand):
113
105
  config_path.write_text(config_content.rstrip() + "\n")
114
106
  return "updated" if existed else "created"
115
107
 
108
+ def _resolve_config_path(self, value: str, project_root: Path) -> Path:
109
+ path = Path(value).expanduser()
110
+ if not path.is_absolute():
111
+ path = project_root / path
112
+ if path.exists() and path.is_dir():
113
+ return (path / USECLI_CONFIG_TOML).resolve()
114
+ return path.resolve()
115
+
116
116
  def _ensure_project_scripts(
117
117
  self, pyproject_path: Path, command_name: str, force: bool
118
118
  ) -> str:
@@ -716,8 +716,36 @@ include = ["{root_package}*"]
716
716
 
717
717
  self._sync_environment(project_root, command_name)
718
718
 
719
+ config_root = project_root
720
+ if commands_path.parent != project_root:
721
+ config_root = commands_path.parent
722
+ existing_config = ConfigManager(start_dir=config_root).usecli_config_path
723
+ default_config_path = (
724
+ existing_config
725
+ if existing_config.exists()
726
+ else config_root / USECLI_CONFIG_TOML
727
+ )
728
+ config_location = Prompt.ask(
729
+ f"[bold {COLOR.SECONDARY}]Config file location[/bold {COLOR.SECONDARY}]"
730
+ " (path or directory)",
731
+ default=str(default_config_path),
732
+ )
733
+ config_path = self._resolve_config_path(config_location, project_root)
734
+ if (
735
+ existing_config.exists()
736
+ and config_path.resolve() != existing_config.resolve()
737
+ and not force
738
+ ):
739
+ replace_existing = Confirm.ask(
740
+ f"[{COLOR.WARNING}]Existing {USECLI_CONFIG_TOML} found at {existing_config}.[/{COLOR.WARNING}]\n"
741
+ "Replace it instead of writing to the new location?",
742
+ default=False,
743
+ )
744
+ if replace_existing:
745
+ config_path = existing_config
746
+
719
747
  usecli_config_status = self._write_usecli_config(
720
- project_root, config_content, force, commands_path
748
+ config_path, config_content, force
721
749
  )
722
750
  if usecli_config_status == "created":
723
751
  console.print(
@@ -11,6 +11,7 @@ Usage:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import importlib.util
14
15
  import sys
15
16
  from pathlib import Path
16
17
  from typing import Any, Callable, Final, final
@@ -25,6 +26,7 @@ PYPROJECT_TOML = "pyproject.toml"
25
26
  USECLI_CONFIG_TOML = "usecli.config.toml"
26
27
  DEFAULT_THEME_NAME = "default"
27
28
  THEMES_DIR = Path(__file__).resolve().parent.parent / "themes"
29
+ _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
28
30
  DEFAULT_THEME_COLORS: dict[str, str] = {
29
31
  "primary": "#60D7FF",
30
32
  "secondary": "#5EFF87",
@@ -48,11 +50,19 @@ DEFAULT_THEME_COLORS: dict[str, str] = {
48
50
  }
49
51
 
50
52
 
51
- def _find_usecli_config_path(root_dir: Path, start_dir: Path) -> Path | None:
53
+ def _find_usecli_config_path(
54
+ root_dir: Path, start_dir: Path, *, skip_venv: bool
55
+ ) -> Path | None:
52
56
  if not root_dir.exists() or not root_dir.is_dir():
53
57
  return None
54
58
 
55
59
  candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
60
+ if skip_venv:
61
+ candidates = [
62
+ path
63
+ for path in candidates
64
+ if not any(part in {".venv", "venv"} for part in path.parts)
65
+ ]
56
66
  if not candidates:
57
67
  return None
58
68
 
@@ -79,6 +89,42 @@ def _find_usecli_config_path(root_dir: Path, start_dir: Path) -> Path | None:
79
89
  return selection[0]
80
90
 
81
91
 
92
+ def _find_usecli_config_in_package() -> Path | None:
93
+ spec = importlib.util.find_spec(_get_package_name())
94
+ if spec is None or not spec.submodule_search_locations:
95
+ return None
96
+ for location in spec.submodule_search_locations:
97
+ package_root = Path(location)
98
+ if not package_root.exists() or not package_root.is_dir():
99
+ continue
100
+ candidates = [
101
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
102
+ ]
103
+ if candidates:
104
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
105
+ return candidates[0]
106
+ return None
107
+
108
+
109
+ def _is_preferred_package_path(path: Path) -> bool:
110
+ return any(part in _PACKAGE_PREFER_DIRS for part in path.parts)
111
+
112
+
113
+ def _is_within_usecli_package(start_dir: Path) -> bool:
114
+ spec = importlib.util.find_spec(_get_package_name())
115
+ if spec is None or not spec.submodule_search_locations:
116
+ return False
117
+ start_dir = start_dir.resolve()
118
+ for location in spec.submodule_search_locations:
119
+ package_root = Path(location)
120
+ try:
121
+ start_dir.relative_to(package_root)
122
+ return True
123
+ except ValueError:
124
+ continue
125
+ return False
126
+
127
+
82
128
  def _find_project_root(start_dir: Path | None = None) -> Path | None:
83
129
  if start_dir is None:
84
130
  start_dir = Path.cwd()
@@ -86,6 +132,10 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
86
132
  current = start_dir.resolve()
87
133
  git_root: Path | None = None
88
134
 
135
+ package_match = _find_usecli_config_in_package()
136
+ if package_match and _is_preferred_package_path(package_match):
137
+ return package_match.parent
138
+
89
139
  while True:
90
140
  pyproject_path = current / PYPROJECT_TOML
91
141
  if pyproject_path.exists():
@@ -106,10 +156,16 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
106
156
  current = parent
107
157
 
108
158
  search_root = git_root or start_dir.resolve()
109
- config_match = _find_usecli_config_path(search_root, start_dir)
159
+ config_match = _find_usecli_config_path(
160
+ search_root, start_dir, skip_venv=_is_within_usecli_package(start_dir)
161
+ )
110
162
  if config_match:
111
163
  return config_match.parent
112
164
 
165
+ package_match = _find_usecli_config_in_package()
166
+ if package_match:
167
+ return package_match.parent
168
+
113
169
  return git_root
114
170
 
115
171
 
@@ -117,12 +173,28 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
117
173
  if project_root is None:
118
174
  return {}
119
175
 
176
+ package_match = _find_usecli_config_in_package()
177
+ if package_match and _is_preferred_package_path(package_match):
178
+ return _load_usecli_config_file(package_match)
179
+
120
180
  config_path = project_root / USECLI_CONFIG_TOML
121
181
  if not config_path.exists():
122
- config_path = _find_usecli_config_path(project_root, project_root)
182
+ config_path = _find_usecli_config_path(
183
+ project_root,
184
+ project_root,
185
+ skip_venv=_is_within_usecli_package(project_root),
186
+ )
187
+ if not config_path or not config_path.exists():
188
+ package_match = _find_usecli_config_in_package()
189
+ if package_match:
190
+ config_path = package_match
123
191
  if not config_path or not config_path.exists():
124
192
  return {}
125
193
 
194
+ return _load_usecli_config_file(config_path)
195
+
196
+
197
+ def _load_usecli_config_file(config_path: Path) -> dict[str, Any]:
126
198
  try:
127
199
  data = tomllib.loads(config_path.read_text())
128
200
  except (tomllib.TOMLDecodeError, OSError):
@@ -141,6 +213,13 @@ def _load_usecli_config(project_root: Path | None) -> dict[str, Any]:
141
213
  return {}
142
214
 
143
215
 
216
+ def _get_package_name() -> str:
217
+ package = __package__ or __name__
218
+ if not package:
219
+ return "usecli"
220
+ return package.split(".")[0]
221
+
222
+
144
223
  def _normalize_color(value: Any) -> str | None:
145
224
  if not isinstance(value, str):
146
225
  return None
@@ -5,6 +5,7 @@ Handles loading and accessing configuration from project-level files.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import importlib.util
8
9
  import sys
9
10
  from pathlib import Path
10
11
  from typing import Any
@@ -58,6 +59,9 @@ def _dedupe_items(items: list[str]) -> list[str]:
58
59
  class ConfigManager:
59
60
  """Manages useCli configuration from project-level files."""
60
61
 
62
+ _SKIP_DIRS = {".venv", "venv"}
63
+ _PACKAGE_PREFER_DIRS = {".venv", "venv", "site-packages"}
64
+
61
65
  DEFAULT_CONFIG: dict[str, Any] = {
62
66
  "title": "usecli",
63
67
  "title_file": None,
@@ -166,6 +170,10 @@ class ConfigManager:
166
170
  def _find_usecli_config(cls, start_dir: Path) -> Path | None:
167
171
  current = start_dir.resolve()
168
172
 
173
+ package_match = cls._find_usecli_config_in_package()
174
+ if package_match and cls._is_preferred_package_path(package_match):
175
+ return package_match
176
+
169
177
  while True:
170
178
  config_path = current / USECLI_CONFIG_TOML
171
179
  if config_path.exists():
@@ -176,19 +184,33 @@ class ConfigManager:
176
184
  break
177
185
  current = parent
178
186
 
187
+ in_usecli_package = cls._is_within_usecli_package(start_dir)
188
+ if in_usecli_package and package_match:
189
+ return package_match
190
+
179
191
  search_root = find_project_root(start_dir) or start_dir.resolve()
180
- recursive_match = cls._find_usecli_config_in_tree(search_root, start_dir)
192
+ recursive_match = cls._find_usecli_config_in_tree(
193
+ search_root, start_dir, skip_venv=in_usecli_package
194
+ )
181
195
  if recursive_match:
182
196
  return recursive_match
183
197
 
184
198
  return cls._find_usecli_config_on_sys_path()
185
199
 
186
200
  @staticmethod
187
- def _find_usecli_config_in_tree(root_dir: Path, start_dir: Path) -> Path | None:
201
+ def _find_usecli_config_in_tree(
202
+ root_dir: Path, start_dir: Path, *, skip_venv: bool
203
+ ) -> Path | None:
188
204
  if not root_dir.exists() or not root_dir.is_dir():
189
205
  return None
190
206
 
191
207
  candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
208
+ if skip_venv:
209
+ candidates = [
210
+ path
211
+ for path in candidates
212
+ if not any(part in ConfigManager._SKIP_DIRS for part in path.parts)
213
+ ]
192
214
  if not candidates:
193
215
  return None
194
216
 
@@ -214,6 +236,42 @@ class ConfigManager:
214
236
  selection.sort(key=_depth_key)
215
237
  return selection[0]
216
238
 
239
+ @staticmethod
240
+ def _find_usecli_config_in_package() -> Path | None:
241
+ spec = importlib.util.find_spec(_get_package_name())
242
+ if spec is None or not spec.submodule_search_locations:
243
+ return None
244
+ for location in spec.submodule_search_locations:
245
+ package_root = Path(location)
246
+ if not package_root.exists() or not package_root.is_dir():
247
+ continue
248
+ candidates = [
249
+ path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
250
+ ]
251
+ if candidates:
252
+ candidates.sort(key=lambda path: (len(path.parts), str(path)))
253
+ return candidates[0]
254
+ return None
255
+
256
+ @staticmethod
257
+ def _is_preferred_package_path(path: Path) -> bool:
258
+ return any(part in ConfigManager._PACKAGE_PREFER_DIRS for part in path.parts)
259
+
260
+ @staticmethod
261
+ def _is_within_usecli_package(start_dir: Path) -> bool:
262
+ spec = importlib.util.find_spec(_get_package_name())
263
+ if spec is None or not spec.submodule_search_locations:
264
+ return False
265
+ start_dir = start_dir.resolve()
266
+ for location in spec.submodule_search_locations:
267
+ package_root = Path(location)
268
+ try:
269
+ start_dir.relative_to(package_root)
270
+ return True
271
+ except ValueError:
272
+ continue
273
+ return False
274
+
217
275
  @staticmethod
218
276
  def _find_usecli_config_on_sys_path() -> Path | None:
219
277
  for entry in sys.path:
@@ -322,6 +380,9 @@ class ConfigManager:
322
380
 
323
381
  def reload(self) -> None:
324
382
  """Reload configuration from disk."""
383
+ self.usecli_config_path = self._find_usecli_config(self.start_dir) or (
384
+ self.start_dir / USECLI_CONFIG_TOML
385
+ )
325
386
  self._load_config()
326
387
 
327
388
  @property
@@ -372,6 +433,10 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
372
433
 
373
434
  current = start_dir.resolve()
374
435
 
436
+ package_match = ConfigManager._find_usecli_config_in_package()
437
+ if package_match and ConfigManager._is_preferred_package_path(package_match):
438
+ return package_match.parent
439
+
375
440
  git_root: Path | None = None
376
441
  while True:
377
442
  pyproject_path = current / PYPROJECT_TOML
@@ -393,8 +458,19 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
393
458
  current = parent
394
459
 
395
460
  search_root = git_root or start_dir.resolve()
396
- config_match = ConfigManager._find_usecli_config_in_tree(search_root, start_dir)
461
+ config_match = ConfigManager._find_usecli_config_in_tree(
462
+ search_root,
463
+ start_dir,
464
+ skip_venv=ConfigManager._is_within_usecli_package(start_dir),
465
+ )
397
466
  if config_match:
398
467
  return config_match.parent
399
468
 
400
469
  return git_root
470
+
471
+
472
+ def _get_package_name() -> str:
473
+ package = __package__ or __name__
474
+ if not package:
475
+ return "usecli"
476
+ return package.split(".")[0]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes