usecli 0.1.76__tar.gz → 0.1.78__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 (77) hide show
  1. {usecli-0.1.76 → usecli-0.1.78}/PKG-INFO +115 -7
  2. {usecli-0.1.76 → usecli-0.1.78}/README.md +107 -1
  3. {usecli-0.1.76 → usecli-0.1.78}/pyproject.toml +26 -12
  4. {usecli-0.1.76 → usecli-0.1.78}/pyproject.toml.orig +23 -12
  5. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/__init__.py +57 -26
  6. usecli-0.1.78/src/usecli/bundler/__init__.py +275 -0
  7. usecli-0.1.78/src/usecli/bundler/__main__.py +6 -0
  8. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/about_command.py +5 -6
  9. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +24 -20
  10. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/core/utils.py +2 -6
  11. usecli-0.1.78/src/usecli/cli/commands/defaults/make/make_bundle_command.py +147 -0
  12. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/make/make_command.py +1 -3
  13. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/make/make_theme_command.py +1 -2
  14. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/init_command.py +4 -3
  15. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/config/colors.py +8 -5
  16. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/base_command.py +19 -20
  17. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/error/handler.py +4 -3
  18. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/exceptions/validation.py +2 -2
  19. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/ui/list.py +4 -6
  20. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/ui/title.py +4 -5
  21. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/services/command_service.py +0 -4
  22. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/utils/interactive/terminal_menu.py +5 -4
  23. usecli-0.1.78/src/usecli/entry.py +201 -0
  24. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/params.py +7 -6
  25. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/progress.py +7 -3
  26. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/shared/config/manager.py +85 -20
  27. {usecli-0.1.76 → usecli-0.1.78}/LICENSE +0 -0
  28. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/__init__.py +0 -0
  29. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/README.md +0 -0
  30. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/__init__.py +0 -0
  31. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/custom/README.md +0 -0
  32. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/custom/__init__.py +0 -0
  33. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/__init__.py +0 -0
  34. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
  35. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
  36. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
  37. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
  38. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
  39. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
  40. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/config/__init__.py +0 -0
  41. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/__init__.py +9 -9
  42. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/error/__init__.py +1 -1
  43. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/error/utils.py +0 -0
  44. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/exceptions/__init__.py +2 -2
  45. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/exceptions/base.py +0 -0
  46. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/exceptions/config.py +0 -0
  47. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/exceptions/usage.py +0 -0
  48. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/runtime.py +0 -0
  49. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/ui/__init__.py +1 -1
  50. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/ui/title.txt +0 -0
  51. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/validators/__init__.py +5 -5
  52. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/validators/network.py +0 -0
  53. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/validators/numeric.py +0 -0
  54. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/validators/path.py +0 -0
  55. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/core/validators/string.py +0 -0
  56. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/services/__init__.py +0 -0
  57. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/templates/command.py.j2 +0 -0
  58. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/templates/theme.toml.j2 +0 -0
  59. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/templates/usecli.config.toml.j2 +0 -0
  60. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/ayu_dark.toml +0 -0
  61. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
  62. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
  63. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
  64. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
  65. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/default.toml +0 -0
  66. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/dracula.toml +0 -0
  67. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
  68. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/nord.toml +0 -0
  69. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/themes/tokyo_night.toml +0 -0
  70. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/utils/__init__.py +0 -0
  71. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/cli/utils/interactive/__init__.py +0 -0
  72. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/menu.py +0 -0
  73. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/shared/__init__.py +0 -0
  74. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/shared/config/__init__.py +0 -0
  75. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/shared/config/globals.py +0 -0
  76. {usecli-0.1.76 → usecli-0.1.78}/src/usecli/ui.py +4 -4
  77. {usecli-0.1.76 → usecli-0.1.78}/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.76
3
+ Version: 0.1.78
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>
@@ -10,20 +10,22 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Intended Audience :: Developers
12
12
  Requires-Dist: case-converter>=1.2.0
13
- Requires-Dist: click>=8.3.1
14
- Requires-Dist: fzf-bin>=0.67.0
13
+ Requires-Dist: click>=8.4.2
14
+ Requires-Dist: fzf-bin>=0.74.1
15
15
  Requires-Dist: jinja2>=3.1.6
