Zush is a CLI framework for building nested CLI applications.
Project description
zush
zush is a Python CLI framework for discovering and serving nested command trees from plugin packages.
It is useful when you want one entry point that can load commands from multiple environments without hard-coding every command into a single application.
What zush does
- Discovers plugin packages from configured directories or site-packages.
- Exposes plugin commands as nested Click subcommands.
- Provides built-in commands for inspecting the active command tree.
- Supports lightweight plugin hooks and shared runtime context.
- Can be used as a standalone CLI or mounted inside another Click app.
Requirements
- Python 3.12+
- Click 8+
Installation
From the repository:
uv sync
uv run zush --help
Or install it as a package and use the zush console script.
Quick Start
Show the active command tree:
zush self map
Run a discovered plugin command:
zush <group> <command> ...
Use the bundled playground without touching your config:
uv run zush --mock-path ./playground self map
uv run zush --mock-path ./playground demo greet
--mock-path and -m scan only the given directory and disable cache usage for that run.
Configuration
zush reads its config from ~/.zush/config.toml.
If the file does not exist yet, zush creates a bootstrap config on first run.
Supported keys:
| Key | Meaning |
|---|---|
envs |
Directories to scan for plugin packages. |
env_prefix |
Allowed package name prefixes. Default: ["zush_"]. |
playground |
Optional directory scanned first for local development overrides. |
include_current_env |
When true, also scan the current Python environment's site-packages. |
Example:
envs = ["/path/to/plugins", "/another/path"]
env_prefix = ["zush_", "my_"]
playground = "/path/to/zush/playground"
include_current_env = true
zush stores config, cache, and other runtime files under ~/.zush/ by default.
Plugin Layout
zush discovers directories whose names match one of the configured prefixes and that contain __zush__.py at the package root.
Inside __zush__.py, export a plugin instance with a .commands dictionary.
Minimal example:
import click
class Plugin:
def __init__(self) -> None:
self.commands = {
"hello": click.Command("hello", callback=lambda: click.echo("Hello")),
}
ZushPlugin = Plugin()
Command keys may use dotted paths to build nested subcommands:
demo.greettools.convert.json
Plugin Helper API
If you do not want to manage dotted command paths by hand, use zush.plugin.Plugin:
import click
from zush.plugin import Plugin
p = Plugin()
p.group("hello", help="Greeting commands").command(
"say",
callback=lambda: click.echo("Hi"),
help="Say hi",
)
ZushPlugin = p
Plugin Hooks
Plugins may optionally expose these attributes on the exported instance:
before_cmdafter_cmdon_erroron_ctx_match
These are lifecycle hooks for command execution and shared context changes. They are not exposed as CLI commands.
Runtime Globals
zush also provides a process-local runtime object store at zush.runtime.g.
This is useful for sharing live objects during a single zush process, for example:
- schedulers
- clients
- registries
- in-memory service handles
Helper-based plugins can register objects into that store:
from zush.plugin import Plugin
p = Plugin()
p.provide("scheduler", object())
ZushPlugin = p
If the object should be created lazily, use provide_factory(...) instead. The value is materialized on first access through zush.runtime.g and then cached for the rest of the process:
from zush.plugin import Plugin
def build_scheduler():
return object()
p = Plugin()
p.provide_factory("scheduler", build_scheduler)
ZushPlugin = p
Factories may also accept a plugin runtime object as their first argument. That runtime can start, stop, restart, or ensure readiness for services declared by the same plugin.
When the provider depends on a service, declare that dependency directly so zush can ensure readiness before construction and invalidate the cached provider when the service changes:
from zush.plugin import Plugin
def build_client(runtime):
return MyClient(runtime)
def close_client(client):
client.close()
p = Plugin()
p.provide_factory(
"client",
build_client,
service="web",
recreate_on_restart=True,
teardown=close_client,
)
ZushPlugin = p
With that setup:
- zush ensures
webis ready before the provider is first created - the provider is rebuilt after
webrestarts or stops - the previous provider instance is passed to
teardownbefore replacement
Objects in zush.runtime.g are not persisted to disk and are only available for the current process.
Persisted Plugin State
Helper-based plugins can persist state with persistedCtx():
import click
from zush.plugin import Plugin
@click.command("save")
def save_cmd() -> None:
with ZushPlugin.persistedCtx() as state:
state["count"] = state.get("count", 0) + 1
click.echo("saved")
p = Plugin()
p.group("persist").command("save", callback=save_cmd.callback)
ZushPlugin = p
Supported payload types:
persistedCtx()for JSONpersistedCtx("notes.txt")for plain textpersistedCtx("settings.toml")for TOMLpersistedCtx("settings.yaml")for YAML
Detached Services
Plugins can also declare detached subprocess-backed services for zush to manage.
Helper-based example:
import sys
from zush.plugin import Plugin
p = Plugin()
p.service(
"web",
[sys.executable, "-m", "flask", "run"],
auto_restart=True,
)
ZushPlugin = p
zush persists service registration and state in user data and exposes a built-in control surface:
zush self services web --start
zush self services web --status
zush self services web --restart
zush self services web --stop
If a service is marked auto_restart=True, zush can restart it when it is missing or when its health check reports unhealthy.
Services may also supply a custom control interface when subprocess spawning is not the only or best lifecycle mechanism. A control interface can implement start(runtime), stop(runtime), restart(runtime), and status(runtime). When present, zush uses those methods first and falls back to OS-level termination only when terminate_fallback=True.
Example:
from zush.plugin import Plugin
class Control:
def start(self, runtime):
runtime.state["running"] = True
runtime.save()
return "started web"
def stop(self, runtime):
runtime.state["running"] = False
runtime.save()
return "stopped web"
def status(self, runtime):
return "healthy" if runtime.state.get("running") else "stopped"
p = Plugin()
p.service(
"web",
["python", "app.py"],
control=Control(),
terminate_fallback=True,
)
ZushPlugin = p
This makes it possible for one plugin package to own both a provider object and the server/service it depends on.
The playground contains a concrete example in playground/README.md under zush_provider_service_demo.
Health checks can be provided as callbacks that return either:
TrueorFalse(True, "message")or(False, "message")
Example:
import httpx
import sys
from zush.plugin import Plugin
def healthcheck(_state):
try:
response = httpx.get("http://127.0.0.1:5000/health", timeout=0.5)
return response.status_code == 200, f"status={response.status_code}"
except Exception as exc:
return False, str(exc)
p = Plugin()
p.service(
"web",
[sys.executable, "-m", "flask", "run", "--no-reload"],
auto_restart=True,
healthcheck=healthcheck,
)
The test suite includes end-to-end service coverage with a real Flask subprocess and httpx clients, including restart behavior for unhealthy services.
Built-in Commands
The self group is reserved for zush itself.
zush self mapprints the active command tree.zush self configopens the active zush config directory.zush self services ...manages plugin-declared detached services.
Plugins cannot register commands under self.
Embedding
zush can be mounted as a subcommand group inside another Click application:
from pathlib import Path
import click
from zush import create_zush_group
from zush.config import Config
from zush.paths import DirectoryStorage, temporary_storage
app = click.Group("myapp")
app.add_command(create_zush_group(), "zush")
storage = DirectoryStorage(Path("/myapp/data/zush"))
config = Config(envs=[Path("/my/envs")], env_prefix=["zush_"])
app.add_command(create_zush_group(config=config, storage=storage), "zush")
with temporary_storage() as temp_storage:
app.add_command(create_zush_group(config=config, storage=temp_storage), "temp-zush")
Factory signature:
create_zush_group(name="zush", config=None, storage=None, mock_path=None)
Playground
The playground/ directory contains sample plugins for local testing and exploration.
See playground/README.md for examples.
Development
Install dev dependencies and run tests:
uv sync --extra dev
uv run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zush-0.2.0.tar.gz.
File metadata
- Download URL: zush-0.2.0.tar.gz
- Upload date:
- Size: 20.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
887cd4f5c6fe13384f5a6257372ff265c2476b99906e71b81b660f88f41d6698
|
|
| MD5 |
98a505b398c9b1f661dc5c20800256b0
|
|
| BLAKE2b-256 |
164c43d289a26a242d143b6aa33530f5994455e6fe4ff515ffaf4f110f0df00a
|
Provenance
The following attestation bundles were made for zush-0.2.0.tar.gz:
Publisher:
publish.yml on ZackaryW/zush
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zush-0.2.0.tar.gz -
Subject digest:
887cd4f5c6fe13384f5a6257372ff265c2476b99906e71b81b660f88f41d6698 - Sigstore transparency entry: 1181145350
- Sigstore integration time:
-
Permalink:
ZackaryW/zush@eae1b05a404d259ed16ae9f6759075ac3d3b7d14 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ZackaryW
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eae1b05a404d259ed16ae9f6759075ac3d3b7d14 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file zush-0.2.0-py3-none-any.whl.
File metadata
- Download URL: zush-0.2.0-py3-none-any.whl
- Upload date:
- Size: 28.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
628ea10c8cc68e57bb60375c11bdb965e08b30843557ea7dd18a62c83f805c08
|
|
| MD5 |
b808ce8031cb277e6cd3832d4d37e1e7
|
|
| BLAKE2b-256 |
42629c4001f837152f1c61b435beed7bc46c3f0d3830ebc80312227cac6553bf
|
Provenance
The following attestation bundles were made for zush-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on ZackaryW/zush
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zush-0.2.0-py3-none-any.whl -
Subject digest:
628ea10c8cc68e57bb60375c11bdb965e08b30843557ea7dd18a62c83f805c08 - Sigstore transparency entry: 1181145360
- Sigstore integration time:
-
Permalink:
ZackaryW/zush@eae1b05a404d259ed16ae9f6759075ac3d3b7d14 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ZackaryW
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eae1b05a404d259ed16ae9f6759075ac3d3b7d14 -
Trigger Event:
workflow_dispatch
-
Statement type: