usecli 0.1.77__tar.gz → 0.1.79__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.
- {usecli-0.1.77 → usecli-0.1.79}/PKG-INFO +123 -1
- {usecli-0.1.77 → usecli-0.1.79}/README.md +120 -0
- {usecli-0.1.77 → usecli-0.1.79}/pyproject.toml +6 -2
- {usecli-0.1.77 → usecli-0.1.79}/pyproject.toml.orig +6 -3
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/__init__.py +8 -2
- usecli-0.1.79/src/usecli/bundler/__init__.py +275 -0
- usecli-0.1.79/src/usecli/bundler/__main__.py +6 -0
- usecli-0.1.79/src/usecli/cli/commands/defaults/base/upgrade_command.py +316 -0
- usecli-0.1.79/src/usecli/cli/commands/defaults/make/make_bundle_command.py +147 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/templates/usecli.config.toml.j2 +3 -0
- usecli-0.1.79/src/usecli/entry.py +204 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/shared/config/manager.py +65 -2
- usecli-0.1.79/src/usecli/shared/upgrades/__init__.py +22 -0
- usecli-0.1.79/src/usecli/shared/upgrades/checker.py +276 -0
- usecli-0.1.79/src/usecli/shared/upgrades/discovery.py +287 -0
- usecli-0.1.79/src/usecli/shared/upgrades/installer.py +262 -0
- usecli-0.1.79/src/usecli/shared/upgrades/pyproject.py +619 -0
- usecli-0.1.79/src/usecli/shared/upgrades/service.py +47 -0
- {usecli-0.1.77 → usecli-0.1.79}/LICENSE +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/README.md +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/custom/README.md +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/custom/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/about_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/help_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/inspire_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/internal/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/base/internal/fzf_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/core/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/core/utils.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/make_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/defaults/make/make_theme_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/commands/init_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/config/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/config/colors.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/base_command.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/error/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/error/handler.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/error/utils.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/exceptions/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/exceptions/base.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/exceptions/config.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/exceptions/usage.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/exceptions/validation.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/runtime.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/ui/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/ui/list.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/ui/title.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/ui/title.txt +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/validators/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/validators/network.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/validators/numeric.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/validators/path.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/core/validators/string.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/services/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/services/command_service.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/templates/command.py.j2 +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/templates/theme.toml.j2 +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/ayu_dark.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_frappe.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_latte.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_macchiato.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/catppuccin_mocha.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/default.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/dracula.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/gruvbox_dark.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/nord.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/themes/tokyo_night.toml +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/utils/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/utils/interactive/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/cli/utils/interactive/terminal_menu.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/menu.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/params.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/progress.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/shared/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/shared/config/__init__.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/shared/config/globals.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/src/usecli/ui.py +0 -0
- {usecli-0.1.77 → usecli-0.1.79}/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.
|
|
3
|
+
Version: 0.1.79
|
|
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>
|
|
@@ -18,12 +18,14 @@ Requires-Dist: rich>=15.0.0
|
|
|
18
18
|
Requires-Dist: simple-term-menu>=1.6.6
|
|
19
19
|
Requires-Dist: tomli>=2.4.1
|
|
20
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 & 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>
|
|
@@ -242,12 +245,131 @@ about Show app info
|
|
|
242
245
|
help Show help
|
|
243
246
|
init Initialize usecli (usecli only)
|
|
244
247
|
inspire Random quote
|
|
248
|
+
upgrade Upgrade the application to the latest version
|
|
245
249
|
make:command Create new command (usecli only)
|
|
246
250
|
make:theme Create new theme (usecli only)
|
|
251
|
+
make:bundle Build a standalone executable with PyInstaller (usecli only)
|
|
247
252
|
```
|
|
248
253
|
|
|
249
254
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
250
255
|
|
|
256
|
+
<!-- ENTRY POINTS & PACKAGING -->
|
|
257
|
+
|
|
258
|
+
## Entry Points & Packaging
|
|
259
|
+
|
|
260
|
+
A usecli CLI can be exposed in several ways, depending on how you distribute and
|
|
261
|
+
run it. There are three common setups — none requires anything but the base
|
|
262
|
+
`usecli` install, and the optional bundler adds PyInstaller packaging on top.
|
|
263
|
+
|
|
264
|
+
### 1. Default — console script (no entry point)
|
|
265
|
+
|
|
266
|
+
The simplest setup. Declare a console script in your project's `pyproject.toml`:
|
|
267
|
+
|
|
268
|
+
```toml
|
|
269
|
+
[project.scripts]
|
|
270
|
+
mycli = "usecli:main"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
usecli resolves your project's config automatically from the current directory
|
|
274
|
+
(config discovery), so no `main.py` is required. Install and run it:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
uv add usecli
|
|
278
|
+
uv run mycli --help
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This is the **default** — no entry point file of your own is needed.
|
|
282
|
+
|
|
283
|
+
### 2. Custom entry point — `main.py`
|
|
284
|
+
|
|
285
|
+
If you want your own entry point, use the public runtime API. It works in both
|
|
286
|
+
development and frozen bundles, and accepts an optional config path (the config
|
|
287
|
+
file is always named `usecli.config.toml`):
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# main.py
|
|
291
|
+
from usecli import run
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main():
|
|
295
|
+
run() # auto-detect project config
|
|
296
|
+
# run("path/to/usecli.config.toml") # or point at a specific config
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
main()
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Run it in development (no PyInstaller required):
|
|
304
|
+
|
|
305
|
+
```sh
|
|
306
|
+
uv run python main.py --help
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
To expose the same entry point as a console script, point `[project.scripts]`
|
|
310
|
+
at the `main` function in your `myapp` package (the usual `src/myapp` layout):
|
|
311
|
+
|
|
312
|
+
```toml
|
|
313
|
+
[project.scripts]
|
|
314
|
+
myapp = "myapp:main"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Because `run()` injects the located config explicitly, a custom `main.py` works
|
|
318
|
+
regardless of whether your CLI also has a `[project.scripts]` entry.
|
|
319
|
+
|
|
320
|
+
### 3. Bundler — standalone executable (optional, requires PyInstaller)
|
|
321
|
+
|
|
322
|
+
The bundler is an **optional** feature gated on PyInstaller being installed.
|
|
323
|
+
Install it with:
|
|
324
|
+
|
|
325
|
+
```sh
|
|
326
|
+
uv add "usecli[pyinstaller]"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Once installed, a `make:bundle` command appears (usecli only) that builds your
|
|
330
|
+
project into a standalone executable. It asks which mode to use via an
|
|
331
|
+
interactive menu, and confirms before building (defaults to no unless `-y`):
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
uv run usecli make:bundle # menu: one file vs one folder + confirm
|
|
335
|
+
uv run usecli make:bundle --mode onefile # single-file executable
|
|
336
|
+
uv run usecli make:bundle --mode onedir # one-folder bundle
|
|
337
|
+
uv run usecli make:bundle -y # skip the confirmation
|
|
338
|
+
uv run usecli make:bundle --mode onedir -y # fully programmatic
|
|
339
|
+
uv run usecli make:bundle --mode onedir --zip -y # one-folder bundle + zip
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
When building a **one-folder** bundle, `make:bundle` asks whether to also zip
|
|
343
|
+
the folder into `dist/<name>.zip` (PyInstaller's recommended distribution
|
|
344
|
+
format for one-folder mode). Pass `--zip` / `--no-zip` to skip the prompt and
|
|
345
|
+
force a choice; with `-y` and no flag, zipping is skipped.
|
|
346
|
+
|
|
347
|
+
A `usecli-bundle` console script wraps the same build, and you can drive it from
|
|
348
|
+
Python too:
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
uv run usecli-bundle
|
|
352
|
+
uv run usecli-bundle --mode onedir
|
|
353
|
+
uv run usecli-bundle --mode onedir --zip
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from usecli import pyinstaller
|
|
358
|
+
|
|
359
|
+
pyinstaller() # auto-detect config, single-file
|
|
360
|
+
pyinstaller(mode="onedir") # one-folder bundle
|
|
361
|
+
pyinstaller(mode="onedir", zip=True) # one-folder bundle + zip
|
|
362
|
+
pyinstaller("path/to/usecli.config.toml") # or point at a specific config
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Output lands in `dist/` — `dist/mycli` for `onefile`, or a `dist/mycli/` folder
|
|
366
|
+
containing the executable (plus `_internal/` with the bundled assets) for
|
|
367
|
+
`onedir`. With `zip=True`, a `dist/mycli.zip` archive is created alongside the
|
|
368
|
+
folder. `make:bundle` and `usecli-bundle` are only available when PyInstaller
|
|
369
|
+
is installed.
|
|
370
|
+
|
|
371
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
372
|
+
|
|
251
373
|
<!-- DEVELOPMENT -->
|
|
252
374
|
|
|
253
375
|
## 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 & 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>
|
|
@@ -214,12 +215,131 @@ about Show app info
|
|
|
214
215
|
help Show help
|
|
215
216
|
init Initialize usecli (usecli only)
|
|
216
217
|
inspire Random quote
|
|
218
|
+
upgrade Upgrade the application to the latest version
|
|
217
219
|
make:command Create new command (usecli only)
|
|
218
220
|
make:theme Create new theme (usecli only)
|
|
221
|
+
make:bundle Build a standalone executable with PyInstaller (usecli only)
|
|
219
222
|
```
|
|
220
223
|
|
|
221
224
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
222
225
|
|
|
226
|
+
<!-- ENTRY POINTS & PACKAGING -->
|
|
227
|
+
|
|
228
|
+
## Entry Points & Packaging
|
|
229
|
+
|
|
230
|
+
A usecli CLI can be exposed in several ways, depending on how you distribute and
|
|
231
|
+
run it. There are three common setups — none requires anything but the base
|
|
232
|
+
`usecli` install, and the optional bundler adds PyInstaller packaging on top.
|
|
233
|
+
|
|
234
|
+
### 1. Default — console script (no entry point)
|
|
235
|
+
|
|
236
|
+
The simplest setup. Declare a console script in your project's `pyproject.toml`:
|
|
237
|
+
|
|
238
|
+
```toml
|
|
239
|
+
[project.scripts]
|
|
240
|
+
mycli = "usecli:main"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
usecli resolves your project's config automatically from the current directory
|
|
244
|
+
(config discovery), so no `main.py` is required. Install and run it:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
uv add usecli
|
|
248
|
+
uv run mycli --help
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
This is the **default** — no entry point file of your own is needed.
|
|
252
|
+
|
|
253
|
+
### 2. Custom entry point — `main.py`
|
|
254
|
+
|
|
255
|
+
If you want your own entry point, use the public runtime API. It works in both
|
|
256
|
+
development and frozen bundles, and accepts an optional config path (the config
|
|
257
|
+
file is always named `usecli.config.toml`):
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
# main.py
|
|
261
|
+
from usecli import run
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def main():
|
|
265
|
+
run() # auto-detect project config
|
|
266
|
+
# run("path/to/usecli.config.toml") # or point at a specific config
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Run it in development (no PyInstaller required):
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
uv run python main.py --help
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
To expose the same entry point as a console script, point `[project.scripts]`
|
|
280
|
+
at the `main` function in your `myapp` package (the usual `src/myapp` layout):
|
|
281
|
+
|
|
282
|
+
```toml
|
|
283
|
+
[project.scripts]
|
|
284
|
+
myapp = "myapp:main"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Because `run()` injects the located config explicitly, a custom `main.py` works
|
|
288
|
+
regardless of whether your CLI also has a `[project.scripts]` entry.
|
|
289
|
+
|
|
290
|
+
### 3. Bundler — standalone executable (optional, requires PyInstaller)
|
|
291
|
+
|
|
292
|
+
The bundler is an **optional** feature gated on PyInstaller being installed.
|
|
293
|
+
Install it with:
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
uv add "usecli[pyinstaller]"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Once installed, a `make:bundle` command appears (usecli only) that builds your
|
|
300
|
+
project into a standalone executable. It asks which mode to use via an
|
|
301
|
+
interactive menu, and confirms before building (defaults to no unless `-y`):
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
uv run usecli make:bundle # menu: one file vs one folder + confirm
|
|
305
|
+
uv run usecli make:bundle --mode onefile # single-file executable
|
|
306
|
+
uv run usecli make:bundle --mode onedir # one-folder bundle
|
|
307
|
+
uv run usecli make:bundle -y # skip the confirmation
|
|
308
|
+
uv run usecli make:bundle --mode onedir -y # fully programmatic
|
|
309
|
+
uv run usecli make:bundle --mode onedir --zip -y # one-folder bundle + zip
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When building a **one-folder** bundle, `make:bundle` asks whether to also zip
|
|
313
|
+
the folder into `dist/<name>.zip` (PyInstaller's recommended distribution
|
|
314
|
+
format for one-folder mode). Pass `--zip` / `--no-zip` to skip the prompt and
|
|
315
|
+
force a choice; with `-y` and no flag, zipping is skipped.
|
|
316
|
+
|
|
317
|
+
A `usecli-bundle` console script wraps the same build, and you can drive it from
|
|
318
|
+
Python too:
|
|
319
|
+
|
|
320
|
+
```sh
|
|
321
|
+
uv run usecli-bundle
|
|
322
|
+
uv run usecli-bundle --mode onedir
|
|
323
|
+
uv run usecli-bundle --mode onedir --zip
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from usecli import pyinstaller
|
|
328
|
+
|
|
329
|
+
pyinstaller() # auto-detect config, single-file
|
|
330
|
+
pyinstaller(mode="onedir") # one-folder bundle
|
|
331
|
+
pyinstaller(mode="onedir", zip=True) # one-folder bundle + zip
|
|
332
|
+
pyinstaller("path/to/usecli.config.toml") # or point at a specific config
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Output lands in `dist/` — `dist/mycli` for `onefile`, or a `dist/mycli/` folder
|
|
336
|
+
containing the executable (plus `_internal/` with the bundled assets) for
|
|
337
|
+
`onedir`. With `zip=True`, a `dist/mycli.zip` archive is created alongside the
|
|
338
|
+
folder. `make:bundle` and `usecli-bundle` are only available when PyInstaller
|
|
339
|
+
is installed.
|
|
340
|
+
|
|
341
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
342
|
+
|
|
223
343
|
<!-- DEVELOPMENT -->
|
|
224
344
|
|
|
225
345
|
## Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.79"
|
|
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"
|
|
@@ -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,7 +39,7 @@ Changelog = "https://github.com/thememium/usecli/blob/master/CHANGELOG.md"
|
|
|
36
39
|
|
|
37
40
|
[project.scripts]
|
|
38
41
|
usecli = "usecli:main"
|
|
39
|
-
|
|
42
|
+
usecli-bundle = "usecli.bundler:cli"
|
|
40
43
|
|
|
41
44
|
[build-system]
|
|
42
45
|
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
@@ -47,6 +50,7 @@ dev = [
|
|
|
47
50
|
"deptry>=0.25.1",
|
|
48
51
|
"isort>=8.0.1",
|
|
49
52
|
"poethepoet>=0.48.0",
|
|
53
|
+
"pyinstaller>=6.21.0",
|
|
50
54
|
"pytest>=9.1.1",
|
|
51
55
|
"pytest-cov>=7.1.0",
|
|
52
56
|
"ruff>=0.16.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usecli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.79"
|
|
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" }]
|
|
@@ -24,6 +24,9 @@ dependencies = [
|
|
|
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,7 +36,7 @@ Changelog = "https://github.com/thememium/usecli/blob/master/CHANGELOG.md"
|
|
|
33
36
|
|
|
34
37
|
[project.scripts]
|
|
35
38
|
usecli = "usecli:main"
|
|
36
|
-
|
|
39
|
+
usecli-bundle = "usecli.bundler:cli"
|
|
37
40
|
|
|
38
41
|
[build-system]
|
|
39
42
|
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
@@ -44,6 +47,7 @@ dev = [
|
|
|
44
47
|
"deptry>=0.25.1",
|
|
45
48
|
"isort>=8.0.1",
|
|
46
49
|
"poethepoet>=0.48.0",
|
|
50
|
+
"pyinstaller>=6.21.0",
|
|
47
51
|
"pytest>=9.1.1",
|
|
48
52
|
"pytest-cov>=7.1.0",
|
|
49
53
|
"ruff>=0.16.1",
|
|
@@ -92,4 +96,3 @@ include = ["cli*"]
|
|
|
92
96
|
|
|
93
97
|
[tool.setuptools.package-data]
|
|
94
98
|
cli = ["**/*.toml", "**/*.j2"]
|
|
95
|
-
|
|
@@ -42,6 +42,8 @@ _LAZY_EXPORTS = {
|
|
|
42
42
|
"console": ("usecli.ui", "console"),
|
|
43
43
|
"Spinner": ("usecli.progress", "Spinner"),
|
|
44
44
|
"ProgressBar": ("usecli.progress", "ProgressBar"),
|
|
45
|
+
"pyinstaller": ("usecli.bundler", "pyinstaller"),
|
|
46
|
+
"run": ("usecli.entry", "run"),
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
|
|
@@ -272,7 +274,7 @@ def _ensure_cli_initialized() -> None:
|
|
|
272
274
|
fail(error.__class__.__name__, str(error), 1)
|
|
273
275
|
|
|
274
276
|
def invoke(self, ctx):
|
|
275
|
-
from click.exceptions import Exit
|
|
277
|
+
from click.exceptions import ClickException, Exit
|
|
276
278
|
|
|
277
279
|
from usecli.cli.core.runtime import is_json_mode
|
|
278
280
|
|
|
@@ -304,7 +306,9 @@ def _ensure_cli_initialized() -> None:
|
|
|
304
306
|
styled_error = UsecliUsageError(e.message, ctx=e.ctx)
|
|
305
307
|
styled_error.show()
|
|
306
308
|
sys.exit(styled_error.exit_code)
|
|
307
|
-
|
|
309
|
+
# UsecliError extends real-click ClickException, not the vendored
|
|
310
|
+
# typer._click one — catch both, mirroring the JSON path.
|
|
311
|
+
except (_TyperClickException, ClickException) as e:
|
|
308
312
|
if hasattr(e, "show"):
|
|
309
313
|
e.show()
|
|
310
314
|
sys.exit(e.exit_code if hasattr(e, "exit_code") else 1)
|
|
@@ -365,6 +369,8 @@ __all__ = [
|
|
|
365
369
|
"colors",
|
|
366
370
|
"console",
|
|
367
371
|
"main",
|
|
372
|
+
"pyinstaller",
|
|
373
|
+
"run",
|
|
368
374
|
"theme",
|
|
369
375
|
]
|
|
370
376
|
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""PyInstaller integration for usecli projects.
|
|
2
|
+
|
|
3
|
+
Every usecli project is a thin layer over usecli: it provides a
|
|
4
|
+
``usecli.config.toml`` plus ``commands/``, ``templates/`` and ``themes/`` that
|
|
5
|
+
usecli loads at runtime. Packaging that with PyInstaller by hand requires a
|
|
6
|
+
hand-written spec, ``--collect-all`` flags and a custom entry point that
|
|
7
|
+
re-injects the bundle paths — easy to get wrong.
|
|
8
|
+
|
|
9
|
+
:func:`pyinstaller` turns all of that into a one-liner::
|
|
10
|
+
|
|
11
|
+
from usecli import pyinstaller
|
|
12
|
+
|
|
13
|
+
pyinstaller() # auto-detect config from the current directory
|
|
14
|
+
pyinstaller("config.toml") # or point at a specific config
|
|
15
|
+
pyinstaller(mode="onedir") # or a one-folder (directory) bundle
|
|
16
|
+
|
|
17
|
+
PyInstaller supports two operating modes (see
|
|
18
|
+
https://pyinstaller.org/en/stable/operating-mode.html):
|
|
19
|
+
|
|
20
|
+
* ``mode="onefile"`` (default): a single self-contained executable.
|
|
21
|
+
* ``mode="onedir"``: a folder containing the executable plus its dependencies
|
|
22
|
+
(faster to start, easier to debug — you can inspect what was collected).
|
|
23
|
+
|
|
24
|
+
It locates the project's ``usecli.config.toml`` (and the sibling
|
|
25
|
+
``commands/``, ``templates/``, ``themes/`` and ``pyproject.toml``), then runs
|
|
26
|
+
PyInstaller to produce a bundle named after the config's ``command_name``.
|
|
27
|
+
PyInstaller is an optional dependency — install it with
|
|
28
|
+
``pip install usecli[pyinstaller]`` (or ``uv add usecli[pyinstaller]``).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
import zipfile
|
|
36
|
+
from collections.abc import Sequence
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from usecli.entry import BUNDLE_DATA_DIR
|
|
40
|
+
|
|
41
|
+
_MODE_FLAG = {"onefile": "--onefile", "onedir": "--onedir"}
|
|
42
|
+
_MODE_LABEL = {
|
|
43
|
+
"onefile": "a single-file executable",
|
|
44
|
+
"onedir": "a one-folder bundle",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_config_path(
|
|
49
|
+
config_path: str | os.PathLike[str] | None,
|
|
50
|
+
) -> Path:
|
|
51
|
+
"""Return an absolute, existing path to the project's usecli.config.toml.
|
|
52
|
+
|
|
53
|
+
When ``config_path`` is ``None``, it is auto-detected: first by walking up
|
|
54
|
+
from the current directory, then (e.g. configs nested under ``cli/``) via a
|
|
55
|
+
bounded recursive search rooted at the detected project root.
|
|
56
|
+
"""
|
|
57
|
+
if config_path is not None:
|
|
58
|
+
resolved = Path(config_path).resolve()
|
|
59
|
+
if not resolved.is_file():
|
|
60
|
+
raise FileNotFoundError(f"usecli.config.toml not found at: {resolved}")
|
|
61
|
+
return resolved
|
|
62
|
+
|
|
63
|
+
from usecli.shared.config.manager import resolve_config_path
|
|
64
|
+
|
|
65
|
+
return resolve_config_path(Path.cwd())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _find_pyproject(data_root: Path) -> Path | None:
|
|
69
|
+
"""Find the nearest pyproject.toml walking up from the data dir."""
|
|
70
|
+
from usecli.shared.config.manager import find_project_pyproject
|
|
71
|
+
|
|
72
|
+
return find_project_pyproject(data_root)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_args(
|
|
76
|
+
config_path: Path,
|
|
77
|
+
entry_path: Path,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None,
|
|
80
|
+
distpath: Path | None,
|
|
81
|
+
workpath: Path | None,
|
|
82
|
+
extra_args: Sequence[str] | None,
|
|
83
|
+
mode: str = "onefile",
|
|
84
|
+
) -> list[str]:
|
|
85
|
+
from usecli.shared.config.manager import ConfigManager
|
|
86
|
+
|
|
87
|
+
if mode not in _MODE_FLAG:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Invalid mode {mode!r}; expected one of {sorted(_MODE_FLAG)}."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
data_root = config_path.parent
|
|
93
|
+
command_name = name or (
|
|
94
|
+
ConfigManager._load_usecli_toml(config_path).get("command_name") or "usecli"
|
|
95
|
+
)
|
|
96
|
+
pyproject = _find_pyproject(data_root)
|
|
97
|
+
|
|
98
|
+
args: list[str] = [_MODE_FLAG[mode], "--name", command_name]
|
|
99
|
+
if distpath is not None:
|
|
100
|
+
args += ["--distpath", str(distpath)]
|
|
101
|
+
if workpath is not None:
|
|
102
|
+
args += ["--workpath", str(workpath)]
|
|
103
|
+
args += [
|
|
104
|
+
"--collect-all",
|
|
105
|
+
"usecli",
|
|
106
|
+
"--collect-all",
|
|
107
|
+
"pyfiglet",
|
|
108
|
+
"--add-data",
|
|
109
|
+
f"{data_root}{os.pathsep}{BUNDLE_DATA_DIR}",
|
|
110
|
+
"--paths",
|
|
111
|
+
str(data_root.parent),
|
|
112
|
+
"-y",
|
|
113
|
+
]
|
|
114
|
+
if pyproject is not None:
|
|
115
|
+
args += ["--add-data", f"{pyproject}{os.pathsep}{BUNDLE_DATA_DIR}"]
|
|
116
|
+
if extra_args:
|
|
117
|
+
args += list(extra_args)
|
|
118
|
+
args.append(str(entry_path))
|
|
119
|
+
return args
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _zip_bundle(dist_dir: Path, name: str) -> Path:
|
|
123
|
+
"""Compress a one-folder bundle into ``<name>.zip``.
|
|
124
|
+
|
|
125
|
+
The archive is rooted at the bundle folder (``<name>/``) so users can
|
|
126
|
+
install the program simply by unzipping it — the distribution flow PyInstaller
|
|
127
|
+
describes for one-folder mode
|
|
128
|
+
(https://pyinstaller.org/en/stable/operating-mode.html#bundling-to-one-folder).
|
|
129
|
+
"""
|
|
130
|
+
bundle_dir = dist_dir / name
|
|
131
|
+
if not bundle_dir.is_dir():
|
|
132
|
+
raise FileNotFoundError(
|
|
133
|
+
f"Cannot zip: expected one-folder bundle at {bundle_dir}."
|
|
134
|
+
)
|
|
135
|
+
target = dist_dir / f"{name}.zip"
|
|
136
|
+
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
137
|
+
for path in sorted(bundle_dir.rglob("*")):
|
|
138
|
+
if path.is_file():
|
|
139
|
+
zf.write(path, arcname=f"{name}/{path.relative_to(bundle_dir)}")
|
|
140
|
+
return target
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def pyinstaller(
|
|
144
|
+
config_path: str | os.PathLike[str] | None = None,
|
|
145
|
+
*,
|
|
146
|
+
name: str | None = None,
|
|
147
|
+
mode: str = "onefile",
|
|
148
|
+
distpath: str | os.PathLike[str] | None = None,
|
|
149
|
+
workpath: str | os.PathLike[str] | None = None,
|
|
150
|
+
extra_args: Sequence[str] | None = None,
|
|
151
|
+
zip: bool = False,
|
|
152
|
+
) -> int:
|
|
153
|
+
"""Build an executable bundle for the given usecli project.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
config_path: Optional path to a ``usecli.config.toml``. When omitted it
|
|
157
|
+
is auto-detected by walking up from the current directory.
|
|
158
|
+
name: Optional override for the executable name. Defaults to the
|
|
159
|
+
config's ``command_name``.
|
|
160
|
+
mode: ``"onefile"`` (single executable, default) or ``"onedir"``
|
|
161
|
+
(folder containing the executable plus its dependencies).
|
|
162
|
+
distpath: Output directory for the bundle (default: ``./dist``).
|
|
163
|
+
workpath: PyInstaller work directory (default: ``./build``).
|
|
164
|
+
extra_args: Additional PyInstaller CLI flags, e.g.
|
|
165
|
+
``["--icon", "icon.icns"]``.
|
|
166
|
+
zip: When ``mode="onedir"`` and ``zip=True``, compress the resulting
|
|
167
|
+
folder into ``<name>.zip`` (alongside the folder in ``distpath``)
|
|
168
|
+
after a successful build — the distribution format PyInstaller
|
|
169
|
+
recommends for one-folder bundles. Ignored for ``mode="onefile"``.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The PyInstaller exit code (``0`` on success).
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
FileNotFoundError: if no config can be located / the given path is not
|
|
176
|
+
a file.
|
|
177
|
+
ValueError: if ``mode`` is not ``"onefile"`` or ``"onedir"``.
|
|
178
|
+
ImportError: if PyInstaller is not installed. Install with
|
|
179
|
+
``pip install usecli[pyinstaller]``.
|
|
180
|
+
"""
|
|
181
|
+
resolved = _resolve_config_path(config_path)
|
|
182
|
+
import usecli.entry as entry_module
|
|
183
|
+
|
|
184
|
+
entry_path = Path(entry_module.__file__)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
from PyInstaller.__main__ import run as _pyinstaller_run
|
|
188
|
+
except ImportError as exc: # pragma: no cover - env dependent
|
|
189
|
+
raise ImportError(
|
|
190
|
+
"PyInstaller is required to build an executable. Install it with "
|
|
191
|
+
"`pip install usecli[pyinstaller]` (or `uv add usecli[pyinstaller]`)."
|
|
192
|
+
) from exc
|
|
193
|
+
|
|
194
|
+
args = _build_args(
|
|
195
|
+
resolved,
|
|
196
|
+
entry_path,
|
|
197
|
+
name=name,
|
|
198
|
+
mode=mode,
|
|
199
|
+
distpath=Path(distpath) if distpath is not None else None,
|
|
200
|
+
workpath=Path(workpath) if workpath is not None else None,
|
|
201
|
+
extra_args=extra_args,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
print(
|
|
205
|
+
f"[usecli] Packaging '{args[args.index('--name') + 1]}' "
|
|
206
|
+
f"from {resolved} as {_MODE_LABEL[mode]}..."
|
|
207
|
+
)
|
|
208
|
+
code = _pyinstaller_run(args)
|
|
209
|
+
if isinstance(code, int) and code != 0:
|
|
210
|
+
raise SystemExit(code)
|
|
211
|
+
result = int(code or 0)
|
|
212
|
+
if zip and mode == "onedir" and result == 0:
|
|
213
|
+
from usecli.shared.config.manager import ConfigManager
|
|
214
|
+
|
|
215
|
+
dist_dir = (
|
|
216
|
+
Path(distpath).resolve() if distpath is not None else Path.cwd() / "dist"
|
|
217
|
+
)
|
|
218
|
+
bundle_name = name or (
|
|
219
|
+
ConfigManager._load_usecli_toml(resolved).get("command_name") or "usecli"
|
|
220
|
+
)
|
|
221
|
+
zip_file = _zip_bundle(dist_dir, bundle_name)
|
|
222
|
+
print(f"[usecli] Created {zip_file}")
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cli(argv: Sequence[str] | None = None) -> int:
|
|
227
|
+
"""Command-line interface backing the ``usecli-bundle`` console script.
|
|
228
|
+
|
|
229
|
+
So a project's pyproject.toml never needs to carry PyInstaller flags —
|
|
230
|
+
``uv run usecli-bundle`` auto-detects the config and builds the bundle::
|
|
231
|
+
|
|
232
|
+
[tool.poe.tasks]
|
|
233
|
+
bundle = "uv run usecli-bundle" # single-file executable
|
|
234
|
+
bundle-dir = "uv run usecli-bundle --mode onedir"
|
|
235
|
+
"""
|
|
236
|
+
import argparse
|
|
237
|
+
|
|
238
|
+
parser = argparse.ArgumentParser(
|
|
239
|
+
prog="usecli-bundle",
|
|
240
|
+
description="Build an executable bundle for the current usecli project.",
|
|
241
|
+
)
|
|
242
|
+
parser.add_argument(
|
|
243
|
+
"config_path",
|
|
244
|
+
nargs="?",
|
|
245
|
+
default=None,
|
|
246
|
+
help="Path to usecli.config.toml (auto-detected when omitted).",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument("--name", default=None, help="Executable name override.")
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
"--mode",
|
|
251
|
+
choices=sorted(_MODE_FLAG),
|
|
252
|
+
default="onefile",
|
|
253
|
+
help="onefile (single executable, default) or onedir (folder bundle).",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument("--distpath", default=None, help="Output directory.")
|
|
256
|
+
parser.add_argument("--workpath", default=None, help="PyInstaller work dir.")
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"--zip",
|
|
259
|
+
action="store_true",
|
|
260
|
+
help="Zip the one-folder bundle into <name>.zip (onedir mode).",
|
|
261
|
+
)
|
|
262
|
+
known, extra = parser.parse_known_args(argv)
|
|
263
|
+
return pyinstaller(
|
|
264
|
+
config_path=known.config_path,
|
|
265
|
+
name=known.name,
|
|
266
|
+
mode=known.mode,
|
|
267
|
+
distpath=known.distpath,
|
|
268
|
+
workpath=known.workpath,
|
|
269
|
+
extra_args=extra or None,
|
|
270
|
+
zip=known.zip,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__": # pragma: no cover - convenience CLI
|
|
275
|
+
sys.exit(cli())
|