Reusable CLI kernel for building unified command-line interfaces with consistent behavior, output style, help, and extension semantics.
Project description
cli-core-yo
Reusable CLI framework layer for downstream Python CLIs built on Typer and Rich.
What This Library Is
cli-core-yo is the shared command-line kernel that downstream repositories embed into their own CLI entrypoints. It is responsible for consistent command-tree construction, built-in framework commands, output conventions, runtime context, plugin loading, and XDG path resolution.
It is not a standalone service CLI and it is not the place for downstream business logic. Downstream repos should define one immutable CliSpec, extend the command tree through the registry/plugin interfaces, and keep domain behavior outside this package.
What You Get Out Of The Box
- A root app factory via
create_app(spec)and an execution entrypoint viarun(spec, argv=None). - Built-in root commands:
versionandinfo. - Optional built-in groups:
configandenv. - Deterministic plugin loading: explicit plugin callables first, entry-point plugins second.
- An immutable runtime context exposed through
get_context(). - Consistent human output primitives in
cli_core_yo.output. - Deterministic JSON emission for commands that explicitly expose
--json/-j. - XDG config/data/state/cache directory resolution with Linux and macOS defaults.
NO_COLORsupport for human output andCLI_CORE_YO_DEBUG=1traceback mode.- Secondary helper modules for cert resolution, OAuth URI validation, server lifecycle helpers, and direct XDG path access.
Quick Start
Install the package:
pip install cli-core-yo
Define one CliSpec and route process exit through run():
from cli_core_yo.app import run
from cli_core_yo.spec import CliSpec, XdgSpec
SPEC = CliSpec(
prog_name="my-tool",
app_display_name="My Tool",
dist_name="my-tool",
root_help="Unified CLI for My Tool.",
xdg=XdgSpec(app_dir_name="my-tool"),
)
raise SystemExit(run(SPEC))
That gives the downstream CLI a root Typer app with:
my-tool versionmy-tool info- Typer/Rich help output
- XDG path initialization
- runtime context initialization
If you need the Typer app object directly, use create_app(spec) instead of run():
from cli_core_yo.app import create_app
app = create_app(SPEC)
Core API
| Symbol | Purpose |
|---|---|
create_app(spec) |
Build and return the configured Typer app. |
run(spec, argv=None) |
Execute the CLI and return an integer exit code without calling sys.exit(). |
CommandRegistry |
Register commands, groups, and Typer sub-apps in a deterministic tree. |
get_context() |
Access the current invocation's immutable runtime context. |
output.* |
Emit human output or deterministic JSON. |
resolve_paths() / XdgPaths |
Resolve app-scoped config/data/state/cache directories. |
Spec Objects
CliSpec is the top-level immutable configuration passed into create_app() or run().
| Dataclass | Current fields |
|---|---|
XdgSpec |
app_dir_name |
ConfigSpec |
primary_filename, template_bytes, template_resource, validator |
EnvSpec |
active_env_var, project_root_env_var, activate_script_name, deactivate_script_name |
PluginSpec |
explicit, entry_points |
CliSpec |
prog_name, app_display_name, dist_name, root_help, xdg, config, env, plugins, info_hooks |
ConfigSpec requires exactly one template source:
template_bytes or template_resource.
Extension Model
The supported extension path is:
- define a
CliSpec - register downstream commands through
CommandRegistry - load that registration through explicit plugins or entry points
Do not mutate the root Typer app ad hoc in downstream repos when this library is the framework layer.
Plugin Signature
Plugin callables must have this shape:
from cli_core_yo.registry import CommandRegistry
from cli_core_yo.spec import CliSpec
def register(registry: CommandRegistry, spec: CliSpec) -> None:
...
Example explicit plugin:
from cli_core_yo import output
from cli_core_yo.registry import CommandRegistry
from cli_core_yo.spec import CliSpec
def greet() -> None:
output.success("hello")
def register(registry: CommandRegistry, spec: CliSpec) -> None:
registry.add_command(None, "greet", greet, help_text="Say hello.")
Wire it into the spec:
from cli_core_yo.spec import PluginSpec
SPEC = CliSpec(
prog_name="my-tool",
app_display_name="My Tool",
dist_name="my-tool",
root_help="Unified CLI for My Tool.",
xdg=XdgSpec(app_dir_name="my-tool"),
plugins=PluginSpec(explicit=["my_tool.plugin.register"]),
)
Or expose it as a package entry point:
[project.entry-points."cli_core_yo.plugins"]
my-tool = "my_tool.plugin:register"
Load Order And Registry Rules
spec.plugins.explicitloads first, in list order.spec.plugins.entry_pointsloads second, in list order.- Entry-point group name is
cli_core_yo.plugins. - Root reserved names are
versionandinfo, plusconfigandenvwhen those built-ins are enabled. - Command and group names must match
^[a-z][a-z0-9-]*$. CommandRegistryis frozen before application to the Typer tree; post-freeze mutation raises framework errors.
CommandRegistry supports:
add_group(name, help_text="", order=None)add_command(group_path, name, callback, help_text="", order=None)add_typer_app(group_path, typer_app, name, help_text="", order=None)
Use group_path=None for root-level commands. Nested paths use slash-separated group paths such as "admin/users".
Optional Built-Ins
config
Enable the config group by supplying a ConfigSpec:
from cli_core_yo.spec import CliSpec, ConfigSpec, XdgSpec
SPEC = CliSpec(
prog_name="my-tool",
app_display_name="My Tool",
dist_name="my-tool",
root_help="Unified CLI for My Tool.",
xdg=XdgSpec(app_dir_name="my-tool"),
config=ConfigSpec(
primary_filename="config.json",
template_bytes=b'{"env": "dev"}\n',
),
)
Built-in subcommands:
config pathconfig initconfig showconfig validateconfig editconfig reset
Behavior notes:
config initwrites the configured template and supports--force.config validatecallsvalidator(content)when provided; otherwise it accepts the config.config editshells out toVISUAL,EDITOR, orviand requires an interactive terminal.config resetbacks up the current file to a UTC timestamped.bakbefore rewriting the template.
env
Enable the env group by supplying an EnvSpec:
from cli_core_yo.spec import CliSpec, EnvSpec, XdgSpec
SPEC = CliSpec(
prog_name="my-tool",
app_display_name="My Tool",
dist_name="my-tool",
root_help="Unified CLI for My Tool.",
xdg=XdgSpec(app_dir_name="my-tool"),
env=EnvSpec(
active_env_var="MY_TOOL_ACTIVE",
project_root_env_var="MY_TOOL_ROOT",
activate_script_name="activate.sh",
deactivate_script_name="deactivate.sh",
),
)
Built-in subcommands:
env statusenv activateenv deactivateenv reset
Behavior notes:
env statusreports environment status from the configured env vars.env activate,env deactivate, andenv resetprint shell commands; they do not mutate the caller's shell environment.
Runtime And Output Contract
Use get_context() inside commands and plugins when you need invocation-scoped state:
from cli_core_yo.runtime import get_context
def show_runtime() -> None:
ctx = get_context()
print(ctx.spec.prog_name)
print(ctx.xdg_paths.config)
print(ctx.json_mode)
print(ctx.debug)
RuntimeContext contains:
specxdg_pathsjson_modedebug
For user-facing output, use cli_core_yo.output instead of raw ANSI formatting:
heading(title)success(msg)warning(msg)error(msg)action(msg)detail(msg)bullet(msg)print_text(msg)emit_json(data)
Important behavior:
- Human primitives are automatically suppressed when runtime JSON mode is enabled.
emit_json()writes deterministic JSON: sorted keys,indent=2, UTF-8 passthrough, trailing newline, and no ANSI.NO_COLOR=1disables ANSI styling in human output.CLI_CORE_YO_DEBUG=1enables traceback printing for framework exceptions.run()returns exit codes instead of raisingSystemExititself.
JSON support is command-specific, not universal. In the core library, version and info expose --json / -j. Commands that do not declare JSON flags will treat --json as a usage error.
Practical exit code expectations:
0for success1for framework/domain failures2for Typer/Click usage errors such as unknown commands or invalid options
Secondary Helper Modules
These modules are public and useful, but they are secondary to the CLI-kernel story.
cli_core_yo.xdg
Use this when you need direct access to resolved app directories outside normal command execution.
resolve_paths(xdg_spec)returnsXdgPaths(config, data, state, cache).- Directories are created automatically.
- Linux defaults use
~/.config,~/.local/share,~/.local/state, and~/.cache. - macOS defaults use
~/.config,~/Library/Application Support,~/Library/Logs, and~/Library/Caches.
cli_core_yo.certs
Use this for local HTTPS cert management in downstream service CLIs.
ensure_certs(certs_dir)ensurescert.pemandkey.pem, generating them withmkcertwhen needed.resolve_https_certs(...)resolves cert/key paths by precedence: explicit paths, generic SSL env vars, caller-supplied legacy env vars, shared dir, fallback dir, then optional generation.shared_dayhoff_certs_dir(deploy_name)resolves the Dayhoff shared cert directory under XDG state.cert_status(certs_dir)reports readiness and mkcert/CA status.
cli_core_yo.oauth
Use this for pure URI validation logic around local OAuth/Cognito flows.
- No I/O
- No AWS calls
- Port-alignment and expected-URL validation helpers for app-client configuration
cli_core_yo.server
Use this for service-style CLIs that need small process-management helpers.
- PID file helpers
- timestamped log-file helpers
- process stop helpers
.envsourcing- user-facing host display normalization
Development
Bootstrap a local development environment from the repo root:
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Validation commands used by this repo:
python -m pytest tests/ -v --cov=cli_core_yo
ruff check cli_core_yo tests
ruff format --check cli_core_yo tests
mypy cli_core_yo --ignore-missing-imports
python -m build
twine check dist/*
The package requires Python 3.10+.
Reference
Use SPEC.md as the formal contract for framework behavior.
Use the tests in tests/ as the executable compatibility surface. When documentation and assumptions diverge, the code and tests win.
License
MIT. See LICENSE.
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 cli_core_yo-1.0.3.tar.gz.
File metadata
- Download URL: cli_core_yo-1.0.3.tar.gz
- Upload date:
- Size: 44.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7049f3d7f9234cbc8b313010302e6d6261b8f8a23b7eb41afdd9a974fcc45706
|
|
| MD5 |
5134a60cdba868d113718e25c93bf799
|
|
| BLAKE2b-256 |
7ad04cae24d61c5c1bced7429e87f7c05c684b90d4d6da53ab6c4c433f9e5665
|
File details
Details for the file cli_core_yo-1.0.3-py3-none-any.whl.
File metadata
- Download URL: cli_core_yo-1.0.3-py3-none-any.whl
- Upload date:
- Size: 25.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3caa3e09f1698ca4a70c3b8feee263eaa97adc00e2b8d73ddf22030e5f0f3499
|
|
| MD5 |
913e4514ef7007d2cadf62cdb2cdd08e
|
|
| BLAKE2b-256 |
9aad3eed2eba43709a47e530e114735d767202a253fc0ddd5b38f1762d5e64ce
|