usecli 0.1.58__tar.gz → 0.1.60__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 (73) hide show
  1. {usecli-0.1.58 → usecli-0.1.60}/PKG-INFO +4 -13
  2. {usecli-0.1.58 → usecli-0.1.60}/README.md +3 -12
  3. {usecli-0.1.58 → usecli-0.1.60}/pyproject.toml +1 -1
  4. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/__init__.py +72 -11
  5. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/about_command.py +67 -32
  6. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/help_command.py +2 -1
  7. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/inspire_command.py +4 -6
  8. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/core/utils.py +16 -3
  9. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/make_command.py +8 -4
  10. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/make_theme_command.py +7 -3
  11. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/init_command.py +31 -6
  12. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/config/colors.py +69 -18
  13. usecli-0.1.60/src/usecli/cli/core/__init__.py +62 -0
  14. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/base_command.py +20 -7
  15. usecli-0.1.60/src/usecli/cli/core/exceptions/__init__.py +30 -0
  16. usecli-0.1.60/src/usecli/cli/core/ui/__init__.py +46 -0
  17. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/ui/title.py +1 -2
  18. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/services/command_service.py +49 -9
  19. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -3
  20. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/shared/config/manager.py +83 -37
  21. usecli-0.1.58/src/usecli/cli/core/__init__.py +0 -66
  22. usecli-0.1.58/src/usecli/cli/core/exceptions/__init__.py +0 -16
  23. usecli-0.1.58/src/usecli/cli/core/ui/__init__.py +0 -31
  24. {usecli-0.1.58 → usecli-0.1.60}/LICENSE +0 -0
  25. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/__init__.py +0 -0
  26. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/README.md +0 -0
  27. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/__init__.py +0 -0
  28. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/custom/README.md +0 -0
  29. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/custom/__init__.py +0 -0
  30. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  31. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  32. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  33. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
  34. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  35. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  36. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/config/__init__.py +0 -0
  37. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/error/__init__.py +0 -0
  38. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/error/handler.py +0 -0
  39. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/error/utils.py +0 -0
  40. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/exceptions/base.py +0 -0
  41. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/exceptions/config.py +0 -0
  42. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/exceptions/usage.py +0 -0
  43. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/exceptions/validation.py +0 -0
  44. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/ui/list.py +0 -0
  45. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/ui/title.txt +0 -0
  46. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/validators/__init__.py +0 -0
  47. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/validators/network.py +0 -0
  48. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/validators/numeric.py +0 -0
  49. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/validators/path.py +0 -0
  50. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/core/validators/string.py +0 -0
  51. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/services/__init__.py +0 -0
  52. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/templates/command.py.j2 +0 -0
  53. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  54. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  55. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  56. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  57. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  58. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  59. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/default.toml +0 -0
  60. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/dracula.toml +0 -0
  61. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  62. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/nord.toml +0 -0
  63. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  64. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/utils/__init__.py +0 -0
  65. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  66. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
  67. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/menu.py +0 -0
  68. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/params.py +0 -0
  69. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/shared/__init__.py +0 -0
  70. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/shared/config/__init__.py +0 -0
  71. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/shared/config/globals.py +0 -0
  72. {usecli-0.1.58 → usecli-0.1.60}/src/usecli/ui.py +0 -0
  73. {usecli-0.1.58 → usecli-0.1.60}/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.58
3
+ Version: 0.1.60
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>
@@ -182,19 +182,10 @@ choice = Menu(["A", "B", "C"]).show()
182
182
  ```
183
183
  about Show app info
184
184
  help Show help
185
- init Initialize usecli
185
+ init Initialize usecli (usecli only)
186
186
  inspire Random quote
187
- make:command Create new command
188
- ```
189
-
190
- ### Hiding Built-in Commands
191
-
192
- Add this to `usecli.config.toml`:
193
-
194
- ```toml
195
- [usecli]
196
- hide_init = true
197
- hide_inspire = true
187
+ make:command Create new command (usecli only)
188
+ make:theme Create new theme (usecli only)
198
189
  ```
199
190
 
200
191
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
@@ -154,19 +154,10 @@ choice = Menu(["A", "B", "C"]).show()
154
154
  ```
155
155
  about Show app info
156
156
  help Show help
157
- init Initialize usecli
157
+ init Initialize usecli (usecli only)
158
158
  inspire Random quote
159
- make:command Create new command
160
- ```
161
-
162
- ### Hiding Built-in Commands
163
-
164
- Add this to `usecli.config.toml`:
165
-
166
- ```toml
167
- [usecli]
168
- hide_init = true
169
- hide_inspire = true
159
+ make:command Create new command (usecli only)
160
+ make:theme Create new theme (usecli only)
170
161
  ```
171
162
 
172
163
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.58"
3
+ version = "0.1.60"
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,16 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import shutil
6
5
  import sys
7
6
  from importlib import import_module
8
- from typing import Any, Optional, Sequence
7
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
9
8
 
10
9
  import click
11
10
  import typer
12
11
  from click.exceptions import BadParameter, ClickException, Exit, UsageError
13
12
  from typer.core import TyperGroup
14
13
 
14
+ if TYPE_CHECKING:
15
+ from rich.console import Console
16
+
17
+ from usecli.menu import Menu
18
+ from usecli.params import Argument, Option
19
+ from usecli.ui import Confirm, Prompt
20
+
21
+ console: Console
22
+
15
23
  try:
16
24
  from typer._click.exceptions import BadParameter as TyperBadParameter # type: ignore[import-untyped]
17
25
  from typer._click.exceptions import ClickException as TyperClickException # type: ignore[import-untyped]
@@ -23,13 +31,8 @@ except ImportError:
23
31
 
24
32
  from usecli.cli.config.colors import COLOR
25
33
  from usecli.cli.core.base_command import BaseCommand
26
- from usecli.cli.core.exceptions import UsecliBadParameter, UsecliUsageError
27
- from usecli.cli.core.ui.list import list_commands
28
34
  from usecli.cli.services.command_service import CommandService
29
- from usecli.menu import Menu
30
- from usecli.params import Argument, Option
31
35
  from usecli.shared.config.manager import get_config
32
- from usecli.ui import Confirm, Console, Prompt, console
33
36
 
34
37
  colors = import_module("usecli.cli.config.colors")
35
38
  theme = COLOR
@@ -37,6 +40,27 @@ theme = COLOR
37
40
  sys.modules.setdefault(__name__ + ".colors", colors)
38
41
  sys.modules.setdefault("colors", colors)
39
42
 
