Skip to main content

Reusable CLI kernel for building unified command-line interfaces with consistent behavior, output style, help, and extension semantics.

Project description

cli-core-yo

GitHub Release GitHub Tag License: MIT

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 via run(spec, argv=None).
  • Built-in root commands: version and info.
  • Optional built-in groups: config and env.
  • 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_COLOR support for human output and CLI_CORE_YO_DEBUG=1 traceback 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 version
  • my-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:

  1. define a CliSpec
  2. register downstream commands through CommandRegistry
  3. 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.explicit loads first, in list order.
  • spec.plugins.entry_points loads second, in list order.
  • Entry-point group name is cli_core_yo.plugins.
  • Root reserved names are version and info, plus config and env when those built-ins are enabled.
  • Command and group names must match ^[a-z][a-z0-9-]*$.
  • CommandRegistry is 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 path
  • config init
  • config show
  • config validate
  • config edit
  • config reset

Behavior notes:

  • The effective config file path is resolved once per invocation.
  • When config is enabled, the root command also accepts --config PATH as a one-invocation override.
  • --config PATH must appear before the subcommand, for example my-tool --config ./alt.json config init.
  • Relative --config paths resolve against the current working directory and are normalized to absolute paths for runtime use.
  • config init writes the configured template and supports --force.
  • config validate calls validator(content) when provided; otherwise it accepts the config.
  • config edit shells out to VISUAL, EDITOR, or vi and requires an interactive terminal.
  • config reset backs up the current file to a UTC timestamped .bak before rewriting the template.
  • If --config is 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 edit is 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 status
  • env activate
  • env deactivate
  • env reset

Behavior notes:

  • env status reports environment status from the configured env vars.
  • env activate, env deactivate, and env reset print 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:

  1. explicit CLI arguments
  2. CliSpec configuration in code
  3. config files managed through the built-in config group
  4. 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=1 enables traceback/debug mode in run().
  • NO_COLOR=1 disables ANSI styling in human output.
  • XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME, XDG_CACHE_HOME override the resolved app directories.
  • The built-in env group reads the downstream-defined names from EnvSpec.active_env_var and EnvSpec.project_root_env_var.
  • VISUAL or EDITOR control the editor used by config edit, with vi as fallback.
  • resolve_https_certs() supports the generic SSL_CERT_FILE / SSL_KEY_FILE pair.
  • shared_dayhoff_certs_dir() respects XDG_STATE_HOME.
  • source_env_file() can load a simple .env file into os.environ, but only when downstream code calls it explicitly.

What this library does not do:

  • it does not automatically populate CliSpec from env vars
  • it does not automatically load .env files 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:

  • spec
  • xdg_paths
  • config_path (the effective active config file after any root --config PATH override)
  • json_mode
  • debug

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)
  • print_rich(renderable)
  • emit_json(data)

Important behavior:

  • Human primitives are automatically suppressed when runtime JSON mode is enabled.
  • print_text() writes literal plain text directly to stdout, with no Rich markup interpretation or terminal wrapping.
  • print_rich() is the styled/renderable path for Rich markup or Rich objects.
  • emit_json() writes deterministic JSON: sorted keys, indent=2, UTF-8 passthrough, trailing newline, and no ANSI.
  • NO_COLOR=1 disables ANSI styling in human output.
  • CLI_CORE_YO_DEBUG=1 enables traceback printing for framework exceptions.
  • run() returns exit codes instead of raising SystemExit itself.

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:

  • 0 for success
  • 1 for framework/domain failures
  • 2 for 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) returns XdgPaths(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) ensures cert.pem and key.pem, generating them with mkcert when needed.
  • resolve_https_certs(...) resolves cert/key paths by precedence: explicit paths, a complete SSL_CERT_FILE / SSL_KEY_FILE pair, shared dir, fallback dir, then optional generation.
  • If only one of SSL_CERT_FILE / SSL_KEY_FILE is set, resolve_https_certs(...) keeps checking configured directories and generation before raising a pair-specific env error.
  • 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
  • .env sourcing
  • 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 CliSpec in downstream CLIs and route execution through run(spec, argv=None).
  • Add downstream behavior through CommandRegistry and plugins, not by mutating the root app directly.
  • Respect reserved root names: version, info, and optional built-ins config and env.
  • Use get_context() for invocation-scoped runtime state.
  • Use cli_core_yo.output for human output and emit_json() for machine output instead of custom ANSI or JSON formatting.
  • Keep behavior aligned with SPEC.md and the tests in tests/.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cli_core_yo-1.3.1.tar.gz (50.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cli_core_yo-1.3.1-py3-none-any.whl (29.1 kB view details)

Uploaded Python 3

File details

Details for the file cli_core_yo-1.3.1.tar.gz.

File metadata

  • Download URL: cli_core_yo-1.3.1.tar.gz
  • Upload date:
  • Size: 50.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for cli_core_yo-1.3.1.tar.gz
Algorithm Hash digest
SHA256 9ff593796d9ccef224c302a9fd202973d333187c584f2c298a588f7946f1bf8c
MD5 b0c1c19911f4b42b576393a9f0c1b01d
BLAKE2b-256 b0447fcbf77d543fb2c0ee7ad52888c01064d3bb6d1baaa363f638907173a649

See more details on using hashes here.

File details

Details for the file cli_core_yo-1.3.1-py3-none-any.whl.

File metadata

  • Download URL: cli_core_yo-1.3.1-py3-none-any.whl
  • Upload date:
  • Size: 29.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for cli_core_yo-1.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5cfbd3a71532f4be4ac87b714b0e32b5b7a1790ed1802923b3d8ccde30a85d7f
MD5 e7e299e7f184dc8440ee037f48e95ce7
BLAKE2b-256 50a6eeb79f1b128b801de1b8921c24243eaac34bbf88f92fc5a7d61d509bbdf9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page