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 |
xdg_relative_path, absolute_path, 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 location source:
xdg_relative_path or absolute_path.
ConfigSpec also 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(
xdg_relative_path="config.json",
template_bytes=b'{"env": "dev"}\n',
),
)
Built-in subcommands:
config pathconfig initconfig showconfig validateconfig editconfig reset
Behavior notes:
- The effective config file path is resolved once per invocation.
- When config is enabled, the root command also accepts
--config PATHas a one-invocation override. --config PATHmust appear before the subcommand, for examplemy-tool --config ./alt.json config init.- Relative
--configpaths resolve against the current working directory and are normalized to absolute paths for runtime use. 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.- If
--configis absent, the invocation falls back to the spec-defined default path. - Downstream commands and plugins can read the effective active file path from
get_context().config_path. - There is no built-in parsed-config API or key-level edit/set/update facility.
config editis whole-file editor launch only; it does not perform structured mutation.- Downstream code is responsible for loading, parsing, and updating config values after creation.
Example fixed absolute default:
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(
absolute_path="/tmp/my-tool-config.json",
template_bytes=b'{"env": "dev"}\n',
),
)
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.
Environment Variables
This library supports a small set of environment-variable hooks, but env vars are discouraged in most cases. Treat them as escape hatches for process-scoped overrides, not the default configuration API between layers.
Prefer, in order:
- explicit CLI arguments
CliSpecconfiguration in code- config files managed through the built-in
configgroup - environment variables only for process-scoped overrides or integration boundaries
There is no generic env-to-CliSpec mapping layer in cli-core-yo.
Supported environment-variable behavior:
CLI_CORE_YO_DEBUG=1enables traceback/debug mode inrun().NO_COLOR=1disables ANSI styling in human output.XDG_CONFIG_HOME,XDG_DATA_HOME,XDG_STATE_HOME,XDG_CACHE_HOMEoverride the resolved app directories.- The built-in
envgroup reads the downstream-defined names fromEnvSpec.active_env_varandEnvSpec.project_root_env_var. VISUALorEDITORcontrol the editor used byconfig edit, withvias fallback.resolve_https_certs()supportsSSL_CERT_FILEandSSL_KEY_FILE, plus caller-supplied legacy env-var names.shared_dayhoff_certs_dir()respectsXDG_STATE_HOME.source_env_file()can load a simple.envfile intoos.environ, but only when downstream code calls it explicitly.
What this library does not do:
- it does not automatically populate
CliSpecfrom env vars - it does not automatically load
.envfiles during startup - it does not use env vars as the primary extension/configuration mechanism
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.config_path)
print(ctx.xdg_paths.config)
print(ctx.json_mode)
print(ctx.debug)
RuntimeContext contains:
specxdg_pathsconfig_path(the effective active config file after any root--config PATHoverride)json_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.
Guidance For AI Agents
If you are modifying this repository with an AI coding agent, treat cli-core-yo as a shared CLI framework layer, not as a standalone service CLI.
- Prefer framework extension points over ad-hoc Typer wiring.
- Define exactly one immutable
CliSpecin downstream CLIs and route execution throughrun(spec, argv=None). - Add downstream behavior through
CommandRegistryand plugins, not by mutating the root app directly. - Respect reserved root names:
version,info, and optional built-insconfigandenv. - Use
get_context()for invocation-scoped runtime state. - Use
cli_core_yo.outputfor human output andemit_json()for machine output instead of custom ANSI or JSON formatting. - Keep behavior aligned with
SPEC.mdand the tests intests/. - Run the repo validation commands before handing work back:
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/*.
For the repo-specific agent policy, see AI_DIRECTIVE.md.
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.3.0.tar.gz.
File metadata
- Download URL: cli_core_yo-1.3.0.tar.gz
- Upload date:
- Size: 50.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d168089465a328ac9eefe17eb833b9e3c8f044231484e712b1d012f73f1b4227
|
|
| MD5 |
0bf4b339be50286c2c8e438c480027ab
|
|
| BLAKE2b-256 |
b8ab411f5a3832d202c586a3d15ba240077fdddee240e44b3ffb40bc85e093b1
|
File details
Details for the file cli_core_yo-1.3.0-py3-none-any.whl.
File metadata
- Download URL: cli_core_yo-1.3.0-py3-none-any.whl
- Upload date:
- Size: 28.7 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 |
ed7e26003f3d9ea61bc0ee0c5ea10385f7be7450f1e383b368b8eb8a2c081543
|
|
| MD5 |
2e22c49f7846cd8a671aab43c538ee67
|
|
| BLAKE2b-256 |
249331754e14b4a2d567bcea317546a9790926a28d4e2ece8867156adc47d995
|