preonic-cli includes a lightweight plugin system for extending functionality.
Plugins are Python files or packages placed in the system plugin directory.
They are discovered automatically and can be managed via the plugins command group.
| Platform | Path |
|---|---|
| Linux | ~/.local/share/preonic/plugins/ |
| macOS | ~/Library/Application Support/preonic/plugins/ |
| Windows | %LOCALAPPDATA%\preonic\plugins\ |
preonic plugins listpreonic plugins info <name>From source code (inline):
preonic plugins install hello --source "print('hello')"From a file:
preonic plugins install myplugin --source ./my_plugin.pypreonic plugins remove <name>A plugin is a Python file that follows the naming convention <name>.py and
is placed in the plugin directory.
"""my_plugin.py — A simple preonic plugin."""
from __future__ import annotations
def register(app) -> None:
"""Register commands with the Typer app.
This function is called by the plugin loader when the plugin is discovered.
"""
import typer
@app.command()
def hello(name: str = "World") -> None:
"""Say hello from my plugin."""
print(f"Hello, {name}!")The register(app) function receives the root Typer application instance.
Inside this function, you can:
- Add commands with
@app.command() - Add command groups with
app.add_typer() - Access the Typer app's full API
- Import and use any preonic-cli services
"""my_plugin.py — Plugin using preonic services."""
from __future__ import annotations
def register(app) -> None:
import typer
from rich.console import Console
console = Console()
@app.command()
def hash_text(text: str, algorithm: str = "sha256") -> None:
"""Hash text using preonic's developer service."""
from preonic.services.developer import DeveloperService
result = DeveloperService.hash_data(text, algorithm=algorithm)
console.print(f"[bold cyan]{algorithm}:[/] {result}")For more complex plugins, create a directory with __init__.py:
~/.local/share/preonic/plugins/
└── my_plugin/
├── __init__.py
├── commands.py
└── utils.py
The __init__.py must expose a register(app) function.
The PluginService scans the plugin directory on startup:
- Looks for
.pyfiles (single-file plugins) - Looks for directories with
__init__.py(package plugins) - Ignores files/directories starting with
_ - Calls
register(app)for each discovered plugin
- Plugins execute arbitrary Python code
- Only install plugins from trusted sources
- Review plugin code before installation
- Plugins run in the same process as preonic-cli
class PluginService:
@staticmethod
def list_plugins() -> list[PluginInfo]
"""Return all discovered plugins."""
@staticmethod
def plugin_info(name: str) -> PluginInfo
"""Get metadata for a specific plugin."""
@staticmethod
def install_plugin(name: str, source: str | Path) -> Path
"""Install a plugin from source code or file path."""
@staticmethod
def remove_plugin(name: str) -> None
"""Remove an installed plugin."""
@staticmethod
def get_plugin_dir() -> Path
"""Return the plugin directory path."""
@staticmethod
def set_plugin_dir(path: Path) -> None
"""Override the plugin directory (useful for testing)."""class PluginInfo:
name: str # Plugin name
path: Path # File or directory path
enabled: bool # Whether the plugin is enabled
@property
def metadata(self) -> dict[str, Any]
"""Plugin metadata from __init__.py."""
def to_dict(self) -> dict[str, Any]
"""Serialize to dictionary."""