43
+ _LAZY_EXPORTS = {
44
+ "Menu": ("usecli.menu", "Menu"),
45
+ "Argument": ("usecli.params", "Argument"),
46
+ "Option": ("usecli.params", "Option"),
47
+ "Prompt": ("usecli.ui", "Prompt"),
48
+ "Confirm": ("usecli.ui", "Confirm"),
49
+ "Console": ("usecli.ui", "Console"),
50
+ "console": ("usecli.ui", "console"),
51
+ }
52
+
53
+
54
+ def __getattr__(name: str) -> Any:
55
+ export = _LAZY_EXPORTS.get(name)
56
+ if export is None:
57
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
58
+ module_name, attr_name = export
59
+ value = getattr(import_module(module_name), attr_name)
60
+ globals()[name] = value
61
+ return value
62
+
63
+
40
64
  __all__ = [
41
65
  "BaseCommand",
42
66
  "console",
@@ -52,6 +76,10 @@ __all__ = [
52
76
  ]
53
77
 
54
78
 
79
+ def _console():
80
+ return __getattr__("console")
81
+
82
+
55
83
  def _is_interactive_flag_present() -> bool:
56
84
  """Check if -i/--interactive flag is present in sys.argv.
57
85
 
@@ -206,10 +234,14 @@ class PrefixMatchingGroup(TyperGroup):
206
234
  except Exit:
207
235
  sys.exit(0)
208
236
  except (BadParameter, TyperBadParameter) as e:
237
+ from usecli.cli.core.exceptions import UsecliBadParameter
238
+
209
239
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
210
240
  styled_error.show()
211
241
  sys.exit(styled_error.exit_code)
212
242
  except (UsageError, TyperUsageError) as e:
243
+ from usecli.cli.core.exceptions import UsecliUsageError
244
+
213
245
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
214
246
  styled_error.show()
215
247
  sys.exit(styled_error.exit_code)
@@ -241,18 +273,34 @@ class FilteredListCommand(click.Command):
241
273
  Args:
242
274
  ctx: The Click context.
243
275
  """
276
+ from usecli.cli.core.ui.list import list_commands
277
+
244
278
  list_commands(app, prefix_filter=self.prefix_filter)
245
279
  return None
246
280
 
247
281
 
282
+ def _get_default_help() -> str:
283
+ return "Usecli CLI - An elegant CLI framework for Python"
284
+
285
+
248
286
  app = typer.Typer(
249
- help=_get_cli_help_text(),
287
+ help=_get_default_help(),
250
288
  invoke_without_command=True,
251
289
  no_args_is_help=False,
252
290
  cls=PrefixMatchingGroup,
253
291
  pretty_exceptions_enable=False, # Use custom error styling
254
292
  )
255
293
 
294
+ _help_resolved = False
295
+
296
+
297
+ def _resolve_help():
298
+ global _help_resolved
299
+ if not _help_resolved:
300
+ app.info.help = _get_cli_help_text()
301
+ _help_resolved = True
302
+
303
+
256
304
  service = CommandService(app)
257
305
  service.load_commands()
258
306
 
@@ -278,14 +326,20 @@ def run_app(
278
326
  version: Flag to show version and exit.
279
327
  help: Flag to show help and exit.
280
328
  """
329
+ _resolve_help()
330
+
281
331
  if help:
332
+ from usecli.cli.core.ui.list import list_commands
333
+
282
334
  list_commands(app)
283
335
  raise typer.Exit()
284
336
 
285
337
  if version:
338
+ import shutil
339
+
286
340
  config = get_config()
287
341
  command_path = shutil.which(sys.argv[0]) or sys.argv[0]
288
- console.print(
342
+ _console().print(
289
343
  f"[bold {theme.SECONDARY}]{config.get('title')} {service.version}[/bold {theme.SECONDARY}] [{theme.INFO}]({command_path})[/{theme.INFO}]"
290
344
  )
291
345
  raise typer.Exit()
@@ -305,18 +359,21 @@ def run_app(
305
359
  prefix_filter: str | None = None
306
360
  if ctx.obj and isinstance(ctx.obj, dict):
307
361
  prefix_filter = ctx.obj.get("prefix_filter")
362
+ from usecli.cli.core.ui.list import list_commands
363
+
308
364
  list_commands(app, prefix_filter=prefix_filter)
309
365
 
310
366
 
311
367
  def main() -> None:
312
368
  """Run the CLI application with custom error handling."""
369
+ _resolve_help()
313
370
  config = get_config()
314
371
  command_name = config._get_command_name()
315
372
  if command_name == "usecli" and not config.is_usecli_direct_dependency():
316
- console.print(
373
+ _console().print(
317
374
  "[bold red]Error:[/bold red] usecli is not a direct dependency of this project."
318
375
  )
319
- console.print(
376
+ _console().print(
320
377
  "Add it to your [cyan]pyproject.toml[/cyan] dependencies or dependency-groups."
321
378
  )
322
379
  sys.exit(1)
@@ -326,10 +383,14 @@ def main() -> None:
326
383
  except Exit:
327
384
  sys.exit(0)
328
385
  except (BadParameter, TyperBadParameter) as e:
386
+ from usecli.cli.core.exceptions import UsecliBadParameter
387
+
329
388
  styled_error = UsecliBadParameter(e.message, ctx=e.ctx, param=e.param)
330
389
  styled_error.show()
331
390
  sys.exit(styled_error.exit_code)
332
391
  except (UsageError, TyperUsageError) as e:
392
+ from usecli.cli.core.exceptions import UsecliUsageError
393
+
333
394
  styled_error = UsecliUsageError(e.message, ctx=e.ctx)
334
395
  styled_error.show()
335
396
  sys.exit(styled_error.exit_code)
@@ -1,30 +1,53 @@
1
1
  from __future__ import annotations
2
2
 
3
- import importlib.metadata
4
3
  import os
5
- import platform
6
- import re
7
4
  import sys
8
- from importlib.metadata import PackageNotFoundError
9
- from importlib.metadata import version as get_version
10
5
  from pathlib import Path
11
6
 
12
- if sys.version_info >= (3, 11):
13
- import tomllib
14
- else:
15
- import tomli as tomllib
16
-
17
- from rich.console import Console
18
-
19
7
  from usecli.cli.config.colors import COLOR
20
8
  from usecli.cli.core.base_command import BaseCommand
21
- from usecli.cli.core.ui.title import get_project_name, get_script_command_name
22
- from usecli.shared.config.manager import ConfigManager, get_config
9
+ from usecli.shared.config.manager import get_config
10
+
11
+
12
+ class _LazyConsole:
13
+ _console = None
14
+
15
+ def _get_console(self):
16
+ if self._console is None:
17
+ from rich.console import Console
18
+
19
+ self._console = Console()
20
+ return self._console
21
+
22
+ def __getattr__(self, name):
23
+ return getattr(self._get_console(), name)
24
+
25
+
26
+ console = _LazyConsole()
27
+
28
+
29
+ def _load_toml(text: str):
30
+ if sys.version_info >= (3, 11):
31
+ import tomllib
32
+ else:
33
+ import tomli as tomllib
34
+
35
+ return tomllib.loads(text)
23
36
 
24
- console = Console()
37
+
38
+ def _toml_decode_error():
39
+ if sys.version_info >= (3, 11):
40
+ pass
41
+ else:
42
+ pass
43
+
44
+ return _toml_decode_error()
25
45
 
26
46
 
27
47
  def _get_version() -> str:
48
+ from importlib.metadata import PackageNotFoundError
49
+ from importlib.metadata import version as get_version
50
+
28
51
  try:
29
52
  return get_version("usecli")
30
53
  except PackageNotFoundError:
@@ -36,6 +59,8 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
36
59
  if not req_core:
37
60
  return "", None
38
61
 
62
+ import re
63
+
39
64
  match = re.match(r"^([A-Za-z0-9_.-]+)", req_core)
40
65
  if not match:
41
66
  return "", None
@@ -57,11 +82,11 @@ def _parse_dependency_requirement(req: str) -> tuple[str, str | None]:
57
82
  return name, remainder
58
83
 
59
84
 
60
- def _get_console_script_distribution(
61
- command_name: str | None,
62
- ) -> importlib.metadata.Distribution | None:
85
+ def _get_console_script_distribution(command_name: str | None):
63
86
  if not command_name:
64
87
  return None
88
+ import importlib.metadata
89
+
65
90
  try:
66
91
  distributions = importlib.metadata.distributions()
67
92
  except Exception:
@@ -79,9 +104,7 @@ def _get_console_script_distribution(
79
104
  return None
80
105
 
81
106
 
82
- def _get_package_dependencies_from_distribution(
83
- dist: importlib.metadata.Distribution,
84
- ) -> list[tuple[str, str | None]]:
107
+ def _get_package_dependencies_from_distribution(dist) -> list[tuple[str, str | None]]:
85
108
  requires = dist.requires or []
86
109
  result: list[tuple[str, str | None]] = []
87
110
  for req in requires:
@@ -93,10 +116,12 @@ def _get_package_dependencies_from_distribution(
93
116
  return result
94
117
 
95
118
 
96
- def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
119
+ def _get_dependencies(config) -> list[tuple[str, str | None]]:
97
120
  command_name = os.path.basename(sys.argv[0]) if sys.argv else None
98
121
  dist = _get_console_script_distribution(command_name)
99
122
  if dist is None:
123
+ from usecli.cli.core.ui.title import get_script_command_name
124
+
100
125
  primary_command = get_script_command_name(default=None)
101
126
  dist = _get_console_script_distribution(primary_command)
102
127
  if dist is not None:
@@ -107,8 +132,8 @@ def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
107
132
  return []
108
133
 
109
134
  try:
110
- data = tomllib.loads(pyproject_path.read_text())
111
- except (tomllib.TOMLDecodeError, OSError):
135
+ data = _load_toml(pyproject_path.read_text())
136
+ except (_toml_decode_error(), OSError):
112
137
  return []
113
138
 
114
139
  deps = data.get("project", {}).get("dependencies", [])
@@ -125,16 +150,18 @@ def _get_dependencies(config: ConfigManager) -> list[tuple[str, str | None]]:
125
150
  return result
126
151
 
127
152
 
128
- def _get_application_distribution() -> importlib.metadata.Distribution | None:
153
+ def _get_application_distribution():
129
154
  command_name = os.path.basename(sys.argv[0]) if sys.argv else None
130
155
  dist = _get_console_script_distribution(command_name)
131
156
  if dist is None:
157
+ from usecli.cli.core.ui.title import get_script_command_name
158
+
132
159
  primary_command = get_script_command_name(default=None)
133
160
  dist = _get_console_script_distribution(primary_command)
134
161
  return dist
135
162
 
136
163
 
137
- def _get_application_version(config: ConfigManager) -> str:
164
+ def _get_application_version(config) -> str:
138
165
  dist = _get_application_distribution()
139
166
  if dist is not None:
140
167
  return dist.version
@@ -146,7 +173,7 @@ def _get_application_version(config: ConfigManager) -> str:
146
173
  return _get_version()
147
174
 
148
175
 
149
- def _get_application_description(config: ConfigManager) -> str:
176
+ def _get_application_description(config) -> str:
150
177
  description = config.get("description")
151
178
  if (
152
179
  config.has_key("description")
@@ -165,14 +192,14 @@ def _get_application_description(config: ConfigManager) -> str:
165
192
  )
166
193
 
167
194
 
168
- def _get_project_description(config: ConfigManager) -> str | None:
195
+ def _get_project_description(config) -> str | None:
169
196
  pyproject_path = config.pyproject_path
170
197
  if not pyproject_path.exists():
171
198
  return None
172
199
 
173
200
  try:
174
- data = tomllib.loads(pyproject_path.read_text())
175
- except (tomllib.TOMLDecodeError, OSError):
201
+ data = _load_toml(pyproject_path.read_text())
202
+ except (_toml_decode_error(), OSError):
176
203
  return None
177
204
 
178
205
  description = data.get("project", {}).get("description")
@@ -202,6 +229,8 @@ def _get_installed_script_commands(command_name: str | None) -> list[str]:
202
229
 
203
230
 
204
231
  def _get_script_commands() -> list[str]:
232
+ from usecli.cli.core.ui.title import get_script_command_name
233
+
205
234
  primary_command = get_script_command_name(default=None)
206
235
  command_name = os.path.basename(sys.argv[0]) if sys.argv else primary_command
207
236
  installed_commands = _get_installed_script_commands(command_name)
@@ -217,8 +246,8 @@ def _get_script_commands() -> list[str]:
217
246
  return []
218
247
 
219
248
  try:
220
- data = tomllib.loads(pyproject_path.read_text())
221
- except (tomllib.TOMLDecodeError, OSError):
249
+ data = _load_toml(pyproject_path.read_text())
250
+ except (_toml_decode_error(), OSError):
222
251
  return []
223
252
 
224
253
  scripts = data.get("project", {}).get("scripts", {})
@@ -245,6 +274,8 @@ class AboutCommand(BaseCommand):
245
274
  def handle(self) -> None:
246
275
  config = get_config()
247
276
  version = _get_application_version(config)
277
+ from usecli.cli.core.ui.title import get_project_name
278
+
248
279
  app_name = get_project_name()
249
280
  description = _get_application_description(config)
250
281
 
@@ -262,6 +293,8 @@ class AboutCommand(BaseCommand):
262
293
  version_label = "Cli Version" if dist is not None else "Application Version"
263
294
  self._print_row(name_label, app_name)
264
295
  self._print_row(version_label, version)
296
+ import platform
297
+
265
298
  self._print_row("Python Version", platform.python_version())
266
299
  self._print_row("Platform", f"[{COLOR.FOREGROUND_MUTED}]{platform.platform()}")
267
300
 
@@ -282,6 +315,8 @@ class AboutCommand(BaseCommand):
282
315
  if deps:
283
316
  for dep_name, spec in deps:
284
317
  try:
318
+ from importlib.metadata import version as get_version
319
+
285
320
  installed_version = get_version(dep_name)
286
321
  self._print_row(dep_name, installed_version)
287
322
  except Exception:
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from usecli.cli.core.base_command import BaseCommand
6
- from usecli.cli.core.ui.list import list_commands
7
6
 
8
7
 
9
8
  class HelpCommand(BaseCommand):
@@ -19,4 +18,6 @@ class HelpCommand(BaseCommand):
19
18
 
20
19
  def handle(self) -> None:
21
20
  """Handle the command execution."""
21
+ from usecli.cli.core.ui.list import list_commands
22
+
22
23
  list_commands(self.app)
@@ -4,15 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import random
6
6
 
7
- from rich.console import Console
8
- from rich.panel import Panel
9
-
10
7
  from usecli.cli.config.colors import COLOR
11
8
  from usecli.cli.core.base_command import BaseCommand
12
9
  from usecli.shared.config.manager import get_config
13
10
 
14
- console = Console()
15
-
16
11
 
17
12
  class InspireCommand(BaseCommand):
18
13
  """Command for displaying random inspirational quotes."""
@@ -78,7 +73,10 @@ class InspireCommand(BaseCommand):
78
73
  selected = random.choice(quotes)
79
74
  quote, author = selected.rsplit(" - ", 1)
80
75
 
81
- console.print(
76
+ from rich.console import Console
77
+ from rich.panel import Panel
78
+
79
+ Console().print(
82
80
  Panel(
83
81
  f"{quote}\n\n[{COLOR.FOREGROUND_MUTED}]— {author}[/{COLOR.FOREGROUND_MUTED}]",
84
82
  border_style=COLOR.PANEL_PRIMARY,
@@ -8,12 +8,25 @@ from datetime import datetime, timedelta
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING
10
10
 
11
- from rich.console import Console
12
-
13
11
  if TYPE_CHECKING:
14
12
  pass
15
13
 
16
- console = Console()
14
+
15
+ class _LazyConsole:
16
+ _console = None
17
+
18
+ def _get_console(self):
19
+ if self._console is None:
20
+ from rich.console import Console
21
+
22
+ self._console = Console()
23
+ return self._console
24
+
25
+ def __getattr__(self, name):
26
+ return getattr(self._get_console(), name)
27
+
28
+
29
+ console = _LazyConsole()
17
30
 
18
31
 
19
32
  def is_interactive() -> bool:
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
6
+ import sys
5
7
  from pathlib import Path
6
8
 
7
9
  import typer
@@ -21,8 +23,8 @@ class MakeCommand(BaseCommand):
21
23
  """Command for generating new CLI command files."""
22
24
 
23
25
  def visible(self) -> bool:
24
- config = get_config()
25
- return not config.get("hide_make_command", False)
26
+ command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
27
+ return command_name == "usecli"
26
28
 
27
29
  def signature(self) -> str:
28
30
  """Return the command signature."""
@@ -50,7 +52,8 @@ class MakeCommand(BaseCommand):
50
52
  if config.get_project_root().resolve() != current_root:
51
53
  reset_config()
52
54
  config = get_config()
53
- commands_dir = config.get_project_commands_dir()
55
+ project_paths = config.get_project_paths()
56
+ commands_dir = project_paths["commands_dir"]
54
57
  commands_dir.mkdir(parents=True, exist_ok=True)
55
58
  target_file = commands_dir / file_name
56
59
 
@@ -59,7 +62,8 @@ class MakeCommand(BaseCommand):
59
62
  f"[{COLOR.ERROR}]Error: Command file {target_file} already exists.[/{COLOR.ERROR}]"
60
63
  )
61
64
  return
62
- project_template_path = config.get_project_templates_dir() / "command.py.j2"
65
+ templates_dir = project_paths["templates_dir"]
66
+ project_template_path = templates_dir / "command.py.j2"
63
67
  if project_template_path.exists():
64
68
  template_path = project_template_path
65
69
  else:
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
6
+ import sys
5
7
  from pathlib import Path
6
8
 
7
9
  import typer
@@ -23,8 +25,8 @@ console = Console()
23
25
 
24
26
  class MakeThemeCommand(BaseCommand):
25
27
  def visible(self) -> bool:
26
- config = get_config()
27
- return not config.get("hide_make_theme", False)
28
+ command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
29
+ return command_name == "usecli"
28
30
 
29
31
  def signature(self) -> str:
30
32
  return "make:theme"
@@ -46,6 +48,7 @@ class MakeThemeCommand(BaseCommand):
46
48
  reset_config()
47
49
  config = get_config()
48
50
 
51
+ project_paths = config.get_project_paths()
49
52
  themes_entries = self._normalize_theme_entries(config.get("themes_dir", []))
50
53
  if not themes_entries:
51
54
  console.print(
@@ -86,7 +89,8 @@ class MakeThemeCommand(BaseCommand):
86
89
  break
87
90
  counter += 1
88
91
 
89
- project_template_path = config.get_project_templates_dir() / "theme.toml.j2"
92
+ templates_dir = project_paths["templates_dir"]
93
+ project_template_path = templates_dir / "theme.toml.j2"
90
94
  if project_template_path.exists():
91
95
  template_path = project_template_path
92
96
  else:
@@ -29,15 +29,15 @@ from usecli.cli.core.exceptions import UsecliBadParameter
29
29
  from usecli.cli.core.validators import validate_command_name
30
30
  from usecli.cli.utils.interactive.terminal_menu import terminal_menu
31
31
  from usecli.shared.config.globals import TEMPLATES_DIR, THEMES_DIR, USECLI_CONFIG_TOML
32
- from usecli.shared.config.manager import ConfigManager, get_config
32
+ from usecli.shared.config.manager import ConfigManager
33
33
 
34
34
  console = Console()
35
35
 
36
36
 
37
37
  class InitCommand(BaseCommand):
38
38
  def visible(self) -> bool:
39
- config = get_config()
40
- return not config.get("hide_init", False)
39
+ command_name = os.path.basename(sys.argv[0]) if sys.argv else ""
40
+ return command_name == "usecli"
41
41
 
42
42
  def signature(self) -> str:
43
43
  return "init"
@@ -244,6 +244,30 @@ class InitCommand(BaseCommand):
244
244
  return "themes"
245
245
  return str(parent / "themes")
246
246
 
247
+ def _infer_commands_dir(self, project_root: Path) -> str:
248
+ src_dir = project_root / "src"
249
+ if src_dir.exists() and src_dir.is_dir():
250
+ packages = [
251
+ d
252
+ for d in src_dir.iterdir()
253
+ if d.is_dir() and not d.name.startswith((".", "_"))
254
+ ]
255
+ if len(packages) == 1:
256
+ package_name = packages[0].name
257
+ return f"src/{package_name}/cli/commands"
258
+ pyproject_path = project_root / "pyproject.toml"
259
+ if pyproject_path.exists():
260
+ try:
261
+ data = tomllib.loads(pyproject_path.read_text())
262
+ project_name = data.get("project", {}).get("name")
263
+ if project_name:
264
+ package_name = project_name.replace("-", "_").replace(" ", "_")
265
+ if (project_root / package_name).is_dir():
266
+ return f"{package_name}/cli/commands"
267
+ except (tomllib.TOMLDecodeError, OSError):
268
+ pass
269
+ return "cli/commands"
270
+
247
271
  def _get_existing_usecli_script_name(self, pyproject_path: Path) -> str | None:
248
272
  if not pyproject_path.exists():
249
273
  return None
@@ -532,9 +556,7 @@ include = ["{root_package}*"]
532
556
  description: str = typer.Option(
533
557
  "A custom CLI tool", help="Description for your CLI"
534
558
  ),
535
- commands_dir: str = typer.Option(
536
- "cli/commands", help="Directory for custom commands"
537
- ),
559
+ commands_dir: str = typer.Option(None, help="Directory for custom commands"),
538
560
  command_name: Annotated[
539
561
  str,
540
562
  typer.Option(
@@ -554,6 +576,9 @@ include = ["{root_package}*"]
554
576
  project_root / "pyproject.toml"
555
577
  )
556
578
 
579
+ if commands_dir is None:
580
+ commands_dir = self._infer_commands_dir(project_root)
581
+
557
582
  console.print()
558
583
  existing_command_name = self._get_existing_usecli_script_name(pyproject_path)
559
584
  if existing_command_name and command_name == "usecli":