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
| Module | Responsibility |
|---|---|
_shared.py | console singleton, _ui singleton, configure_console(), and all cross-command utilities (_build_exclusion_manager, _output_json_findings, _render_link_error, etc.) |
_check.py | check_app Typer sub-app + seven check * commands; private helpers re-exported from _governance.py, _target_resolver.py, and _command_setup.py |
_command_setup.py | setup_command() factory — consolidates repo-root discovery, config loading, target resolution, and exclusion-manager construction used by all check commands |
_clean.py | clean_app Typer sub-app + clean assets command |
_config_explain.py | explain command + config genealogy / rule introspection surface |
_governance.py | config_app Typer sub-app + governance profile commands + per-file-ignore and directory-policy filter helpers (_apply_per_file_ignores, _apply_directory_policies) |
_guard.py | guard_app Typer sub-app + scan / init commands for the fast secret guard |
_inspect.py | inspect_app Typer sub-app + capabilities, codes, and routes commands |
_lab.py | lab command + interactive scenario showcase |
_metadata.py | Single source of truth for root help panels, command grouping, and short help text |
_standalone.py | score, 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__.py | Public re-export surface consumed by main.py — do 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 throughget_ui()andget_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.DIMfor 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
_EMOJIinzenzic/core/ui.pybefore 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
- Create
src/zenzic/cli/_myfeature.pywithmyfeature_app = typer.Typer(...)and your commands. - Export
myfeature_appfromsrc/zenzic/cli/__init__.py. - Register in
src/zenzic/main.py:app.add_typer(myfeature_app, name="myfeature", rich_help_panel="..."). - Add a
CommandMeta(...)entry insrc/zenzic/cli/_metadata.pyso root help panels and short help stay authoritative. - If the sub-app uses
no_args_is_help=True, add"myfeature"to the_SUBAPPS_WITH_MENUfrozenset incli_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:
| Code | Meaning |
|---|---|
0 | All checks passed |
1 | Quality issues found |
2 | SECURITY — leaked credential detected |
3 | SECURITY — 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.