A dynamic, plugin-based command-line tool — fully containerized so it runs anywhere with zero local Python setup.
onecli is a framework for building a single, unified CLI tool out of independent command plugins. Each command lives in its own subdirectory under commands/ and is discovered automatically at runtime — no registration, no hardcoded imports.
The entire application runs inside a Docker container, ensuring consistent behaviour across every machine and OS. A thin shell wrapper script (onecli) abstracts all Docker plumbing so callers interact with a normal CLI experience.
Configuration is handled through two sources with a clear precedence model:
| Source | Precedence | Use for |
|---|---|---|
ONECLI_* environment variables |
High | Secrets, CI overrides |
~/.oneclirc INI file |
Low | Personal defaults |
| Dependency | Purpose |
|---|---|
| Python 3.13 | Language runtime |
| click | CLI framework |
| Dependency | Purpose |
|---|---|
| pytest | Test runner (installed in the container image via requirements.txt) |
| Requirement | Notes |
|---|---|
| Docker | Required to build and run the containerized CLI |
sh-compatible shell |
The onecli wrapper is a POSIX shell script |
Note: The Docker image installs all Python dependencies from
requirements.txt, includingpytest. Whilepytestis primarily used for development and testing, it is present inside the runtime container as well.
Install Python dev dependencies locally with:
pip install -r requirements.txt┌────────────────────────────────────────────────┐
│ Host machine │
│ │
│ $ ./onecli hello --name Alice │
│ │ │
│ ▼ │
│ onecli (shell script) │
│ • mounts ~/.oneclirc (if present) │
│ • forwards ONECLI_* env vars │
│ • docker run --rm onecli-app ... │
│ │ │
└────────┼───────────────────────────────────────┘
│
┌────────▼───────────────────────────────────────┐
│ Docker container (onecli-app) │
│ │
│ python onecli.py hello --name Alice │
│ │ │
│ ▼ │
│ DynamicCommandLoader │
│ • scans commands/ for subdirs │
│ • imports commands.<name>.command │
│ │ │
│ ▼ │
│ commands/hello/__init__.py :: command() │
│ • reads settings from common/config.py │
│ • prints greeting │
└────────────────────────────────────────────────┘
| File | Responsibility |
|---|---|
onecli |
Shell wrapper — Docker entry point for the host |
onecli.py |
Click entrypoint + DynamicCommandLoader |
common/config.py |
Merges .oneclirc file and ONECLI_* env vars into settings |
commands/<name>/__init__.py |
Self-contained command plugin |
| Subcommand | Description |
|---|---|
build |
Builds the Docker image (docker build -t onecli-app .) |
update |
Pulls the latest changes from main (git pull origin main) and rebuilds the Docker image |
dev <cmd> |
Mounts the local project directory into the container — code changes take effect without rebuilding |
shell |
Opens an interactive sh session inside the container for debugging |
| (any other) | Runs the specified command inside the container normally |
common/config.py runs at startup and builds a single settings dict:
- Reads all
[section]keys from/root/.oneclirc(INI format). - Reads every
ONECLI_*environment variable, strips the prefix, lower-cases the key, and merges it at the top level — overriding any same-named section from step 1.
Example: ONECLI_USER=override replaces the entire [user] section from the file.
Adding a new command requires only one new directory and one Python file.
-
Create the command directory:
mkdir commands/mycommand
-
Create
commands/mycommand/__init__.pyand expose acommandobject:import click from common.config import settings @click.command(name="mycommand") @click.option("--verbose", is_flag=True, help="Enable verbose output.") def command(verbose: bool) -> None: """Short description shown in --help.""" if verbose: click.echo(f"Settings loaded: {settings}") click.echo("mycommand executed!")
-
Rebuild the Docker image to include the new file:
./onecli build
-
Run it:
./onecli mycommand --verbose
- The directory name becomes the CLI sub-command name.
- The
__init__.pymust expose a module-level object named exactlycommand. - It must be a
click.Commandinstance (decorated with@click.command()). - Access shared configuration via
from common.config import settings. - Secrets should be passed as
ONECLI_*env vars — never hardcoded.
| Command | Description | Documentation |
|---|---|---|
hello |
Example greeting command — demonstrates config and env var integration | built-in |
senior-stock |
Fetches and displays stock data from the Senior stock API | senior-stock |
tesseract |
Fetches and displays product variation data from the Tesseract API | tesseract |
./onecli buildThis runs docker build -t onecli-app . under the hood.
./onecli updatePulls the latest changes from the main branch and rebuilds the Docker image in one step — equivalent to running:
git pull origin main && ./onecli buildUse this whenever a new version is released to stay current without having to run the two commands separately.
./onecli <command> [OPTIONS]# Default greeting
./onecli hello
# Hello, World!
# Override the name via CLI option
./onecli hello --name Alice
# Hello, Alice!
# Override the name via ~/.oneclirc
cat ~/.oneclirc
# [user]
# default_name = Bob
./onecli hello
# Hello, Bob!
# Pass a secret token via environment variable
ONECLI_SECRET_TOKEN=my-secret ./onecli hello
# Hello, World!
# Secret token found — authenticated mode active.During active development, use the dev subcommand to mount your local source directory directly into the running container. This means code changes are reflected immediately — no docker build needed between iterations.
./onecli dev <command> [OPTIONS]Example:
./onecli dev hello --name AliceUnder the hood this runs:
docker run --rm -v <project-dir>:/app onecli-app hello --name AliceNote: you still need to run
./onecli buildat least once before usingdevmode, and again wheneverrequirements.txtor theDockerfilechanges.
Use the shell subcommand to drop into an interactive sh session inside the container. Useful for debugging, inspecting the filesystem, or running one-off Python commands.
./onecli shellThe container starts with the same ~/.oneclirc mount and ONECLI_* env vars as a normal run, but the entrypoint is replaced with sh and the session is interactive (-it).
./onecli --help./onecli hello --helpCreate an INI-formatted file at ~/.oneclirc on your host machine:
[user]
default_name = YourName
[server]
host = api.example.comIt is automatically mounted into the container as read-only.
Any variable prefixed with ONECLI_ on the host is forwarded into the container:
export ONECLI_SECRET_TOKEN="my-secret-token"
export ONECLI_API_KEY="key-abc123"
./onecli helloTests are written with pytest and run locally (outside Docker), so Python must be available.
pip install -r requirements.txtpython -m pytest tests/ -v# Configuration loading tests
python -m pytest tests/test_config.py -v
# Command discovery tests
python -m pytest tests/test_discovery.py -v| Test file | What it covers |
|---|---|
tests/test_config.py |
INI file parsing, env var loading, precedence rules |
tests/test_discovery.py |
Directory scanning, __init__.py detection, get_command, CLI runner |
See CONTRIBUTING.md for the full contribution guide.
