usecli 0.1.60__tar.gz → 0.1.61__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.60 → usecli-0.1.61}/PKG-INFO +1 -1
  2. {usecli-0.1.60 → usecli-0.1.61}/pyproject.toml +1 -1
  3. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/config/colors.py +106 -41
  4. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/shared/config/manager.py +128 -50
  5. {usecli-0.1.60 → usecli-0.1.61}/LICENSE +0 -0
  6. {usecli-0.1.60 → usecli-0.1.61}/README.md +0 -0
  7. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/__init__.py +0 -0
  8. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/__init__.py +0 -0
  9. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/README.md +0 -0
  10. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/__init__.py +0 -0
  11. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/custom/README.md +0 -0
  12. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/custom/__init__.py +0 -0
  13. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  14. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  15. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
  16. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  17. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  18. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  19. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  20. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  21. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
  22. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  23. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
  24. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
  25. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/commands/init_command.py +0 -0
  26. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/config/__init__.py +0 -0
  27. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/__init__.py +0 -0
  28. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/base_command.py +0 -0
  29. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/error/__init__.py +0 -0
  30. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/error/handler.py +0 -0
  31. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/error/utils.py +0 -0
  32. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/exceptions/__init__.py +0 -0
  33. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/exceptions/base.py +0 -0
  34. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/exceptions/config.py +0 -0
  35. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/exceptions/usage.py +0 -0
  36. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/exceptions/validation.py +0 -0
  37. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/ui/__init__.py +0 -0
  38. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/ui/list.py +0 -0
  39. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/ui/title.py +0 -0
  40. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/ui/title.txt +0 -0
  41. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/validators/__init__.py +0 -0
  42. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/validators/network.py +0 -0
  43. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/validators/numeric.py +0 -0
  44. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/validators/path.py +0 -0
  45. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/core/validators/string.py +0 -0
  46. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/services/__init__.py +0 -0
  47. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/services/command_service.py +0 -0
  48. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/templates/command.py.j2 +0 -0
  49. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  50. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  51. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  52. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  53. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  54. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  55. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  56. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/default.toml +0 -0
  57. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/dracula.toml +0 -0
  58. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  59. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/nord.toml +0 -0
  60. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  61. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/utils/__init__.py +0 -0
  62. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  63. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  64. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/menu.py +0 -0
  65. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/params.py +0 -0
  66. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/shared/__init__.py +0 -0
  67. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/shared/config/__init__.py +0 -0
  68. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/shared/config/globals.py +0 -0
  69. {usecli-0.1.60 → usecli-0.1.61}/src/usecli/ui.py +0 -0
  70. {usecli-0.1.60 → usecli-0.1.61}/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.60
3
+ Version: 0.1.61
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.60"
3
+ version = "0.1.61"
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" }]
@@ -39,6 +39,78 @@ _SKIP_DIRS = {
39
39
  "pipx",
40
40
  "venvs",
41
41
  }