16
16
  Requires-Dist: pyfiglet>=1.0.4
17
- Requires-Dist: rich>=14.3.1
17
+ Requires-Dist: rich>=15.0.0
18
18
  Requires-Dist: simple-term-menu>=1.6.6
19
- Requires-Dist: tomli>=2.4.0
20
- Requires-Dist: typer>=0.21.1
19
+ Requires-Dist: tomli>=2.4.1
20
+ Requires-Dist: typer>=0.27.0
21
+ Requires-Dist: pyinstaller>=6.21.0 ; extra == 'pyinstaller'
21
22
  Requires-Python: >=3.10
22
23
  Project-URL: Homepage, https://github.com/thememium/usecli
23
24
  Project-URL: Documentation, https://github.com/thememium/usecli
24
25
  Project-URL: Repository, https://github.com/thememium/usecli.git
25
26
  Project-URL: Issues, https://github.com/thememium/usecli/issues
26
27
  Project-URL: Changelog, https://github.com/thememium/usecli/blob/master/CHANGELOG.md
28
+ Provides-Extra: pyinstaller
27
29
  Description-Content-Type: text/markdown
28
30
 
29
31
  <a name="readme-top"></a>
@@ -52,6 +54,7 @@ Description-Content-Type: text/markdown
52
54
  <li><a href="#about">About</a></li>
53
55
  <li><a href="#quick-start">Quick Start</a></li>
54
56
  <li><a href="#usage">Usage</a></li>
57
+ <li><a href="#entry-points--packaging">Entry Points &amp; Packaging</a></li>
55
58
  <li><a href="#development">Development</a></li>
56
59
  <li><a href="#contributing">Contributing</a></li>
57
60
  <li><a href="#license">License</a></li>
@@ -163,7 +166,7 @@ Space-separated signatures create nested subcommands:
163
166
 
164
167
  ```python
165
168
  def signature(self) -> str:
166
- return "spec show" # usecli spec show
169
+ return "spec show" # usecli spec show
167
170
  ```
168
171
 
169
172
  ### JSON Output
@@ -244,10 +247,115 @@ init Initialize usecli (usecli only)
244
247
  inspire Random quote
245
248
  make:command Create new command (usecli only)
246
249
  make:theme Create new theme (usecli only)
