Skip to main content

CLI Architecture

The CLI is organised as a package (src/zenzic/cli/) rather than a single module. Each file owns one domain of responsibility.


Module Map

ModuleResponsibility
_shared.pyconsole singleton, _ui singleton, configure_console(), and all cross-command utilities (_build_exclusion_manager, _output_json_findings, _render_link_error, etc.)
_check.pycheck_app Typer sub-app + seven check * commands; private helpers re-exported from _governance.py, _target_resolver.py, and _command_setup.py
_command_setup.pysetup_command() factory — consolidates repo-root discovery, config loading, target resolution, and exclusion-manager construction used by all check commands
_clean.pyclean_app Typer sub-app + clean assets command
_config_explain.pyexplain command + config genealogy / rule introspection surface
_governance.pyconfig_app Typer sub-app + governance profile commands + per-file-ignore and directory-policy filter helpers (_apply_per_file_ignores, _apply_directory_policies)
_guard.pyguard_app Typer sub-app + scan / init commands for the fast secret guard
_inspect.pyinspect_app Typer sub-app + capabilities, codes, and routes commands
_lab.pylab command + interactive scenario showcase
_metadata.pySingle source of truth for root help panels, command grouping, and short help text
_standalone.pyscore, diff, and init commands + their private helpers
_target_resolver.py_resolve_target() and _apply_target() — path lookup and config-patching helpers shared by check commands and the lab command
__init__.pyPublic re-export surface consumed by main.pydo not add logic here

main.py is the unified Typer registration factory. New top-level commands and sub-apps must be registered there, and root help metadata must stay aligned with _metadata.py.


The Visual State Manager

_shared.py is the sole owner of all console and UI state. This is the most critical architectural rule in the CLI layer:

PROHIBITION: No command module may instantiate Console() or a custom UI class directly. All output must go through get_ui() and get_console() from _shared.py.

# ✅ Correct — in any _check.py / _clean.py / _standalone.py command
from . import _shared
_shared.get_ui().print_header(__version__)
_shared.get_console().print("output")

# ❌ FORBIDDEN — never do this in a command module
from rich.console import Console
from mypackage.ui import LegacyInterfaceV1
console = Console(...) # breaks shared state
ui = LegacyInterfaceV1(console) # creates an orphaned instance

This rule exists because configure_console() replaces the module-level console and _ui singletons when --no-color or --force-color is passed. Any locally-created Console or UI instance will be frozen at the pre-flag state and ignore the user's color preference.

The force_terminal parameter of the module-level Console is always None (auto-detect via sys.stdout.isatty()), never False. Setting force_terminal=False is a silent bug that strips all ANSI styling even in interactive terminals.

UI output conventions:

  • Always use ZenzicPalette.DIM for dim/secondary text — never the raw Rich tag [dim].
  • Vertical spacing: compact (Ruff-style). No blank lines between individual footer lines. Use Rule() separators only to divide major report sections.
  • New symbols must be added to _EMOJI in zenzic/core/ui.py before use — never inline Unicode literals.

Adding a Command to an Existing Sub-App

# src/zenzic/cli/_check.py (example: adding "check metadata")
@check_app.command(name="metadata")
def check_metadata(path: Path = ...) -> None:
...

No changes to __init__.py, main.py, or _metadata.py are required — the existing Typer sub-app already owns this command surface.


Adding a New Top-Level Sub-App

  1. Create src/zenzic/cli/_myfeature.py with myfeature_app = typer.Typer(...) and your commands.
  2. Export myfeature_app from src/zenzic/cli/__init__.py.
  3. Register in src/zenzic/main.py: app.add_typer(myfeature_app, name="myfeature", rich_help_panel="...").
  4. Add a CommandMeta(...) entry in src/zenzic/cli/_metadata.py so root help panels and short help stay authoritative.
  5. If the sub-app uses no_args_is_help=True, add "myfeature" to the _SUBAPPS_WITH_MENU frozenset in cli_main() so the Zenzic banner appears when the sub-app is invoked with no arguments.

Exit Codes

The CLI exits with one of four codes. These are frozen — do not add new exit codes without an explicit architecture decision:

CodeMeaning
0All checks passed
1Quality issues found
2SECURITY — leaked credential detected
3SECURITY — system-path traversal detected

PLUGIN_FORBIDDEN_EXITS enforces that third-party adapters cannot emit exit codes outside this set. See the Adapter API reference for the full contract.