42
+
43
+ _MAX_RGLOB_DEPTH = 6
44
+
45
+ _WALK_SKIP_ALWAYS: frozenset[str] = frozenset(
46
+ {
47
+ ".git",
48
+ "__pycache__",
49
+ "node_modules",
50
+ ".tox",
51
+ ".nox",
52
+ ".mypy_cache",
53
+ ".pytest_cache",
54
+ ".ruff_cache",
55
+ ".auto",
56
+ ".eggs",
57
+ "*.egg-info",
58
+ }
59
+ )
60
+
61
+ _WALK_SKIP_VENV: frozenset[str] = frozenset(
62
+ {
63
+ ".venv",
64
+ "venv",
65
+ "site-packages",
66
+ "dist-packages",
67
+ "__pypackages__",
68
+ "pipx",
69
+ "venvs",
70
+ }
71
+ )
72
+
73
+
74
+ def _walk_for_filename(
75
+ directory: Path,
76
+ filename: str,
77
+ depth: int,
78
+ max_depth: int,
79
+ skip_dirs: frozenset[str],
80
+ results: list[Path],
81
+ ) -> None:
82
+ if depth > max_depth:
83
+ return
84
+ try:
85
+ entries = list(directory.iterdir())
86
+ except (PermissionError, OSError):
87
+ return
88
+ for entry in entries:
89
+ try:
90
+ if entry.is_file() and entry.name == filename:
91
+ results.append(entry)
92
+ elif entry.is_dir() and entry.name not in skip_dirs:
93
+ _walk_for_filename(
94
+ entry, filename, depth + 1, max_depth, skip_dirs, results
95
+ )
96
+ except (PermissionError, OSError):
97
+ continue
98
+
99
+
100
+ def _rglob_limited(
101
+ root_dir: Path,
102
+ filename: str,
103
+ *,
104
+ skip_venv: bool = True,
105
+ max_depth: int = _MAX_RGLOB_DEPTH,
106
+ ) -> list[Path]:
107
+ """Depth-bounded recursive filename search that prunes dirs during the walk."""
108
+ skip_dirs = _WALK_SKIP_ALWAYS | _WALK_SKIP_VENV if skip_venv else _WALK_SKIP_ALWAYS
109
+ results: list[Path] = []
110
+ _walk_for_filename(root_dir, filename, 0, max_depth, skip_dirs, results)
111
+ return results
112
+
113
+
42
114
  DEFAULT_THEME_COLORS: dict[str, str] = {
43
115
  "primary": "#60D7FF",
44
116
  "secondary": "#5EFF87",
@@ -68,13 +140,7 @@ def _find_usecli_config_path(
68
140
  if not root_dir.exists() or not root_dir.is_dir():
69
141
  return None
70
142
 
71
- candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
72
- if skip_venv:
73
- candidates = [
74
- path
75
- for path in candidates
76
- if not any(part in _SKIP_DIRS for part in path.parts)
77
- ]
143
+ candidates = _rglob_limited(root_dir, USECLI_CONFIG_TOML, skip_venv=skip_venv)
78
144
  if not candidates:
79
145
  return None
80
146
 
@@ -109,17 +175,28 @@ def _get_command_name() -> str | None:
109
175
  return command if command else None
110
176
 
111
177
 
178
+ _distributions_cache: list[Any] | None = None
179
+
180
+
181
+ def _get_distributions() -> list[Any]:
182
+ global _distributions_cache
183
+ if _distributions_cache is not None:
184
+ return _distributions_cache
185
+ try:
186
+ import importlib.metadata
187
+
188
+ _distributions_cache = list(importlib.metadata.distributions())
189
+ except Exception:
190
+ _distributions_cache = []
191
+ return _distributions_cache
192
+
193
+
112
194
  def _get_console_script_aliases(command_name: str | None) -> set[str]:
113
195
  """Get all aliases for a console script from package metadata."""
114
196
  if not command_name:
115
197
  return set()
116
198
  aliases: set[str] = {command_name}
117
- try:
118
- import importlib.metadata
119
-
120
- distributions = importlib.metadata.distributions()
121
- except Exception:
122
- return aliases
199
+ distributions = _get_distributions()
123
200
  for dist in distributions:
124
201
  try:
125
202
  entry_points = dist.entry_points
@@ -163,13 +240,7 @@ def _find_usecli_config_path_for_command(
163
240
  if not root_dir.exists() or not root_dir.is_dir():
164
241
  return None
165
242
 
166
- candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
167
- if skip_venv:
168
- candidates = [
169
- path
170
- for path in candidates
171
- if not any(part in _SKIP_DIRS for part in path.parts)
172
- ]
243
+ candidates = _rglob_limited(root_dir, USECLI_CONFIG_TOML, skip_venv=skip_venv)
173
244
  if not candidates:
174
245
  return None
175
246
 
@@ -217,9 +288,7 @@ def _find_usecli_config_in_package() -> Path | None:
217
288
  package_root = Path(location)
218
289
  if not package_root.exists() or not package_root.is_dir():
219
290
  continue
220
- candidates = [
221
- path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
222
- ]
291
+ candidates = _rglob_limited(package_root, USECLI_CONFIG_TOML, skip_venv=False)
223
292
  if candidates:
224
293
  candidates.sort(key=lambda path: (len(path.parts), str(path)))
225
294
  return candidates[0]
@@ -238,9 +307,7 @@ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
238
307
  package_root = Path(location)
239
308
  if not package_root.exists() or not package_root.is_dir():
240
309
  continue
241
- candidates = [
242
- path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
243
- ]
310
+ candidates = _rglob_limited(package_root, USECLI_CONFIG_TOML, skip_venv=False)
244
311
  if candidates:
245
312
  candidates.sort(key=lambda path: (len(path.parts), str(path)))
246
313
  return candidates[0]
@@ -248,15 +315,10 @@ def _find_usecli_config_in_named_package(package_name: str) -> Path | None:
248
315
 
249
316
 
250
317
  def _find_usecli_config_for_console_script() -> Path | None:
251
- import importlib.metadata
252
-
253
318
  command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
254
319
  if not command_name:
255
320
  return None
256
- try:
257
- distributions = importlib.metadata.distributions()
258
- except Exception:
259
- return None
321
+ distributions = _get_distributions()
260
322
  for dist in distributions:
261
323
  try:
262
324
  entry_points = dist.entry_points
@@ -334,10 +396,8 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
334
396
  current = parent
335
397
 
336
398
  search_root = git_root or start_dir.resolve()
337
- config_match = _find_usecli_config_path(search_root, start_dir, skip_venv=True)
338
- if config_match:
339
- return config_match.parent
340
399
 
400
+ # Try fast lookups before expensive rglob (perf: global tools).
341
401
  console_match = _find_usecli_config_for_console_script()
342
402
  if console_match:
343
403
  return console_match.parent
@@ -346,6 +406,10 @@ def _find_project_root(start_dir: Path | None = None) -> Path | None:
346
406
  if package_match:
347
407
  return package_match.parent
348
408
 
409
+ config_match = _find_usecli_config_path(search_root, start_dir, skip_venv=True)
410
+ if config_match:
411
+ return config_match.parent
412
+
349
413
  return git_root
350
414
 
351
415
 
@@ -357,18 +421,19 @@ def _load_usecli_config(
357
421
 
358
422
  config_path = project_root / USECLI_CONFIG_TOML
359
423
  if not config_path.exists():
360
- config_path = _find_usecli_config_path_for_command(
361
- project_root,
362
- project_root,
363
- skip_venv=True,
364
- )
365
- if not config_path or not config_path.exists():
424
+ # Try fast lookups before expensive rglob (perf: global tools).
366
425
  console_match = _find_usecli_config_for_console_script()
367
426
  if console_match:
368
427
  return _load_usecli_config_file(console_match), console_match
369
428
  package_match = _find_usecli_config_in_package()
370
429
  if package_match:
371
430
  config_path = package_match
431
+ if not config_path or not config_path.exists():
432
+ config_path = _find_usecli_config_path_for_command(
433
+ project_root,
434
+ project_root,
435
+ skip_venv=True,
436
+ )
372
437
  if not config_path or not config_path.exists():
373
438
  return {}, None
374
439
 
@@ -18,6 +18,77 @@ if sys.version_info >= (3, 11):
18
18
  else:
19
19
  import tomli as tomllib
20
20
 
21
+ # Depth cap for rglob – prevents scanning massive trees like ~/ghq.
22
+ _MAX_RGLOB_DEPTH = 6
23
+
24
+ _WALK_SKIP_ALWAYS: frozenset[str] = frozenset(
25
+ {
26
+ ".git",
27
+ "__pycache__",
28
+ "node_modules",
29
+ ".tox",
30
+ ".nox",
31
+ ".mypy_cache",
32
+ ".pytest_cache",
33
+ ".ruff_cache",
34
+ ".auto",
35
+ ".eggs",
36
+ "*.egg-info",
37
+ }
38
+ )
39
+
40
+ _WALK_SKIP_VENV: frozenset[str] = frozenset(
41
+ {
42
+ ".venv",
43
+ "venv",
44
+ "site-packages",
45
+ "dist-packages",
46
+ "__pypackages__",
47
+ "pipx",
48
+ "venvs",
49
+ }
50
+ )
51
+
52
+
53
+ def _walk_for_filename(
54
+ directory: Path,
55
+ filename: str,
56
+ depth: int,
57
+ max_depth: int,
58
+ skip_dirs: frozenset[str],
59
+ results: list[Path],
60
+ ) -> None:
61
+ if depth > max_depth:
62
+ return
63
+ try:
64
+ entries = list(directory.iterdir())
65
+ except (PermissionError, OSError):
66
+ return
67
+ for entry in entries:
68
+ try:
69
+ if entry.is_file() and entry.name == filename:
70
+ results.append(entry)
71
+ elif entry.is_dir() and entry.name not in skip_dirs:
72
+ _walk_for_filename(
73
+ entry, filename, depth + 1, max_depth, skip_dirs, results
74
+ )
75
+ except (PermissionError, OSError):
76
+ continue
77
+
78
+
79
+ def _rglob_limited(
80
+ root_dir: Path,
81
+ filename: str,
82
+ *,
83
+ skip_venv: bool = True,
84
+ max_depth: int = _MAX_RGLOB_DEPTH,
85
+ ) -> list[Path]:
86
+ """Depth-bounded recursive filename search that prunes dirs during the walk."""
87
+ skip_dirs = _WALK_SKIP_ALWAYS | _WALK_SKIP_VENV if skip_venv else _WALK_SKIP_ALWAYS
88
+ results: list[Path] = []
89
+ _walk_for_filename(root_dir, filename, 0, max_depth, skip_dirs, results)
90
+ return results
91
+
21
92
 
22
93
  def _get_importlib_metadata():
23
94
  import importlib.metadata
@@ -25,6 +96,26 @@ def _get_importlib_metadata():
25
96
  return importlib.metadata
26
97
 
27
98
 
99
+ _distributions_cache: list[Any] | None = None
100
+
101
+
102
+ def _get_distributions() -> list[Any]:
103
+ global _distributions_cache
104
+ if _distributions_cache is not None:
105
+ return _distributions_cache
106
+ try:
107
+ metadata = _get_importlib_metadata()
108
+ _distributions_cache = list(metadata.distributions())
109
+ except Exception:
110
+ _distributions_cache = []
111
+ return _distributions_cache
112
+
113
+
114
+ def _reset_distributions_cache() -> None:
115
+ global _distributions_cache
116
+ _distributions_cache = None
117
+
118
+
28
119
  def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
29
120
  result = base.copy()
30
121
  for key, value in override.items():
@@ -220,6 +311,20 @@ class ConfigManager:
220
311
  break
221
312
  current = parent
222
313
 
314
+ # Try fast lookups before expensive rglob (perf: global tools).
315
+ console_match = cls._find_usecli_config_for_console_script()
316
+ if console_match:
317
+ return console_match
318
+
319
+ if cls._is_within_usecli_package(start_dir):
320
+ package_match = cls._find_usecli_config_in_package()
321
+ if package_match:
322
+ return package_match
323
+
324
+ sys_match = cls._find_usecli_config_on_sys_path()
325
+ if sys_match:
326
+ return sys_match
327
+
223
328
  search_root = find_project_root(start_dir) or start_dir.resolve()
224
329
  is_framework = command_name == "usecli" if command_name else True
225
330
  recursive_match = cls._find_usecli_config_in_tree(
@@ -228,18 +333,7 @@ class ConfigManager:
228
333
  if recursive_match:
229
334
  return recursive_match
230
335
 
231
- console_match = cls._find_usecli_config_for_console_script()
232
- if console_match:
233
- return console_match
234
-
235
- if not cls._is_within_usecli_package(start_dir):
236
- return None
237
-
238
- package_match = cls._find_usecli_config_in_package()
239
- if package_match:
240
- return package_match
241
-
242
- return cls._find_usecli_config_on_sys_path()
336
+ return None
243
337
 
244
338
  @staticmethod
245
339
  def _find_usecli_config_in_tree(
@@ -248,13 +342,7 @@ class ConfigManager:
248
342
  if not root_dir.exists() or not root_dir.is_dir():
249
343
  return None
250
344
 
251
- candidates = [path for path in root_dir.rglob(USECLI_CONFIG_TOML)]
252
- if skip_venv:
253
- candidates = [
254
- path
255
- for path in candidates
256
- if not any(part in ConfigManager._SKIP_DIRS for part in path.parts)
257
- ]
345
+ candidates = _rglob_limited(root_dir, USECLI_CONFIG_TOML, skip_venv=skip_venv)
258
346
  command_name = ConfigManager._get_command_name()
259
347
  if command_name:
260
348
  candidates = [
@@ -313,9 +401,9 @@ class ConfigManager:
313
401
  package_root = Path(location)
314
402
  if not package_root.exists() or not package_root.is_dir():
315
403
  continue
316
- candidates = [
317
- path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
318
- ]
404
+ candidates = _rglob_limited(
405
+ package_root, USECLI_CONFIG_TOML, skip_venv=False
406
+ )
319
407
  if command_name:
320
408
  candidates = [
321
409
  path
@@ -354,9 +442,9 @@ class ConfigManager:
354
442
  package_root = Path(location)
355
443
  if not package_root.exists() or not package_root.is_dir():
356
444
  continue
357
- candidates = [
358
- path for path in package_root.rglob(USECLI_CONFIG_TOML) if path.exists()
359
- ]
445
+ candidates = _rglob_limited(
446
+ package_root, USECLI_CONFIG_TOML, skip_venv=False
447
+ )
360
448
  if command_name:
361
449
  candidates = [
362
450
  path
@@ -373,11 +461,7 @@ class ConfigManager:
373
461
  command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
374
462
  if not command_name:
375
463
  return None
376
- try:
377
- metadata = _get_importlib_metadata()
378
- distributions = metadata.distributions()
379
- except Exception:
380
- return None
464
+ distributions = _get_distributions()
381
465
  for dist in distributions:
382
466
  try:
383
467
  entry_points = dist.entry_points
@@ -478,11 +562,7 @@ class ConfigManager:
478
562
  if not command_name:
479
563
  return set()
480
564
  aliases: set[str] = {command_name}
481
- try:
482
- metadata = _get_importlib_metadata()
483
- distributions = metadata.distributions()
484
- except Exception:
485
- return aliases
565
+ distributions = _get_distributions()
486
566
  for dist in distributions:
487
567
  try:
488
568
  entry_points = dist.entry_points
@@ -571,11 +651,7 @@ class ConfigManager:
571
651
  """Search a source tree for a ``usecli.config.toml`` that matches."""
572
652
  if not source_root.exists() or not source_root.is_dir():
573
653
  return None
574
- candidates = [
575
- p
576
- for p in source_root.rglob(USECLI_CONFIG_TOML)
577
- if not any(part in ConfigManager._SKIP_DIRS for part in p.parts)
578
- ]
654
+ candidates = _rglob_limited(source_root, USECLI_CONFIG_TOML)
579
655
  if command_name:
580
656
  candidates = [
581
657
  p
@@ -685,11 +761,7 @@ class ConfigManager:
685
761
  project_root = find_project_root(start_dir)
686
762
  if project_root is None:
687
763
  return None
688
- candidates = [
689
- p
690
- for p in project_root.rglob(USECLI_CONFIG_TOML)
691
- if not any(part in self._SKIP_DIRS for part in p.parts)
692
- ]
764
+ candidates = _rglob_limited(project_root, USECLI_CONFIG_TOML)
693
765
  if not candidates:
694
766
  return None
695
767
  candidates.sort(key=lambda p: (len(p.parts), str(p)))
@@ -821,6 +893,17 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
821
893
  current = parent
822
894
 
823
895
  search_root = git_root or start_dir.resolve()
896
+
897
+ # Try fast lookups before expensive rglob (perf: global tools).
898
+ console_match = ConfigManager._find_usecli_config_for_console_script()
899
+ if console_match:
900
+ return console_match.parent
901
+
902
+ if ConfigManager._is_within_usecli_package(start_dir):
903
+ package_match = ConfigManager._find_usecli_config_in_package()
904
+ if package_match:
905
+ return package_match.parent
906
+
824
907
  config_match = ConfigManager._find_usecli_config_in_tree(
825
908
  search_root,
826
909
  start_dir,
@@ -829,11 +912,6 @@ def find_project_root(start_dir: Path | None = None) -> Path | None:
829
912
  if config_match:
830
913
  return config_match.parent
831
914
 
832
- if ConfigManager._is_within_usecli_package(start_dir):
833
- package_match = ConfigManager._find_usecli_config_in_package()
834
- if package_match:
835
- return package_match.parent
836
-
837
915
  return git_root
838
916
 
839
917
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes