Small route-driven command utilities with three interfaces over the same command set:
- Click CLI for direct terminal usage
- Textual TUI for local interactive use
- Browser-hosted Textual app via
textual-serve - FastAPI service for non-interactive HTTP access
The project also discovers Makefile targets at startup and exposes them as commands.
- Python 3.14+
uv
Install dependencies and create the virtual environment:
make syncOr with uv directly:
uv syncShow available commands:
uv run python main.py --helpExample top-level commands include:
greetsaluteinteractive-demolongruntuiwebapimake-*commands generated from the localMakefile
Examples:
uv run python main.py greet --name World
uv run python main.py salute --name Alice
uv run python main.py longrun --name Alice
uv run python main.py interactive-demo
uv run python main.py make-help
uv run python main.py make-remove --pkg requestsLaunch the local Textual app:
uv run python main.py tuiWhat the TUI supports:
- Parameter form generation from registered command metadata
- Live stdout rendering while commands run
- Interactive stdin for commands marked as interactive
- Kill support for running commands
- Router and Makefile command sources
- Command palette actions for theme, quit, keys, screenshot, layout, and source switching
Useful keys:
q: quitctrl+r: toggle router vs Makefile command sourcectrl+t: toggle stacked vs side-by-side layoutctrl+f: toggle output fullscreen
Launch the Textual app through textual-serve:
uv run python main.py webDefaults:
- bind host:
0.0.0.0 - bind port:
8000 - public URL announced to the browser:
http://localhost:8001
Custom example:
uv run python main.py web --host 0.0.0.0 --port 8000 --public-url http://localhost:8001Launch the HTTP API:
uv run python main.py apiOr:
uv run python main.py api --host 0.0.0.0 --port 8000API surfaces:
GET /healthGET /commandsPOST /commands/{name}POST /commands/{name}/stream
Interactive commands are intentionally not exposed over HTTP.
Example:
curl -X POST "http://localhost:8000/commands/greet?name=Alice"Typical response:
{
"command": "greet",
"result": "Hello, Alice!",
"stdout": "",
"stderr": ""
}Streaming uses Server-Sent Events and emits these event types:
startstdoutstderrresulterror
Example:
curl -N -X POST "http://localhost:8000/commands/longrun/stream?name=Alice"Example event stream:
event: start
data: {"command": "longrun"}
event: stdout
data: {"chunk": "Working on Alice...\n"}
event: result
data: {"command": "longrun", "result": null, "stdout": "...", "stderr": ""}
Returns a simple greeting. Defaults to World.
uv run python main.py greet
uv run python main.py greet --name AliceRequires a name and returns a farewell.
uv run python main.py salute --name AliceDemonstrates interactive stdin and live output. Best used in the TUI or web-hosted TUI.
Prints progress every five seconds to demonstrate long-running output.
Every discovered Makefile target becomes a command such as:
make-helpmake-syncmake-lockmake-updatemake-removemake-clean
Targets with Makefile variables are mapped to command parameters. For example, this Makefile target:
remove:
uv remove $(PKG)becomes:
uv run python main.py make-remove --pkg requestsIn the TUI, you can switch between router commands and Makefile commands with ctrl+r.
.
├── api_service.py
├── cli_router.py
├── main.py
├── Makefile
└── commands/
├── api.py
├── greet.py
├── interactive_demo.py
├── long_running.py
├── makefile_routes.py
└── salute.py
- Command modules are auto-discovered from
commands/. - The FastAPI layer wraps the same router used by the CLI and TUI.
- Makefile routes are registered at import time based on the current working directory.
Common maintenance targets:
make sync
make lock
make update
make clean