250
+ make:bundle Build a standalone executable with PyInstaller (usecli only)
247
251
  ```
248
252
 
249
253
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
250
254
 
255
+ <!-- ENTRY POINTS & PACKAGING -->
256
+
257
+ ## Entry Points &amp; Packaging
258
+
259
+ A usecli CLI can be exposed in several ways, depending on how you distribute and
260
+ run it. There are three common setups — none requires anything but the base
261
+ `usecli` install, and the optional bundler adds PyInstaller packaging on top.
262
+
263
+ ### 1. Default — console script (no entry point)
264
+
265
+ The simplest setup. Declare a console script in your project's `pyproject.toml`:
266
+
267
+ ```toml
268
+ [project.scripts]
269
+ mycli = "usecli:main"
270
+ ```
271
+
272
+ usecli resolves your project's config automatically from the current directory
273
+ (config discovery), so no `main.py` is required. Install and run it:
274
+
275
+ ```sh
276
+ uv add usecli
277
+ uv run mycli --help
278
+ ```
279
+
280
+ This is the **default** — no entry point file of your own is needed.
281
+
282
+ ### 2. Custom entry point — `main.py`
283
+
284
+ If you want your own entry point, use the public runtime API. It works in both
285
+ development and frozen bundles, and accepts an optional config path (the config
286
+ file is always named `usecli.config.toml`):
287
+
288
+ ```python
289
+ # main.py
290
+ from usecli import run
291
+
292
+ if __name__ == "__main__":
293
+ run() # auto-detect project config
294
+ # run("path/to/usecli.config.toml") # or point at a specific config
295
+ ```
296
+
297
+ Run it in development (no PyInstaller required):
298
+
299
+ ```sh
300
+ uv run python main.py --help
301
+ ```
302
+
303
+ Because `run()` injects the located config explicitly, a custom `main.py` works
304
+ regardless of whether your CLI also has a `[project.scripts]` entry.
305
+
306
+ ### 3. Bundler — standalone executable (optional, requires PyInstaller)
307
+
308
+ The bundler is an **optional** feature gated on PyInstaller being installed.
309
+ Install it with:
310
+
311
+ ```sh
312
+ uv add "usecli[pyinstaller]"
313
+ ```
314
+
315
+ Once installed, a `make:bundle` command appears (usecli only) that builds your
316
+ project into a standalone executable. It asks which mode to use via an
317
+ interactive menu, and confirms before building (defaults to no unless `-y`):
318
+
319
+ ```sh
320
+ uv run usecli make:bundle # menu: one file vs one folder + confirm
321
+ uv run usecli make:bundle --mode onefile # single-file executable
322
+ uv run usecli make:bundle --mode onedir # one-folder bundle
323
+ uv run usecli make:bundle -y # skip the confirmation
324
+ uv run usecli make:bundle --mode onedir -y # fully programmatic
325
+ uv run usecli make:bundle --mode onedir --zip -y # one-folder bundle + zip
326
+ ```
327
+
328
+ When building a **one-folder** bundle, `make:bundle` asks whether to also zip
329
+ the folder into `dist/<name>.zip` (PyInstaller's recommended distribution
330
+ format for one-folder mode). Pass `--zip` / `--no-zip` to skip the prompt and
331
+ force a choice; with `-y` and no flag, zipping is skipped.
332
+
333
+ A `usecli-bundle` console script wraps the same build, and you can drive it from
334
+ Python too:
335
+
336
+ ```sh
337
+ uv run usecli-bundle
338
+ uv run usecli-bundle --mode onedir
339
+ uv run usecli-bundle --mode onedir --zip
340
+ ```
341
+
342
+ ```python
343
+ from usecli import pyinstaller
344
+
345
+ pyinstaller() # auto-detect config, single-file
346
+ pyinstaller(mode="onedir") # one-folder bundle
347
+ pyinstaller(mode="onedir", zip=True) # one-folder bundle + zip
348
+ pyinstaller("path/to/usecli.config.toml") # or point at a specific config
349
+ ```
350
+
351
+ Output lands in `dist/` — `dist/mycli` for `onefile`, or a `dist/mycli/` folder
352
+ containing the executable (plus `_internal/` with the bundled assets) for
353
+ `onedir`. With `zip=True`, a `dist/mycli.zip` archive is created alongside the
354
+ folder. `make:bundle` and `usecli-bundle` are only available when PyInstaller
355
+ is installed.
356
+
357
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
358
+
251
359
  <!-- DEVELOPMENT -->
252
360
 
253
361
  ## Development
@@ -24,6 +24,7 @@
24
24
  <li><a href="#about">About</a></li>
25
25
  <li><a href="#quick-start">Quick Start</a></li>
26
26
  <li><a href="#usage">Usage</a></li>
27
+ <li><a href="#entry-points--packaging">Entry Points &amp; Packaging</a></li>
27
28
  <li><a href="#development">Development</a></li>
28
29
  <li><a href="#contributing">Contributing</a></li>
29
30
  <li><a href="#license">License</a></li>
@@ -135,7 +136,7 @@ Space-separated signatures create nested subcommands:
135
136
 
136
137
  ```python
137
138
  def signature(self) -> str:
138
- return "spec show" # usecli spec show
139
+ return "spec show" # usecli spec show
139
140
  ```
140
141
 
141
142
  ### JSON Output
@@ -216,10 +217,115 @@ init Initialize usecli (usecli only)
216
217
  inspire Random quote
217
218
  make:command Create new command (usecli only)
218
219
  make:theme Create new theme (usecli only)
220
+ make:bundle Build a standalone executable with PyInstaller (usecli only)
219
221
  ```
220
222
 
221
223
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
222
224
 
225
+ <!-- ENTRY POINTS & PACKAGING -->
226
+
227
+ ## Entry Points &amp; Packaging
228
+
229
+ A usecli CLI can be exposed in several ways, depending on how you distribute and
230
+ run it. There are three common setups — none requires anything but the base
231
+ `usecli` install, and the optional bundler adds PyInstaller packaging on top.
232
+
233
+ ### 1. Default — console script (no entry point)
234
+
235
+ The simplest setup. Declare a console script in your project's `pyproject.toml`:
236
+
237
+ ```toml
238
+ [project.scripts]
239
+ mycli = "usecli:main"
240
+ ```
241
+
242
+ usecli resolves your project's config automatically from the current directory
243
+ (config discovery), so no `main.py` is required. Install and run it:
244
+
245
+ ```sh
246
+ uv add usecli
247
+ uv run mycli --help
248
+ ```
249
+
250
+ This is the **default** — no entry point file of your own is needed.
251
+
252
+ ### 2. Custom entry point — `main.py`
253
+
254
+ If you want your own entry point, use the public runtime API. It works in both
255
+ development and frozen bundles, and accepts an optional config path (the config
256
+ file is always named `usecli.config.toml`):
257
+
258
+ ```python
259
+ # main.py
260
+ from usecli import run
261
+
262
+ if __name__ == "__main__":
263
+ run() # auto-detect project config
264
+ # run("path/to/usecli.config.toml") # or point at a specific config
265
+ ```
266
+
267
+ Run it in development (no PyInstaller required):
268
+
269
+ ```sh
270
+ uv run python main.py --help
271
+ ```
272
+
273
+ Because `run()` injects the located config explicitly, a custom `main.py` works
274
+ regardless of whether your CLI also has a `[project.scripts]` entry.
275
+
276
+ ### 3. Bundler — standalone executable (optional, requires PyInstaller)
277
+
278
+ The bundler is an **optional** feature gated on PyInstaller being installed.
279
+ Install it with:
280
+
281
+ ```sh
282
+ uv add "usecli[pyinstaller]"
283
+ ```
284
+
285
+ Once installed, a `make:bundle` command appears (usecli only) that builds your
286
+ project into a standalone executable. It asks which mode to use via an
287
+ interactive menu, and confirms before building (defaults to no unless `-y`):
288
+
289
+ ```sh
290
+ uv run usecli make:bundle # menu: one file vs one folder + confirm
291
+ uv run usecli make:bundle --mode onefile # single-file executable
292
+ uv run usecli make:bundle --mode onedir # one-folder bundle
293
+ uv run usecli make:bundle -y # skip the confirmation
294
+ uv run usecli make:bundle --mode onedir -y # fully programmatic
295
+ uv run usecli make:bundle --mode onedir --zip -y # one-folder bundle + zip
296
+ ```
297
+
298
+ When building a **one-folder** bundle, `make:bundle` asks whether to also zip
299
+ the folder into `dist/<name>.zip` (PyInstaller's recommended distribution
300
+ format for one-folder mode). Pass `--zip` / `--no-zip` to skip the prompt and
301
+ force a choice; with `-y` and no flag, zipping is skipped.
302
+
303
+ A `usecli-bundle` console script wraps the same build, and you can drive it from
304
+ Python too:
305
+
306
+ ```sh
307
+ uv run usecli-bundle
308
+ uv run usecli-bundle --mode onedir
309
+ uv run usecli-bundle --mode onedir --zip
310
+ ```
311
+
312
+ ```python
313
+ from usecli import pyinstaller
314
+
315
+ pyinstaller() # auto-detect config, single-file
316
+ pyinstaller(mode="onedir") # one-folder bundle
317
+ pyinstaller(mode="onedir", zip=True) # one-folder bundle + zip
318
+ pyinstaller("path/to/usecli.config.toml") # or point at a specific config
319
+ ```
320
+
321
+ Output lands in `dist/` — `dist/mycli` for `onefile`, or a `dist/mycli/` folder
322
+ containing the executable (plus `_internal/` with the bundled assets) for
323
+ `onedir`. With `zip=True`, a `dist/mycli.zip` archive is created alongside the
324
+ folder. `make:bundle` and `usecli-bundle` are only available when PyInstaller
325
+ is installed.
326
+
327
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
328
+
223
329
  <!-- DEVELOPMENT -->
224
330
 
225
331
  ## Development
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.76"
3
+ version = "0.1.78"
4
4
  description = "A powerful Python CLI framework for building beautiful, developer-friendly command-line tools."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,14 +11,14 @@ classifiers = [
11
11
  ]
12
12
  dependencies = [
13
13
  "case-converter>=1.2.0",
14
- "click>=8.3.1",
15
- "fzf-bin>=0.67.0",
14
+ "click>=8.4.2",
15
+ "fzf-bin>=0.74.1",
16
16
  "jinja2>=3.1.6",
17
17
  "pyfiglet>=1.0.4",
18
- "rich>=14.3.1",
18
+ "rich>=15.0.0",
19
19
  "simple-term-menu>=1.6.6",
20
- "tomli>=2.4.0",
21
- "typer>=0.21.1",
20
+ "tomli>=2.4.1",
21
+ "typer>=0.27.0",
22
22
  ]
23
23
  license = "MIT"
24
24
  license-files = ["LICEN[CS]E*"]
@@ -27,6 +27,9 @@ license-files = ["LICEN[CS]E*"]
27
27
  name = "Edward Boswell"
28
28
  email = "thememium@gmail.com"
29
29
 
30
+ [project.optional-dependencies]
31
+ pyinstaller = ["pyinstaller>=6.21.0"]
32
+
30
33
  [project.urls]
31
34
  Homepage = "https://github.com/thememium/usecli"
32
35
  Documentation = "https://github.com/thememium/usecli"
@@ -36,6 +39,7 @@ Changelog = "https://github.com/thememium/usecli/blob/master/CHANGELOG.md"
36
39
 
37
40
  [project.scripts]
38
41
  usecli = "usecli:main"
42
+ usecli-bundle = "usecli.bundler:cli"
39
43
 
40
44
  [build-system]
41
45
  requires = ["uv_build>=0.10.2,<0.11.0"]
@@ -43,13 +47,14 @@ build-backend = "uv_build"
43
47
 
44
48
  [dependency-groups]
45
49
  dev = [
46
- "deptry>=0.24.0",
47
- "isort>=7.0.0",
48
- "poethepoet>=0.41.0",
49
- "pytest>=9.0.2",
50
+ "deptry>=0.25.1",
51
+ "isort>=8.0.1",
52
+ "poethepoet>=0.48.0",
53
+ "pyinstaller>=6.21.0",
54
+ "pytest>=9.1.1",
50
55
  "pytest-cov>=7.1.0",
51
- "ruff>=0.15.1",
52
- "ty>=0.0.17",
56
+ "ruff>=0.16.1",
57
+ "ty>=0.0.65",
53
58
  ]
54
59
 
55
60
  [tool.deptry]
@@ -75,6 +80,9 @@ typecheck = "uv run ty check"
75
80
  build = "uv build --no-sources"
76
81
  release = "bash scripts/release.sh"
77
82
 
83
+ [tool.ty.environment]
84
+ python-version = "3.12"
85
+
78
86
  [tool.ty.src]
79
87
  include = [
80
88
  "src",
@@ -90,3 +98,9 @@ exclude = [
90
98
  [tool.setuptools.packages.find]
91
99
  where = ["."]
92
100
  include = ["cli*"]
101
+
102
+ [tool.setuptools.package-data]
103
+ cli = [
104
+ "**/*.toml",
105
+ "**/*.j2",
106
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usecli"
3
- version = "0.1.76"
3
+ version = "0.1.78"
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" }]
@@ -12,18 +12,21 @@ classifiers = [
12
12
  ]
13
13
  dependencies = [
14
14
  "case-converter>=1.2.0",
15
- "click>=8.3.1",
16
- "fzf-bin>=0.67.0",
15
+ "click>=8.4.2",
16
+ "fzf-bin>=0.74.1",
17
17
  "jinja2>=3.1.6",
18
18
  "pyfiglet>=1.0.4",
19
- "rich>=14.3.1",
19
+ "rich>=15.0.0",
20
20
  "simple-term-menu>=1.6.6",
21
- "tomli>=2.4.0",
22
- "typer>=0.21.1",
21
+ "tomli>=2.4.1",
22
+ "typer>=0.27.0",
23
23
  ]
24
24
  license = "MIT"
25
25
  license-files = ["LICEN[CS]E*"]
26
26
 
27
+ [project.optional-dependencies]
28
+ pyinstaller = ["pyinstaller>=6.21.0"]
29
+
27
30
  [project.urls]
28
31
  Homepage = "https://github.com/thememium/usecli"
29
32
  Documentation = "https://github.com/thememium/usecli"
@@ -33,6 +36,7 @@ Changelog = "https://github.com/thememium/usecli/blob/master/CHANGELOG.md"
33
36
 
34
37
  [project.scripts]
35
38
  usecli = "usecli:main"
39
+ usecli-bundle = "usecli.bundler:cli"
36
40
 
37
41
  [build-system]
38
42
  requires = ["uv_build>=0.10.2,<0.11.0"]
@@ -40,13 +44,14 @@ build-backend = "uv_build"
40
44
 
41
45
  [dependency-groups]
42
46
  dev = [
43
- "deptry>=0.24.0",
44
- "isort>=7.0.0",
45
- "poethepoet>=0.41.0",
46
- "pytest>=9.0.2",
47
+ "deptry>=0.25.1",
48
+ "isort>=8.0.1",
49
+ "poethepoet>=0.48.0",
50
+ "pyinstaller>=6.21.0",
51
+ "pytest>=9.1.1",
47
52
  "pytest-cov>=7.1.0",
48
- "ruff>=0.15.1",
49
- "ty>=0.0.17",
53
+ "ruff>=0.16.1",
54
+ "ty>=0.0.65",
50
55
  ]
51
56
 
52
57
  [tool.deptry]
@@ -74,6 +79,9 @@ release = "bash scripts/release.sh"
74
79
 
75
80
  [tool.ty]
76
81
 
82
+ [tool.ty.environment]
83
+ python-version = "3.12"
84
+
77
85
  [tool.ty.src]
78
86
  include = ["src", "tests"]
79
87
  exclude = ["**/__pycache__", "**/.venv"]
@@ -85,3 +93,6 @@ exclude = ["**/__pycache__", "**/.venv"]
85
93
  [tool.setuptools.packages.find]
86
94
  where = ["."]
87
95
  include = ["cli*"]
96
+
97
+ [tool.setuptools.package-data]
98
+ cli = ["**/*.toml", "**/*.j2"]
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import sys
6
6
  from importlib import import_module
7
7
 
8
+ from click import Command as _ClickCommand
9
+
8
10
  # Avoid importing typing at module level (costs ~6ms)
9
11
  # TYPE_CHECKING is False at runtime, so the if-block never executes
10
12
  TYPE_CHECKING = False
@@ -40,6 +42,8 @@ _LAZY_EXPORTS = {
40
42
  "console": ("usecli.ui", "console"),
41
43
  "Spinner": ("usecli.progress", "Spinner"),
42
44
  "ProgressBar": ("usecli.progress", "ProgressBar"),
45
+ "pyinstaller": ("usecli.bundler", "pyinstaller"),
46
+ "run": ("usecli.entry", "run"),
43
47
  }
44
48
 
45
49
 
@@ -84,11 +88,14 @@ def _ensure_cli_initialized() -> None:
84
88
  from usecli.cli.services.command_service import CommandService
85
89
 
86
90
  # Store exceptions for error handling
91
+ # Typer vendors click internally and raises its own exception types.
92
+ # We import them dynamically since typer._click has no type stubs.
87
93
  try:
88
- from typer._click.exceptions import BadParameter as TyperBadParameter # type: ignore[import-untyped]
89
- from typer._click.exceptions import ClickException as TyperClickException # type: ignore[import-untyped]
90
- from typer._click.exceptions import UsageError as TyperUsageError # type: ignore[import-untyped]
91
- except ImportError:
94
+ _typer_exceptions = import_module("typer._click.exceptions")
95
+ TyperBadParameter: Any = _typer_exceptions.BadParameter
96
+ TyperClickException: Any = _typer_exceptions.ClickException
97
+ TyperUsageError: Any = _typer_exceptions.UsageError
98
+ except (ImportError, AttributeError):
92
99
  TyperBadParameter = BadParameter
93
100
  TyperClickException = ClickException
94
101
  TyperUsageError = UsageError
@@ -138,7 +145,7 @@ def _ensure_cli_initialized() -> None:
138
145
  return None
139
146
 
140
147
  if cmd_name in matches:
141
- if (
148
+ if ( # pragma: no cover - unreachable duplicate of the branch above
142
149
  cmd_name in group_alias_to_primary
143
150
  and group_alias_to_primary[cmd_name] != cmd_name
144
151
  ):
@@ -252,7 +259,14 @@ def _ensure_cli_initialized() -> None:
252
259
  write_document(success_document())
253
260
  return None
254
261
  fail("Exit", f"Command exited with status {code}", code)
255
- except Exception as error:
262
+ except (
263
+ OSError,
264
+ RuntimeError,
265
+ ValueError,
266
+ TypeError,
267
+ AttributeError,
268
+ KeyError,
269
+ ) as error:
256
270
  print(
257
271
  f"{error.__class__.__name__}: {error}",
258
272
  file=sys.stderr,
@@ -340,19 +354,21 @@ def _get_service():
340
354
 
341
355
 
342
356
  __all__ = [
343
- "BaseCommand",
344
357
  "COLOR",
345
- "console",
358
+ "Argument",
359
+ "BaseCommand",
360
+ "Confirm",
346
361
  "Console",
347
- "main",
348
362
  "Menu",
349
- "Argument",
350
363
  "Option",
364
+ "ProgressBar",
351
365
  "Prompt",
352
- "Confirm",
353
366
  "Spinner",
354
- "ProgressBar",
355
367
  "colors",
368
+ "console",
369
+ "main",
370
+ "pyinstaller",
371
+ "run",
356
372
  "theme",
357
373
  ]
358
374
 
@@ -432,32 +448,40 @@ def _build_alias_to_primary(alias_registry: dict[str, list[str]]) -> dict[str, s
432
448
  PrefixMatchingGroup = None
433
449
 
434
450
 
435
- class _FilteredListCommand:
451
+ class _FilteredListCommand(_ClickCommand):
436
452
  """Command that displays a filtered list of commands."""
437
453
 
438
454
  def __init__(self, prefix_filter: str) -> None:
455
+ super().__init__(
456
+ name="filtered-list",
457
+ context_settings={},
458
+ params=[],
459
+ callback=None,
460
+ help=None,
461
+ epilog=None,
462
+ short_help=None,
463
+ no_args_is_help=False,
464
+ hidden=False,
465
+ deprecated=False,
466
+ )
439
467
  self.prefix_filter = prefix_filter
440
- self.name = "filtered-list"
441
468
  self.allow_extra_args = True
442
469
  self.allow_interspersed_args = True
443
470
  self.ignore_unknown_options = True
444
- self.hidden = False
445
- self.deprecated = False
446
- self.params = []
447
- self.callback = None
448
- self.help = None
449
- self.epilog = None
450
- self.short_help = None
451
- self.no_args_is_help = False
452
- self.context_settings = {}
453
471
 
454
472
  def get_short_help_str(self, limit=45):
455
473
  return ""
456
474
 
457
- def make_context(self, info_name: str, args: list[str], **kwargs: object):
475
+ def make_context(
476
+ self,
477
+ info_name: str | None,
478
+ args: list[str],
479
+ parent: object | None = None,
480
+ **extra: Any,
481
+ ):
458
482
  from click import Context
459
483
 
460
- ctx = Context(self, info_name=info_name, **kwargs) # type: ignore[arg-type]
484
+ ctx = Context(self, info_name=info_name, **extra)
461
485
  return ctx
462
486
 
463
487
  def invoke(self, ctx):
@@ -615,7 +639,14 @@ def main() -> None:
615
639
  if hasattr(e, "show"):
616
640
  e.show()
617
641
  sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
618
- except Exception as e:
642
+ except (
643
+ OSError,
644
+ RuntimeError,
645
+ ValueError,
646
+ TypeError,
647
+ AttributeError,
648
+ KeyError,
649
+ ) as e:
619
650
  from usecli.cli.core.exceptions import UsecliUsageError
620
651
 
621
652
  styled_error = UsecliUsageError(str(e))