Skip to content

Utilities Module

The utilities module (cli_utils.py) provides shared infrastructure used across all CLI handlers.

Overview

This module contains:

  • Color System: HCL color palette with true color support and 16-color fallback
  • ConsequenceType: Dual-tense action labels (prompt/result) with semantic colors
  • ResultReporter: Unified rendering system for mutation commands
  • TableFormatter: Aligned table output for list commands
  • Error Formatting: Structured validation and error messages
  • Parsing Utilities: Functions for parsing command-line arguments

Key Components

Color System

  • Color enum: HCL color palette with semantic mapping
  • _colors_enabled(): TTY detection and NO_COLOR support
  • _supports_truecolor(): True color capability detection
  • highlight(): Entity name highlighting for show commands

ConsequenceType System

  • Dual-tense labels (present for prompts, past for results)
  • Semantic color mapping (green=constructive, red=destructive, etc.)
  • Categories: Constructive, Recovery, Destructive, Modification, Transfer, Informational, No-op

Output Formatting

  • ResultReporter: Tracks consequences and renders with tense-aware colors
  • TableFormatter: Renders aligned tables with auto-width support
  • Consequence: Data model for nested consequences

Error Handling

  • ValidationError: Structured validation errors with field and suggestion
  • format_validation_error(): Formatted error output
  • format_info(): Info messages
  • format_warning(): Warning messages

Utilities

  • request_confirmation(): User confirmation with auto-approve support
  • parse_env_vars(): Parse KEY=VALUE environment variables
  • parse_header(): Parse KEY=VALUE HTTP headers
  • parse_input(): Parse VS Code input variable definitions
  • parse_host_list(): Parse comma-separated hosts or 'all'
  • get_package_mcp_server_config(): Extract MCP config from package metadata

Module Reference

hatch.cli.cli_utils

Shared utilities for Hatch CLI.

This module provides common utilities used across CLI handlers, extracted from the monolithic cli_hatch.py to enable cleaner handler-based architecture and easier testing.

Constants

EXIT_SUCCESS (int): Exit code for successful operations (0) EXIT_ERROR (int): Exit code for failed operations (1)

Classes:

Name Description
Color

ANSI color codes with brightness variants for tense distinction

Functions:

Name Description
get_hatch_version

Retrieve version from package metadata

request_confirmation

Interactive user confirmation with auto-approve support

parse_env_vars

Parse KEY=VALUE environment variable arguments

parse_header

Parse KEY=VALUE HTTP header arguments

parse_input

Parse VSCode input configurations

parse_host_list

Parse comma-separated host list or 'all'

get_package_mcp_server_config

Extract MCP server config from package metadata

_colors_enabled

Check if color output should be enabled

Example

from hatch.cli.cli_utils import EXIT_SUCCESS, EXIT_ERROR, request_confirmation if request_confirmation("Proceed?", auto_approve=False): ... return EXIT_SUCCESS ... else: ... return EXIT_ERROR

from hatch.cli.cli_utils import parse_env_vars env_dict = parse_env_vars(["API_KEY=secret", "DEBUG=true"])

Returns:

Classes

Color

Bases: Enum

HCL color palette with true color support and 16-color fallback.

Uses a qualitative HCL palette with equal perceived brightness for accessibility and visual harmony. True color (24-bit) is used when supported, falling back to standard 16-color ANSI codes.

Reference: R12 §3.2 (12-enhancing_colors_v0.md) Reference: R06 §3.1 (06-dependency_analysis_v0.md) Reference: R03 §4 (03-mutation_output_specification_v0.md)

HCL Palette Values

GREEN #80C990 → rgb(128, 201, 144) RED #EFA6A2 → rgb(239, 166, 162) YELLOW #C8C874 → rgb(200, 200, 116) BLUE #A3B8EF → rgb(163, 184, 239) MAGENTA #E6A3DC → rgb(230, 163, 220) CYAN #50CACD → rgb(80, 202, 205) GRAY #808080 → rgb(128, 128, 128) AMBER #A69460 → rgb(166, 148, 96)

Color Semantics

Green → Constructive (CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE) Blue → Recovery (RESTORE) Red → Destructive (REMOVE, DELETE, CLEAN) Yellow → Modification (SET, UPDATE) Magenta → Transfer (SYNC) Cyan → Informational (VALIDATE) Gray → No-op (SKIP, EXISTS, UNCHANGED) Amber → Entity highlighting (show commands)

Example

from hatch.cli.cli_utils import Color, _colors_enabled if _colors_enabled(): ... print(f"{Color.GREEN.value}Success{Color.RESET.value}") ... else: ... print("Success")

Source code in hatch/cli/cli_utils.py
class Color(Enum):
    """HCL color palette with true color support and 16-color fallback.

    Uses a qualitative HCL palette with equal perceived brightness
    for accessibility and visual harmony. True color (24-bit) is used
    when supported, falling back to standard 16-color ANSI codes.

    Reference: R12 §3.2 (12-enhancing_colors_v0.md)
    Reference: R06 §3.1 (06-dependency_analysis_v0.md)
    Reference: R03 §4 (03-mutation_output_specification_v0.md)

    HCL Palette Values:
        GREEN   #80C990 → rgb(128, 201, 144)
        RED     #EFA6A2 → rgb(239, 166, 162)
        YELLOW  #C8C874 → rgb(200, 200, 116)
        BLUE    #A3B8EF → rgb(163, 184, 239)
        MAGENTA #E6A3DC → rgb(230, 163, 220)
        CYAN    #50CACD → rgb(80, 202, 205)
        GRAY    #808080 → rgb(128, 128, 128)
        AMBER   #A69460 → rgb(166, 148, 96)

    Color Semantics:
        Green   → Constructive (CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE)
        Blue    → Recovery (RESTORE)
        Red     → Destructive (REMOVE, DELETE, CLEAN)
        Yellow  → Modification (SET, UPDATE)
        Magenta → Transfer (SYNC)
        Cyan    → Informational (VALIDATE)
        Gray    → No-op (SKIP, EXISTS, UNCHANGED)
        Amber   → Entity highlighting (show commands)

    Example:
        >>> from hatch.cli.cli_utils import Color, _colors_enabled
        >>> if _colors_enabled():
        ...     print(f"{Color.GREEN.value}Success{Color.RESET.value}")
        ... else:
        ...     print("Success")
    """

    # === Bright colors (execution results - past tense) ===

    # Green #80C990 - CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE
    GREEN = "\033[38;2;128;201;144m" if TRUECOLOR else "\033[92m"

    # Red #EFA6A2 - REMOVE, DELETE, CLEAN
    RED = "\033[38;2;239;166;162m" if TRUECOLOR else "\033[91m"

    # Yellow #C8C874 - SET, UPDATE
    YELLOW = "\033[38;2;200;200;116m" if TRUECOLOR else "\033[93m"

    # Blue #A3B8EF - RESTORE
    BLUE = "\033[38;2;163;184;239m" if TRUECOLOR else "\033[94m"

    # Magenta #E6A3DC - SYNC
    MAGENTA = "\033[38;2;230;163;220m" if TRUECOLOR else "\033[95m"

    # Cyan #50CACD - VALIDATE
    CYAN = "\033[38;2;80;202;205m" if TRUECOLOR else "\033[96m"

    # === Dim colors (confirmation prompts - present tense) ===

    # Aquamarine #5ACCAF (green shifted)
    GREEN_DIM = "\033[38;2;90;204;175m" if TRUECOLOR else "\033[2;32m"

    # Orange #E0AF85 (red shifted)
    RED_DIM = "\033[38;2;224;175;133m" if TRUECOLOR else "\033[2;31m"

    # Amber #A69460 (yellow shifted)
    YELLOW_DIM = "\033[38;2;166;148;96m" if TRUECOLOR else "\033[2;33m"

    # Violet #CCACED (blue shifted)
    BLUE_DIM = "\033[38;2;204;172;237m" if TRUECOLOR else "\033[2;34m"

    # Rose #F2A1C2 (magenta shifted)
    MAGENTA_DIM = "\033[38;2;242;161;194m" if TRUECOLOR else "\033[2;35m"

    # Azure #74C3E4 (cyan shifted)
    CYAN_DIM = "\033[38;2;116;195;228m" if TRUECOLOR else "\033[2;36m"

    # === Utility colors ===

    # Gray #808080 - SKIP, EXISTS, UNCHANGED
    GRAY = "\033[38;2;128;128;128m" if TRUECOLOR else "\033[90m"

    # Amber #A69460 - Entity name highlighting (NEW)
    AMBER = "\033[38;2;166;148;96m" if TRUECOLOR else "\033[33m"

    # Reset
    RESET = "\033[0m"

ColumnDef dataclass

Column definition for TableFormatter.

Reference: R06 §3.6 (06-dependency_analysis_v0.md) Reference: R02 §5 (02-list_output_format_specification_v2.md)

Attributes:

Name Type Description
name str

Column header text

width Union[int, Literal['auto']]

Fixed width (int) or "auto" for auto-calculation

align Literal['left', 'right', 'center']

Text alignment ("left", "right", "center")

Example

col = ColumnDef(name="Name", width=20, align="left") col_auto = ColumnDef(name="Count", width="auto", align="right")

Source code in hatch/cli/cli_utils.py
@dataclass
class ColumnDef:
    """Column definition for TableFormatter.

    Reference: R06 §3.6 (06-dependency_analysis_v0.md)
    Reference: R02 §5 (02-list_output_format_specification_v2.md)

    Attributes:
        name: Column header text
        width: Fixed width (int) or "auto" for auto-calculation
        align: Text alignment ("left", "right", "center")

    Example:
        >>> col = ColumnDef(name="Name", width=20, align="left")
        >>> col_auto = ColumnDef(name="Count", width="auto", align="right")
    """

    name: str
    width: Union[int, Literal["auto"]]
    align: Literal["left", "right", "center"] = "left"

Consequence dataclass

Data model for a single consequence (resource or field level).

Consequences represent actions that will be or have been performed. They can be nested to show resource-level actions with field-level details.

Reference: R06 §3.3 (06-dependency_analysis_v0.md) Reference: R04 §5.1 (04-reporting_infrastructure_coexistence_v0.md)

Attributes:

Name Type Description
type ConsequenceType

The ConsequenceType indicating the action category

message str

Human-readable description of the consequence

children List[Consequence]

Nested consequences (e.g., field-level details under resource)

Invariants
  • children only populated for resource-level consequences
  • field-level consequences have empty children list
  • nesting limited to 2 levels (resource → field)
Example

parent = Consequence( ... type=ConsequenceType.CONFIGURE, ... message="Server 'weather' on 'claude-desktop'", ... children=[ ... Consequence(ConsequenceType.UPDATE, "command: None → 'python'"), ... Consequence(ConsequenceType.SKIP, "timeout: unsupported"), ... ] ... )

Source code in hatch/cli/cli_utils.py
@dataclass
class Consequence:
    """Data model for a single consequence (resource or field level).

    Consequences represent actions that will be or have been performed.
    They can be nested to show resource-level actions with field-level details.

    Reference: R06 §3.3 (06-dependency_analysis_v0.md)
    Reference: R04 §5.1 (04-reporting_infrastructure_coexistence_v0.md)

    Attributes:
        type: The ConsequenceType indicating the action category
        message: Human-readable description of the consequence
        children: Nested consequences (e.g., field-level details under resource)

    Invariants:
        - children only populated for resource-level consequences
        - field-level consequences have empty children list
        - nesting limited to 2 levels (resource → field)

    Example:
        >>> parent = Consequence(
        ...     type=ConsequenceType.CONFIGURE,
        ...     message="Server 'weather' on 'claude-desktop'",
        ...     children=[
        ...         Consequence(ConsequenceType.UPDATE, "command: None → 'python'"),
        ...         Consequence(ConsequenceType.SKIP, "timeout: unsupported"),
        ...     ]
        ... )
    """

    type: ConsequenceType
    message: str
    children: List["Consequence"] = field(default_factory=list)

ConsequenceType

Bases: Enum

Action types with dual-tense labels and semantic colors.

Each consequence type has: - prompt_label: Present tense for confirmation prompts (e.g., "CREATE") - result_label: Past tense for execution results (e.g., "CREATED") - prompt_color: Dim color for prompts - result_color: Bright color for results

Reference: R06 §3.2 (06-dependency_analysis_v0.md) Reference: R03 §2 (03-mutation_output_specification_v0.md)

Categories

Constructive (Green): CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE Recovery (Blue): RESTORE Destructive (Red): REMOVE, DELETE, CLEAN Modification (Yellow): SET, UPDATE Transfer (Magenta): SYNC Informational (Cyan): VALIDATE No-op (Gray): SKIP, EXISTS, UNCHANGED

Example

ct = ConsequenceType.CREATE print(f"[{ct.prompt_label}]") # [CREATE] print(f"[{ct.result_label}]") # [CREATED]

Source code in hatch/cli/cli_utils.py
class ConsequenceType(Enum):
    """Action types with dual-tense labels and semantic colors.

    Each consequence type has:
    - prompt_label: Present tense for confirmation prompts (e.g., "CREATE")
    - result_label: Past tense for execution results (e.g., "CREATED")
    - prompt_color: Dim color for prompts
    - result_color: Bright color for results

    Reference: R06 §3.2 (06-dependency_analysis_v0.md)
    Reference: R03 §2 (03-mutation_output_specification_v0.md)

    Categories:
        Constructive (Green): CREATE, ADD, CONFIGURE, INSTALL, INITIALIZE
        Recovery (Blue): RESTORE
        Destructive (Red): REMOVE, DELETE, CLEAN
        Modification (Yellow): SET, UPDATE
        Transfer (Magenta): SYNC
        Informational (Cyan): VALIDATE
        No-op (Gray): SKIP, EXISTS, UNCHANGED

    Example:
        >>> ct = ConsequenceType.CREATE
        >>> print(f"[{ct.prompt_label}]")  # [CREATE]
        >>> print(f"[{ct.result_label}]")  # [CREATED]
    """

    # Value format: (prompt_label, result_label, prompt_color, result_color)

    # Constructive actions (Green)
    CREATE = ("CREATE", "CREATED", Color.GREEN_DIM, Color.GREEN)
    ADD = ("ADD", "ADDED", Color.GREEN_DIM, Color.GREEN)
    CONFIGURE = ("CONFIGURE", "CONFIGURED", Color.GREEN_DIM, Color.GREEN)
    INSTALL = ("INSTALL", "INSTALLED", Color.GREEN_DIM, Color.GREEN)
    INITIALIZE = ("INITIALIZE", "INITIALIZED", Color.GREEN_DIM, Color.GREEN)

    # Recovery actions (Blue)
    RESTORE = ("RESTORE", "RESTORED", Color.BLUE_DIM, Color.BLUE)

    # Destructive actions (Red)
    REMOVE = ("REMOVE", "REMOVED", Color.RED_DIM, Color.RED)
    DELETE = ("DELETE", "DELETED", Color.RED_DIM, Color.RED)
    CLEAN = ("CLEAN", "CLEANED", Color.RED_DIM, Color.RED)

    # Modification actions (Yellow)
    SET = ("SET", "SET", Color.YELLOW_DIM, Color.YELLOW)  # Irregular: no change
    UPDATE = ("UPDATE", "UPDATED", Color.YELLOW_DIM, Color.YELLOW)

    # Transfer actions (Magenta)
    SYNC = ("SYNC", "SYNCED", Color.MAGENTA_DIM, Color.MAGENTA)

    # Informational actions (Cyan)
    VALIDATE = ("VALIDATE", "VALIDATED", Color.CYAN_DIM, Color.CYAN)
    INFO = ("INFO", "INFO", Color.CYAN_DIM, Color.CYAN)

    # No-op actions (Gray) - same color for prompt and result
    SKIP = ("SKIP", "SKIPPED", Color.GRAY, Color.GRAY)
    EXISTS = ("EXISTS", "EXISTS", Color.GRAY, Color.GRAY)  # Irregular: no change
    UNCHANGED = (
        "UNCHANGED",
        "UNCHANGED",
        Color.GRAY,
        Color.GRAY,
    )  # Irregular: no change

    @property
    def prompt_label(self) -> str:
        """Present tense label for confirmation prompts."""
        return self.value[0]

    @property
    def result_label(self) -> str:
        """Past tense label for execution results."""
        return self.value[1]

    @property
    def prompt_color(self) -> Color:
        """Dim color for confirmation prompts."""
        return self.value[2]

    @property
    def result_color(self) -> Color:
        """Bright color for execution results."""
        return self.value[3]
Attributes
prompt_color property

Dim color for confirmation prompts.

prompt_label property

Present tense label for confirmation prompts.

result_color property

Bright color for execution results.

result_label property

Past tense label for execution results.

ResultReporter

Unified rendering system for all CLI output.

Tracks consequences and renders them with tense-aware, color-coded output. Present tense (dim colors) for confirmation prompts, past tense (bright colors) for execution results.

Reference: R06 §3.4 (06-dependency_analysis_v0.md) Reference: R04 §5.2 (04-reporting_infrastructure_coexistence_v0.md) Reference: R01 §8.2 (01-cli_output_analysis_v2.md)

Attributes:

Name Type Description
command_name str

Display name for the command (e.g., "hatch mcp configure")

dry_run bool

If True, append "- DRY RUN" suffix to result labels

consequences List[Consequence]

List of tracked consequences in order of addition

Invariants
  • consequences list is append-only
  • report_prompt() and report_result() are idempotent
  • Order of add() calls determines output order
Example

reporter = ResultReporter("hatch env create", dry_run=False) reporter.add(ConsequenceType.CREATE, "Environment 'dev'") reporter.add(ConsequenceType.CREATE, "Python environment (3.11)") prompt = reporter.report_prompt() # Present tense, dim colors

... user confirms ...

reporter.report_result() # Past tense, bright colors

Source code in hatch/cli/cli_utils.py
class ResultReporter:
    """Unified rendering system for all CLI output.

    Tracks consequences and renders them with tense-aware, color-coded output.
    Present tense (dim colors) for confirmation prompts, past tense (bright colors)
    for execution results.

    Reference: R06 §3.4 (06-dependency_analysis_v0.md)
    Reference: R04 §5.2 (04-reporting_infrastructure_coexistence_v0.md)
    Reference: R01 §8.2 (01-cli_output_analysis_v2.md)

    Attributes:
        command_name: Display name for the command (e.g., "hatch mcp configure")
        dry_run: If True, append "- DRY RUN" suffix to result labels
        consequences: List of tracked consequences in order of addition

    Invariants:
        - consequences list is append-only
        - report_prompt() and report_result() are idempotent
        - Order of add() calls determines output order

    Example:
        >>> reporter = ResultReporter("hatch env create", dry_run=False)
        >>> reporter.add(ConsequenceType.CREATE, "Environment 'dev'")
        >>> reporter.add(ConsequenceType.CREATE, "Python environment (3.11)")
        >>> prompt = reporter.report_prompt()  # Present tense, dim colors
        >>> # ... user confirms ...
        >>> reporter.report_result()  # Past tense, bright colors
    """

    def __init__(self, command_name: str, dry_run: bool = False):
        """Initialize ResultReporter.

        Args:
            command_name: Display name for the command
            dry_run: If True, results show "- DRY RUN" suffix
        """
        self._command_name = command_name
        self._dry_run = dry_run
        self._consequences: List[Consequence] = []

    @property
    def command_name(self) -> str:
        """Display name for the command."""
        return self._command_name

    @property
    def dry_run(self) -> bool:
        """Whether this is a dry-run preview."""
        return self._dry_run

    @property
    def consequences(self) -> List[Consequence]:
        """List of tracked consequences in order of addition."""
        return self._consequences

    def add(
        self,
        consequence_type: ConsequenceType,
        message: str,
        children: Optional[List[Consequence]] = None,
    ) -> None:
        """Add a consequence with optional nested children.

        Args:
            consequence_type: The type of action
            message: Human-readable description
            children: Optional nested consequences (e.g., field-level details)

        Invariants:
            - Order of add() calls determines output order
            - Children inherit parent's tense during rendering
        """
        consequence = Consequence(
            type=consequence_type, message=message, children=children or []
        )
        self._consequences.append(consequence)

    def add_from_conversion_report(self, report: "ConversionReport") -> None:
        """Convert ConversionReport field operations to nested consequences.

        Maps ConversionReport data to the unified consequence model:
        - report.operation → resource ConsequenceType
        - field_op "UPDATED" → ConsequenceType.UPDATE
        - field_op "UNSUPPORTED" → ConsequenceType.SKIP
        - field_op "UNCHANGED" → ConsequenceType.UNCHANGED

        Reference: R06 §3.5 (06-dependency_analysis_v0.md)
        Reference: R04 §1.2 (04-reporting_infrastructure_coexistence_v0.md)

        Args:
            report: ConversionReport with field operations to convert

        Invariants:
            - All field operations become children of resource consequence
            - UNSUPPORTED fields include "(unsupported by host)" suffix
        """
        # Import here to avoid circular dependency

        # Map report.operation to resource ConsequenceType
        operation_map = {
            "create": ConsequenceType.CONFIGURE,
            "update": ConsequenceType.CONFIGURE,
            "delete": ConsequenceType.REMOVE,
            "migrate": ConsequenceType.CONFIGURE,
        }
        resource_type = operation_map.get(report.operation, ConsequenceType.CONFIGURE)

        # Build resource message
        resource_message = (
            f"Server '{report.server_name}' on '{report.target_host.value}'"
        )

        # Map field operations to child consequences
        field_op_map = {
            "UPDATED": ConsequenceType.UPDATE,
            "UNSUPPORTED": ConsequenceType.SKIP,
            "UNCHANGED": ConsequenceType.UNCHANGED,
        }

        children = []
        for field_op in report.field_operations:
            child_type = field_op_map.get(field_op.operation, ConsequenceType.UPDATE)

            # Format field message based on operation type
            if field_op.operation == "UPDATED":
                child_message = f"{field_op.field_name}: {repr(field_op.old_value)}{repr(field_op.new_value)}"
            elif field_op.operation == "UNSUPPORTED":
                child_message = f"{field_op.field_name}: (unsupported by host)"
            else:  # UNCHANGED
                child_message = f"{field_op.field_name}: {repr(field_op.new_value)}"

            children.append(Consequence(type=child_type, message=child_message))

        # Add the resource consequence with children
        self.add(resource_type, resource_message, children=children)

    def _format_consequence(
        self, consequence: Consequence, use_result_tense: bool, indent: int = 2
    ) -> str:
        """Format a single consequence with color and tense.

        Args:
            consequence: The consequence to format
            use_result_tense: True for past tense (result), False for present (prompt)
            indent: Number of spaces for indentation

        Returns:
            Formatted string with optional ANSI colors
        """
        ct = consequence.type
        label = ct.result_label if use_result_tense else ct.prompt_label
        color = ct.result_color if use_result_tense else ct.prompt_color

        # Add dry-run suffix for results
        if use_result_tense and self._dry_run:
            label = f"{label} - DRY RUN"

        # Format with or without colors
        indent_str = " " * indent
        if _colors_enabled():
            line = f"{indent_str}{color.value}[{label}]{Color.RESET.value} {consequence.message}"
        else:
            line = f"{indent_str}[{label}] {consequence.message}"

        return line

    def report_prompt(self) -> str:
        """Generate confirmation prompt (present tense, dim colors).

        Output format:
            {command_name}:
              [VERB] resource message
                [VERB] field message
                [VERB] field message

        Returns:
            Formatted prompt string, empty string if no consequences.

        Invariants:
            - All consequences shown (including UNCHANGED, SKIP)
            - Empty string if no consequences
        """
        if not self._consequences:
            return ""

        lines = [f"{self._command_name}:"]

        for consequence in self._consequences:
            lines.append(self._format_consequence(consequence, use_result_tense=False))
            for child in consequence.children:
                lines.append(
                    self._format_consequence(child, use_result_tense=False, indent=4)
                )

        return "\n".join(lines)

    def report_result(self) -> None:
        """Print execution results (past tense, bright colors).

        Output format:
            [SUCCESS] summary (or [DRY RUN] for dry-run mode)
              [VERB-ED] resource message
                [VERB-ED] field message (only changed fields)

        Invariants:
            - UNCHANGED and SKIP fields may be omitted from result (noise reduction)
            - Dry-run appends "- DRY RUN" suffix
            - No output if consequences list is empty
        """
        if not self._consequences:
            return

        # Print header
        if self._dry_run:
            if _colors_enabled():
                print(
                    f"{Color.CYAN.value}[DRY RUN]{Color.RESET.value} Preview of changes:"
                )
            else:
                print("[DRY RUN] Preview of changes:")
        else:
            if _colors_enabled():
                print(
                    f"{Color.GREEN.value}[SUCCESS]{Color.RESET.value} Operation completed:"
                )
            else:
                print("[SUCCESS] Operation completed:")

        # Print consequences
        for consequence in self._consequences:
            print(self._format_consequence(consequence, use_result_tense=True))
            for child in consequence.children:
                # Optionally filter out UNCHANGED/SKIP in results for noise reduction
                # For now, show all for transparency
                print(self._format_consequence(child, use_result_tense=True, indent=4))

    def report_error(self, summary: str, details: Optional[List[str]] = None) -> None:
        """Report execution failure with structured details.

        Prints error message with [ERROR] prefix in bright red color (when colors enabled).
        Details are indented with 2 spaces for visual hierarchy.

        Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

        Args:
            summary: High-level error description
            details: Optional list of detail lines to print below summary

        Output format:
            [ERROR] <summary>
              <detail_line_1>
              <detail_line_2>

        Example:
            >>> reporter = ResultReporter("hatch env create")
            >>> reporter.report_error(
            ...     "Failed to create environment 'dev'",
            ...     details=["Python environment creation failed: conda not available"]
            ... )
            [ERROR] Failed to create environment 'dev'
              Python environment creation failed: conda not available
        """
        if not summary:
            return

        # Print error header with color
        if _colors_enabled():
            print(f"{Color.RED.value}[ERROR]{Color.RESET.value} {summary}")
        else:
            print(f"[ERROR] {summary}")

        # Print details with indentation
        if details:
            for detail in details:
                print(f"  {detail}")

    def report_partial_success(
        self, summary: str, successes: List[str], failures: List[Tuple[str, str]]
    ) -> None:
        """Report mixed success/failure results with ✓/✗ symbols.

        Prints warning message with [WARNING] prefix in bright yellow color.
        Uses ✓/✗ symbols for success/failure items (with ASCII fallback).
        Includes summary line showing success ratio.

        Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

        Args:
            summary: High-level summary description
            successes: List of successful item descriptions
            failures: List of (item, reason) tuples for failed items

        Output format:
            [WARNING] <summary>
              ✓ <success_item>
              ✗ <failure_item>: <reason>
              Summary: X/Y succeeded

        Example:
            >>> reporter = ResultReporter("hatch mcp sync")
            >>> reporter.report_partial_success(
            ...     "Partial synchronization",
            ...     successes=["claude-desktop (backup: ~/.hatch/backups/...)"],
            ...     failures=[("cursor", "Config file not found")]
            ... )
            [WARNING] Partial synchronization
              ✓ claude-desktop (backup: ~/.hatch/backups/...)
              ✗ cursor: Config file not found
              Summary: 1/2 succeeded
        """
        # Determine symbols based on unicode support
        success_symbol = "✓" if _supports_unicode() else "+"
        failure_symbol = "✗" if _supports_unicode() else "x"

        # Print warning header with color
        if _colors_enabled():
            print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} {summary}")
        else:
            print(f"[WARNING] {summary}")

        # Print success items
        for item in successes:
            if _colors_enabled():
                print(
                    f"  {Color.GREEN.value}{success_symbol}{Color.RESET.value} {item}"
                )
            else:
                print(f"  {success_symbol} {item}")

        # Print failure items
        for item, reason in failures:
            if _colors_enabled():
                print(
                    f"  {Color.RED.value}{failure_symbol}{Color.RESET.value} {item}: {reason}"
                )
            else:
                print(f"  {failure_symbol} {item}: {reason}")

        # Print summary line
        total = len(successes) + len(failures)
        succeeded = len(successes)
        print(f"  Summary: {succeeded}/{total} succeeded")
Attributes
command_name property

Display name for the command.

consequences property

List of tracked consequences in order of addition.

dry_run property

Whether this is a dry-run preview.

Functions
__init__(command_name, dry_run=False)

Initialize ResultReporter.

Parameters:

Name Type Description Default
command_name str

Display name for the command

required
dry_run bool

If True, results show "- DRY RUN" suffix

False
Source code in hatch/cli/cli_utils.py
def __init__(self, command_name: str, dry_run: bool = False):
    """Initialize ResultReporter.

    Args:
        command_name: Display name for the command
        dry_run: If True, results show "- DRY RUN" suffix
    """
    self._command_name = command_name
    self._dry_run = dry_run
    self._consequences: List[Consequence] = []
add(consequence_type, message, children=None)

Add a consequence with optional nested children.

Parameters:

Name Type Description Default
consequence_type ConsequenceType

The type of action

required
message str

Human-readable description

required
children Optional[List[Consequence]]

Optional nested consequences (e.g., field-level details)

None
Invariants
  • Order of add() calls determines output order
  • Children inherit parent's tense during rendering
Source code in hatch/cli/cli_utils.py
def add(
    self,
    consequence_type: ConsequenceType,
    message: str,
    children: Optional[List[Consequence]] = None,
) -> None:
    """Add a consequence with optional nested children.

    Args:
        consequence_type: The type of action
        message: Human-readable description
        children: Optional nested consequences (e.g., field-level details)

    Invariants:
        - Order of add() calls determines output order
        - Children inherit parent's tense during rendering
    """
    consequence = Consequence(
        type=consequence_type, message=message, children=children or []
    )
    self._consequences.append(consequence)
add_from_conversion_report(report)

Convert ConversionReport field operations to nested consequences.

Maps ConversionReport data to the unified consequence model: - report.operation → resource ConsequenceType - field_op "UPDATED" → ConsequenceType.UPDATE - field_op "UNSUPPORTED" → ConsequenceType.SKIP - field_op "UNCHANGED" → ConsequenceType.UNCHANGED

Reference: R06 §3.5 (06-dependency_analysis_v0.md) Reference: R04 §1.2 (04-reporting_infrastructure_coexistence_v0.md)

Parameters:

Name Type Description Default
report ConversionReport

ConversionReport with field operations to convert

required
Invariants
  • All field operations become children of resource consequence
  • UNSUPPORTED fields include "(unsupported by host)" suffix
Source code in hatch/cli/cli_utils.py
def add_from_conversion_report(self, report: "ConversionReport") -> None:
    """Convert ConversionReport field operations to nested consequences.

    Maps ConversionReport data to the unified consequence model:
    - report.operation → resource ConsequenceType
    - field_op "UPDATED" → ConsequenceType.UPDATE
    - field_op "UNSUPPORTED" → ConsequenceType.SKIP
    - field_op "UNCHANGED" → ConsequenceType.UNCHANGED

    Reference: R06 §3.5 (06-dependency_analysis_v0.md)
    Reference: R04 §1.2 (04-reporting_infrastructure_coexistence_v0.md)

    Args:
        report: ConversionReport with field operations to convert

    Invariants:
        - All field operations become children of resource consequence
        - UNSUPPORTED fields include "(unsupported by host)" suffix
    """
    # Import here to avoid circular dependency

    # Map report.operation to resource ConsequenceType
    operation_map = {
        "create": ConsequenceType.CONFIGURE,
        "update": ConsequenceType.CONFIGURE,
        "delete": ConsequenceType.REMOVE,
        "migrate": ConsequenceType.CONFIGURE,
    }
    resource_type = operation_map.get(report.operation, ConsequenceType.CONFIGURE)

    # Build resource message
    resource_message = (
        f"Server '{report.server_name}' on '{report.target_host.value}'"
    )

    # Map field operations to child consequences
    field_op_map = {
        "UPDATED": ConsequenceType.UPDATE,
        "UNSUPPORTED": ConsequenceType.SKIP,
        "UNCHANGED": ConsequenceType.UNCHANGED,
    }

    children = []
    for field_op in report.field_operations:
        child_type = field_op_map.get(field_op.operation, ConsequenceType.UPDATE)

        # Format field message based on operation type
        if field_op.operation == "UPDATED":
            child_message = f"{field_op.field_name}: {repr(field_op.old_value)}{repr(field_op.new_value)}"
        elif field_op.operation == "UNSUPPORTED":
            child_message = f"{field_op.field_name}: (unsupported by host)"
        else:  # UNCHANGED
            child_message = f"{field_op.field_name}: {repr(field_op.new_value)}"

        children.append(Consequence(type=child_type, message=child_message))

    # Add the resource consequence with children
    self.add(resource_type, resource_message, children=children)
report_error(summary, details=None)

Report execution failure with structured details.

Prints error message with [ERROR] prefix in bright red color (when colors enabled). Details are indented with 2 spaces for visual hierarchy.

Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

Parameters:

Name Type Description Default
summary str

High-level error description

required
details Optional[List[str]]

Optional list of detail lines to print below summary

None
Output format

[ERROR]

Example

reporter = ResultReporter("hatch env create") reporter.report_error( ... "Failed to create environment 'dev'", ... details=["Python environment creation failed: conda not available"] ... ) [ERROR] Failed to create environment 'dev' Python environment creation failed: conda not available

Source code in hatch/cli/cli_utils.py
def report_error(self, summary: str, details: Optional[List[str]] = None) -> None:
    """Report execution failure with structured details.

    Prints error message with [ERROR] prefix in bright red color (when colors enabled).
    Details are indented with 2 spaces for visual hierarchy.

    Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

    Args:
        summary: High-level error description
        details: Optional list of detail lines to print below summary

    Output format:
        [ERROR] <summary>
          <detail_line_1>
          <detail_line_2>

    Example:
        >>> reporter = ResultReporter("hatch env create")
        >>> reporter.report_error(
        ...     "Failed to create environment 'dev'",
        ...     details=["Python environment creation failed: conda not available"]
        ... )
        [ERROR] Failed to create environment 'dev'
          Python environment creation failed: conda not available
    """
    if not summary:
        return

    # Print error header with color
    if _colors_enabled():
        print(f"{Color.RED.value}[ERROR]{Color.RESET.value} {summary}")
    else:
        print(f"[ERROR] {summary}")

    # Print details with indentation
    if details:
        for detail in details:
            print(f"  {detail}")
report_partial_success(summary, successes, failures)

Report mixed success/failure results with ✓/✗ symbols.

Prints warning message with [WARNING] prefix in bright yellow color. Uses ✓/✗ symbols for success/failure items (with ASCII fallback). Includes summary line showing success ratio.

Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

Parameters:

Name Type Description Default
summary str

High-level summary description

required
successes List[str]

List of successful item descriptions

required
failures List[Tuple[str, str]]

List of (item, reason) tuples for failed items

required
Output format

[WARNING]

: Summary: X/Y succeeded

Example

reporter = ResultReporter("hatch mcp sync") reporter.report_partial_success( ... "Partial synchronization", ... successes=["claude-desktop (backup: ~/.hatch/backups/...)"], ... failures=[("cursor", "Config file not found")] ... ) [WARNING] Partial synchronization ✓ claude-desktop (backup: ~/.hatch/backups/...) ✗ cursor: Config file not found Summary: 1/2 succeeded

Source code in hatch/cli/cli_utils.py
def report_partial_success(
    self, summary: str, successes: List[str], failures: List[Tuple[str, str]]
) -> None:
    """Report mixed success/failure results with ✓/✗ symbols.

    Prints warning message with [WARNING] prefix in bright yellow color.
    Uses ✓/✗ symbols for success/failure items (with ASCII fallback).
    Includes summary line showing success ratio.

    Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)

    Args:
        summary: High-level summary description
        successes: List of successful item descriptions
        failures: List of (item, reason) tuples for failed items

    Output format:
        [WARNING] <summary>
          ✓ <success_item>
          ✗ <failure_item>: <reason>
          Summary: X/Y succeeded

    Example:
        >>> reporter = ResultReporter("hatch mcp sync")
        >>> reporter.report_partial_success(
        ...     "Partial synchronization",
        ...     successes=["claude-desktop (backup: ~/.hatch/backups/...)"],
        ...     failures=[("cursor", "Config file not found")]
        ... )
        [WARNING] Partial synchronization
          ✓ claude-desktop (backup: ~/.hatch/backups/...)
          ✗ cursor: Config file not found
          Summary: 1/2 succeeded
    """
    # Determine symbols based on unicode support
    success_symbol = "✓" if _supports_unicode() else "+"
    failure_symbol = "✗" if _supports_unicode() else "x"

    # Print warning header with color
    if _colors_enabled():
        print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} {summary}")
    else:
        print(f"[WARNING] {summary}")

    # Print success items
    for item in successes:
        if _colors_enabled():
            print(
                f"  {Color.GREEN.value}{success_symbol}{Color.RESET.value} {item}"
            )
        else:
            print(f"  {success_symbol} {item}")

    # Print failure items
    for item, reason in failures:
        if _colors_enabled():
            print(
                f"  {Color.RED.value}{failure_symbol}{Color.RESET.value} {item}: {reason}"
            )
        else:
            print(f"  {failure_symbol} {item}: {reason}")

    # Print summary line
    total = len(successes) + len(failures)
    succeeded = len(successes)
    print(f"  Summary: {succeeded}/{total} succeeded")
report_prompt()

Generate confirmation prompt (present tense, dim colors).

Output format

{command_name}: [VERB] resource message [VERB] field message [VERB] field message

Returns:

Type Description
str

Formatted prompt string, empty string if no consequences.

Invariants
  • All consequences shown (including UNCHANGED, SKIP)
  • Empty string if no consequences
Source code in hatch/cli/cli_utils.py
def report_prompt(self) -> str:
    """Generate confirmation prompt (present tense, dim colors).

    Output format:
        {command_name}:
          [VERB] resource message
            [VERB] field message
            [VERB] field message

    Returns:
        Formatted prompt string, empty string if no consequences.

    Invariants:
        - All consequences shown (including UNCHANGED, SKIP)
        - Empty string if no consequences
    """
    if not self._consequences:
        return ""

    lines = [f"{self._command_name}:"]

    for consequence in self._consequences:
        lines.append(self._format_consequence(consequence, use_result_tense=False))
        for child in consequence.children:
            lines.append(
                self._format_consequence(child, use_result_tense=False, indent=4)
            )

    return "\n".join(lines)
report_result()

Print execution results (past tense, bright colors).

Output format

[SUCCESS] summary (or [DRY RUN] for dry-run mode) [VERB-ED] resource message [VERB-ED] field message (only changed fields)

Invariants
  • UNCHANGED and SKIP fields may be omitted from result (noise reduction)
  • Dry-run appends "- DRY RUN" suffix
  • No output if consequences list is empty
Source code in hatch/cli/cli_utils.py
def report_result(self) -> None:
    """Print execution results (past tense, bright colors).

    Output format:
        [SUCCESS] summary (or [DRY RUN] for dry-run mode)
          [VERB-ED] resource message
            [VERB-ED] field message (only changed fields)

    Invariants:
        - UNCHANGED and SKIP fields may be omitted from result (noise reduction)
        - Dry-run appends "- DRY RUN" suffix
        - No output if consequences list is empty
    """
    if not self._consequences:
        return

    # Print header
    if self._dry_run:
        if _colors_enabled():
            print(
                f"{Color.CYAN.value}[DRY RUN]{Color.RESET.value} Preview of changes:"
            )
        else:
            print("[DRY RUN] Preview of changes:")
    else:
        if _colors_enabled():
            print(
                f"{Color.GREEN.value}[SUCCESS]{Color.RESET.value} Operation completed:"
            )
        else:
            print("[SUCCESS] Operation completed:")

    # Print consequences
    for consequence in self._consequences:
        print(self._format_consequence(consequence, use_result_tense=True))
        for child in consequence.children:
            # Optionally filter out UNCHANGED/SKIP in results for noise reduction
            # For now, show all for transparency
            print(self._format_consequence(child, use_result_tense=True, indent=4))

TableFormatter

Aligned table output for list commands.

Renders data as aligned columns with headers and separator line. Supports fixed and auto-calculated column widths.

Reference: R06 §3.6 (06-dependency_analysis_v0.md) Reference: R02 §5 (02-list_output_format_specification_v2.md)

Attributes:

Name Type Description
columns

List of column definitions

Example

columns = [ ... ColumnDef(name="Name", width=20), ... ColumnDef(name="Status", width=10), ... ] formatter = TableFormatter(columns) formatter.add_row(["my-server", "active"]) print(formatter.render()) Name Status ───────────────────────────────── my-server active

Source code in hatch/cli/cli_utils.py
class TableFormatter:
    """Aligned table output for list commands.

    Renders data as aligned columns with headers and separator line.
    Supports fixed and auto-calculated column widths.

    Reference: R06 §3.6 (06-dependency_analysis_v0.md)
    Reference: R02 §5 (02-list_output_format_specification_v2.md)

    Attributes:
        columns: List of column definitions

    Example:
        >>> columns = [
        ...     ColumnDef(name="Name", width=20),
        ...     ColumnDef(name="Status", width=10),
        ... ]
        >>> formatter = TableFormatter(columns)
        >>> formatter.add_row(["my-server", "active"])
        >>> print(formatter.render())
        Name                 Status
        ─────────────────────────────────
        my-server            active
    """

    def __init__(self, columns: List[ColumnDef]):
        """Initialize TableFormatter with column definitions.

        Args:
            columns: List of ColumnDef specifying table structure
        """
        self._columns = columns
        self._rows: List[List[str]] = []

    def add_row(self, values: List[str]) -> None:
        """Add a data row to the table.

        Args:
            values: List of string values, one per column
        """
        self._rows.append(values)

    def _calculate_widths(self) -> List[int]:
        """Calculate actual column widths, resolving 'auto' widths.

        Returns:
            List of integer widths for each column
        """
        widths = []
        for i, col in enumerate(self._columns):
            if col.width == "auto":
                # Calculate from header and all row values
                max_width = len(col.name)
                for row in self._rows:
                    if i < len(row):
                        max_width = max(max_width, len(row[i]))
                widths.append(max_width)
            else:
                widths.append(col.width)
        return widths

    def _align_value(self, value: str, width: int, align: str) -> str:
        """Align a value within the specified width.

        Args:
            value: The string value to align
            width: Target width
            align: Alignment type ("left", "right", "center")

        Returns:
            Aligned string, truncated with ellipsis if too long
        """
        # Truncate if too long
        if len(value) > width:
            if width > 1:
                return value[: width - 1] + "…"
            return value[:width]

        # Apply alignment
        if align == "right":
            return value.rjust(width)
        elif align == "center":
            return value.center(width)
        else:  # left (default)
            return value.ljust(width)

    def render(self) -> str:
        """Render the table as a formatted string.

        Returns:
            Multi-line string with headers, separator, and data rows
        """
        widths = self._calculate_widths()
        lines = []

        # Header row
        header_parts = []
        for i, col in enumerate(self._columns):
            header_parts.append(self._align_value(col.name, widths[i], col.align))
        lines.append("  " + "  ".join(header_parts))

        # Separator line
        total_width = (
            sum(widths) + (len(widths) - 1) * 2 + 2
        )  # columns + separators + indent
        lines.append("  " + "─" * (total_width - 2))

        # Data rows
        for row in self._rows:
            row_parts = []
            for i, col in enumerate(self._columns):
                value = row[i] if i < len(row) else ""
                row_parts.append(self._align_value(value, widths[i], col.align))
            lines.append("  " + "  ".join(row_parts))

        return "\n".join(lines)
Functions
__init__(columns)

Initialize TableFormatter with column definitions.

Parameters:

Name Type Description Default
columns List[ColumnDef]

List of ColumnDef specifying table structure

required
Source code in hatch/cli/cli_utils.py
def __init__(self, columns: List[ColumnDef]):
    """Initialize TableFormatter with column definitions.

    Args:
        columns: List of ColumnDef specifying table structure
    """
    self._columns = columns
    self._rows: List[List[str]] = []
add_row(values)

Add a data row to the table.

Parameters:

Name Type Description Default
values List[str]

List of string values, one per column

required
Source code in hatch/cli/cli_utils.py
def add_row(self, values: List[str]) -> None:
    """Add a data row to the table.

    Args:
        values: List of string values, one per column
    """
    self._rows.append(values)
render()

Render the table as a formatted string.

Returns:

Type Description
str

Multi-line string with headers, separator, and data rows

Source code in hatch/cli/cli_utils.py
def render(self) -> str:
    """Render the table as a formatted string.

    Returns:
        Multi-line string with headers, separator, and data rows
    """
    widths = self._calculate_widths()
    lines = []

    # Header row
    header_parts = []
    for i, col in enumerate(self._columns):
        header_parts.append(self._align_value(col.name, widths[i], col.align))
    lines.append("  " + "  ".join(header_parts))

    # Separator line
    total_width = (
        sum(widths) + (len(widths) - 1) * 2 + 2
    )  # columns + separators + indent
    lines.append("  " + "─" * (total_width - 2))

    # Data rows
    for row in self._rows:
        row_parts = []
        for i, col in enumerate(self._columns):
            value = row[i] if i < len(row) else ""
            row_parts.append(self._align_value(value, widths[i], col.align))
        lines.append("  " + "  ".join(row_parts))

    return "\n".join(lines)

ValidationError

Bases: Exception

Validation error with structured context.

Provides structured error information for input validation failures, including optional field name and suggestion for resolution.

Reference: R13 §4.2.2 (13-error_message_formatting_v0.md)

Attributes:

Name Type Description
message

Human-readable error description

field

Optional field/argument name that caused the error

suggestion

Optional suggestion for resolving the error

Example

raise ValidationError( ... "Invalid host 'vsc'", ... field="--host", ... suggestion="Supported hosts: claude-desktop, vscode, cursor" ... )

Source code in hatch/cli/cli_utils.py
class ValidationError(Exception):
    """Validation error with structured context.

    Provides structured error information for input validation failures,
    including optional field name and suggestion for resolution.

    Reference: R13 §4.2.2 (13-error_message_formatting_v0.md)

    Attributes:
        message: Human-readable error description
        field: Optional field/argument name that caused the error
        suggestion: Optional suggestion for resolving the error

    Example:
        >>> raise ValidationError(
        ...     "Invalid host 'vsc'",
        ...     field="--host",
        ...     suggestion="Supported hosts: claude-desktop, vscode, cursor"
        ... )
    """

    def __init__(self, message: str, field: str = None, suggestion: str = None):
        """Initialize ValidationError.

        Args:
            message: Human-readable error description
            field: Optional field/argument name that caused the error
            suggestion: Optional suggestion for resolving the error
        """
        self.message = message
        self.field = field
        self.suggestion = suggestion
        super().__init__(message)
Functions
__init__(message, field=None, suggestion=None)

Initialize ValidationError.

Parameters:

Name Type Description Default
message str

Human-readable error description

required
field str

Optional field/argument name that caused the error

None
suggestion str

Optional suggestion for resolving the error

None
Source code in hatch/cli/cli_utils.py
def __init__(self, message: str, field: str = None, suggestion: str = None):
    """Initialize ValidationError.

    Args:
        message: Human-readable error description
        field: Optional field/argument name that caused the error
        suggestion: Optional suggestion for resolving the error
    """
    self.message = message
    self.field = field
    self.suggestion = suggestion
    super().__init__(message)

Functions

format_info(message)

Print formatted info message with color.

Prints message with [INFO] prefix in bright blue color. Used for informational messages like "Operation cancelled".

Reference: R13-B §B.6.2 (13-error_message_formatting_appendix_b_v0.md)

Parameters:

Name Type Description Default
message str

Info message to display

required
Output format

[INFO]

Example

from hatch.cli.cli_utils import format_info format_info("Operation cancelled") [INFO] Operation cancelled

Source code in hatch/cli/cli_utils.py
def format_info(message: str) -> None:
    """Print formatted info message with color.

    Prints message with [INFO] prefix in bright blue color.
    Used for informational messages like "Operation cancelled".

    Reference: R13-B §B.6.2 (13-error_message_formatting_appendix_b_v0.md)

    Args:
        message: Info message to display

    Output format:
        [INFO] <message>

    Example:
        >>> from hatch.cli.cli_utils import format_info
        >>> format_info("Operation cancelled")
        [INFO] Operation cancelled
    """
    if _colors_enabled():
        print(f"{Color.BLUE.value}[INFO]{Color.RESET.value} {message}")
    else:
        print(f"[INFO] {message}")

format_validation_error(error)

Print formatted validation error with color.

Prints error message with [ERROR] prefix in bright red color. Optionally includes field name and suggestion if provided.

Reference: R13 §4.3 (13-error_message_formatting_v0.md)

Parameters:

Name Type Description Default
error ValidationError

ValidationError instance with message, field, and suggestion

required
Output format

[ERROR] Field: (if provided) Suggestion: (if provided)

Example

from hatch.cli.cli_utils import ValidationError, format_validation_error format_validation_error(ValidationError( ... "Invalid host 'vsc'", ... field="--host", ... suggestion="Supported hosts: claude-desktop, vscode, cursor" ... )) [ERROR] Invalid host 'vsc' Field: --host Suggestion: Supported hosts: claude-desktop, vscode, cursor

Source code in hatch/cli/cli_utils.py
def format_validation_error(error: "ValidationError") -> None:
    """Print formatted validation error with color.

    Prints error message with [ERROR] prefix in bright red color.
    Optionally includes field name and suggestion if provided.

    Reference: R13 §4.3 (13-error_message_formatting_v0.md)

    Args:
        error: ValidationError instance with message, field, and suggestion

    Output format:
        [ERROR] <message>
          Field: <field> (if provided)
          Suggestion: <suggestion> (if provided)

    Example:
        >>> from hatch.cli.cli_utils import ValidationError, format_validation_error
        >>> format_validation_error(ValidationError(
        ...     "Invalid host 'vsc'",
        ...     field="--host",
        ...     suggestion="Supported hosts: claude-desktop, vscode, cursor"
        ... ))
        [ERROR] Invalid host 'vsc'
          Field: --host
          Suggestion: Supported hosts: claude-desktop, vscode, cursor
    """
    # Print error header with color
    if _colors_enabled():
        print(f"{Color.RED.value}[ERROR]{Color.RESET.value} {error.message}")
    else:
        print(f"[ERROR] {error.message}")

    # Print field if provided
    if error.field:
        print(f"  Field: {error.field}")

    # Print suggestion if provided
    if error.suggestion:
        print(f"  Suggestion: {error.suggestion}")

format_warning(message, suggestion=None)

Print formatted warning message with color.

Prints message with [WARNING] prefix in bright yellow color. Used for non-fatal warnings that don't prevent operation completion.

Reference: R13-A §A.5 P3 (13-error_message_formatting_appendix_a_v0.md)

Parameters:

Name Type Description Default
message str

Warning message to display

required
suggestion str

Optional suggestion for resolution

None
Output format

[WARNING] Suggestion: (if provided)

Example

from hatch.cli.cli_utils import format_warning format_warning("Invalid header format 'foo'", suggestion="Expected KEY=VALUE") [WARNING] Invalid header format 'foo' Suggestion: Expected KEY=VALUE

Source code in hatch/cli/cli_utils.py
def format_warning(message: str, suggestion: str = None) -> None:
    """Print formatted warning message with color.

    Prints message with [WARNING] prefix in bright yellow color.
    Used for non-fatal warnings that don't prevent operation completion.

    Reference: R13-A §A.5 P3 (13-error_message_formatting_appendix_a_v0.md)

    Args:
        message: Warning message to display
        suggestion: Optional suggestion for resolution

    Output format:
        [WARNING] <message>
          Suggestion: <suggestion> (if provided)

    Example:
        >>> from hatch.cli.cli_utils import format_warning
        >>> format_warning("Invalid header format 'foo'", suggestion="Expected KEY=VALUE")
        [WARNING] Invalid header format 'foo'
          Suggestion: Expected KEY=VALUE
    """
    if _colors_enabled():
        print(f"{Color.YELLOW.value}[WARNING]{Color.RESET.value} {message}")
    else:
        print(f"[WARNING] {message}")

    if suggestion:
        print(f"  Suggestion: {suggestion}")

get_hatch_version()

Get Hatch version from package metadata.

Returns:

Name Type Description
str str

Version string from package metadata, or 'unknown (development mode)' if package is not installed.

Source code in hatch/cli/cli_utils.py
def get_hatch_version() -> str:
    """Get Hatch version from package metadata.

    Returns:
        str: Version string from package metadata, or 'unknown (development mode)'
             if package is not installed.
    """
    try:
        return version("hatch-xclam")
    except PackageNotFoundError:
        return "unknown (development mode)"

get_package_mcp_server_config(env_manager, env_name, package_name)

Get MCP server configuration for a package using existing APIs.

Parameters:

Name Type Description Default
env_manager HatchEnvironmentManager

The environment manager instance

required
env_name str

Name of the environment containing the package

required
package_name str

Name of the package to get config for

required

Returns:

Name Type Description
MCPServerConfig MCPServerConfig

Server configuration for the package

Raises:

Type Description
ValueError

If package not found, not a Hatch package, or has no MCP entry point

Source code in hatch/cli/cli_utils.py
def get_package_mcp_server_config(
    env_manager: HatchEnvironmentManager, env_name: str, package_name: str
) -> MCPServerConfig:
    """Get MCP server configuration for a package using existing APIs.

    Args:
        env_manager: The environment manager instance
        env_name: Name of the environment containing the package
        package_name: Name of the package to get config for

    Returns:
        MCPServerConfig: Server configuration for the package

    Raises:
        ValueError: If package not found, not a Hatch package, or has no MCP entry point
    """
    try:
        # Get package info from environment
        packages = env_manager.list_packages(env_name)
        package_info = next(
            (pkg for pkg in packages if pkg["name"] == package_name), None
        )

        if not package_info:
            raise ValueError(
                f"Package '{package_name}' not found in environment '{env_name}'"
            )

        # Load package metadata using existing pattern from environment_manager.py:716-727
        package_path = Path(package_info["source"]["path"])
        metadata_path = package_path / "hatch_metadata.json"

        if not metadata_path.exists():
            raise ValueError(
                f"Package '{package_name}' is not a Hatch package (no hatch_metadata.json)"
            )

        with open(metadata_path, "r") as f:
            metadata = json.load(f)

        # Use PackageService for schema-aware access
        from hatch_validator.package.package_service import PackageService

        package_service = PackageService(metadata)

        # Get the HatchMCP entry point (this handles both v1.2.0 and v1.2.1 schemas)
        mcp_entry_point = package_service.get_mcp_entry_point()
        if not mcp_entry_point:
            raise ValueError(
                f"Package '{package_name}' does not have a HatchMCP entry point"
            )

        # Get environment-specific Python executable
        python_executable = env_manager.get_current_python_executable()
        if not python_executable:
            # Fallback to system Python if no environment-specific Python available
            python_executable = "python"

        # Create server configuration
        server_path = str(package_path / mcp_entry_point)
        server_config = MCPServerConfig(
            name=package_name, command=python_executable, args=[server_path], env={}
        )

        return server_config

    except Exception as e:
        raise ValueError(
            f"Failed to get MCP server config for package '{package_name}': {e}"
        )

highlight(text)

Apply highlight formatting (bold + amber) to entity names.

Used in show commands to emphasize host and server names for quick visual scanning of detailed output.

Reference: R12 §3.3 (12-enhancing_colors_v0.md) Reference: R11 §3.2 (11-enhancing_show_command_v0.md)

Parameters:

Name Type Description Default
text str

The entity name to highlight

required

Returns:

Name Type Description
str str

Text with bold + amber formatting if colors enabled, otherwise plain text.

Example

print(f"MCP Host: {highlight('claude-desktop')}") MCP Host: claude-desktop # (bold + amber in TTY)

Source code in hatch/cli/cli_utils.py
def highlight(text: str) -> str:
    """Apply highlight formatting (bold + amber) to entity names.

    Used in show commands to emphasize host and server names for
    quick visual scanning of detailed output.

    Reference: R12 §3.3 (12-enhancing_colors_v0.md)
    Reference: R11 §3.2 (11-enhancing_show_command_v0.md)

    Args:
        text: The entity name to highlight

    Returns:
        str: Text with bold + amber formatting if colors enabled,
             otherwise plain text.

    Example:
        >>> print(f"MCP Host: {highlight('claude-desktop')}")
        MCP Host: claude-desktop  # (bold + amber in TTY)
    """
    if _colors_enabled():
        # Bold (\033[1m) + Amber color
        return f"\033[1m{Color.AMBER.value}{text}{Color.RESET.value}"
    return text

parse_env_vars(env_list)

Parse environment variables from command line format.

Parameters:

Name Type Description Default
env_list Optional[list]

List of strings in KEY=VALUE format

required

Returns:

Name Type Description
dict dict

Dictionary of environment variable key-value pairs

Source code in hatch/cli/cli_utils.py
def parse_env_vars(env_list: Optional[list]) -> dict:
    """Parse environment variables from command line format.

    Args:
        env_list: List of strings in KEY=VALUE format

    Returns:
        dict: Dictionary of environment variable key-value pairs
    """
    if not env_list:
        return {}

    env_dict = {}
    for env_var in env_list:
        if "=" not in env_var:
            format_warning(
                f"Invalid environment variable format '{env_var}'",
                suggestion="Expected KEY=VALUE",
            )
            continue
        key, value = env_var.split("=", 1)
        env_dict[key.strip()] = value.strip()

    return env_dict

parse_header(header_list)

Parse HTTP headers from command line format.

Parameters:

Name Type Description Default
header_list Optional[list]

List of strings in KEY=VALUE format

required

Returns:

Name Type Description
dict dict

Dictionary of header key-value pairs

Source code in hatch/cli/cli_utils.py
def parse_header(header_list: Optional[list]) -> dict:
    """Parse HTTP headers from command line format.

    Args:
        header_list: List of strings in KEY=VALUE format

    Returns:
        dict: Dictionary of header key-value pairs
    """
    if not header_list:
        return {}

    headers_dict = {}
    for header in header_list:
        if "=" not in header:
            format_warning(
                f"Invalid header format '{header}'", suggestion="Expected KEY=VALUE"
            )
            continue
        key, value = header.split("=", 1)
        headers_dict[key.strip()] = value.strip()

    return headers_dict

parse_host_list(host_arg)

Parse comma-separated host list or 'all'.

Parameters:

Name Type Description Default
host_arg str

Comma-separated host names or 'all' for all available hosts

required

Returns:

Type Description
List[str]

List[str]: List of host name strings

Raises:

Type Description
ValueError

If an unknown host name is provided

Source code in hatch/cli/cli_utils.py
def parse_host_list(host_arg: str) -> List[str]:
    """Parse comma-separated host list or 'all'.

    Args:
        host_arg: Comma-separated host names or 'all' for all available hosts

    Returns:
        List[str]: List of host name strings

    Raises:
        ValueError: If an unknown host name is provided
    """
    if not host_arg:
        return []

    if host_arg.lower() == "all":
        available_hosts = MCPHostRegistry.detect_available_hosts()
        return [host.value for host in available_hosts]

    hosts = []
    for host_str in host_arg.split(","):
        host_str = host_str.strip()
        try:
            host_type = MCPHostType(host_str)
            hosts.append(host_type.value)
        except ValueError:
            available = [h.value for h in MCPHostType]
            raise ValueError(f"Unknown host '{host_str}'. Available: {available}")

    return hosts

parse_input(input_list)

Parse VS Code input variable definitions from command line format.

Format: type,id,description[,password=true] Example: promptString,api-key,GitHub Personal Access Token,password=true

Parameters:

Name Type Description Default
input_list Optional[list]

List of input definition strings

required

Returns:

Type Description
Optional[list]

List of input variable definition dictionaries, or None if no inputs provided.

Source code in hatch/cli/cli_utils.py
def parse_input(input_list: Optional[list]) -> Optional[list]:
    """Parse VS Code input variable definitions from command line format.

    Format: type,id,description[,password=true]
    Example: promptString,api-key,GitHub Personal Access Token,password=true

    Args:
        input_list: List of input definition strings

    Returns:
        List of input variable definition dictionaries, or None if no inputs provided.
    """
    if not input_list:
        return None

    parsed_inputs = []
    for input_str in input_list:
        parts = [p.strip() for p in input_str.split(",")]
        if len(parts) < 3:
            format_warning(
                f"Invalid input format '{input_str}'",
                suggestion="Expected: type,id,description[,password=true]",
            )
            continue

        input_def = {"type": parts[0], "id": parts[1], "description": parts[2]}

        # Check for optional password flag
        if len(parts) > 3 and parts[3].lower() == "password=true":
            input_def["password"] = True

        parsed_inputs.append(input_def)

    return parsed_inputs if parsed_inputs else None

request_confirmation(message, auto_approve=False)

Request user confirmation with non-TTY support following Hatch patterns.

Parameters:

Name Type Description Default
message str

The confirmation message to display

required
auto_approve bool

If True, automatically approve without prompting

False

Returns:

Name Type Description
bool bool

True if confirmed, False otherwise

Source code in hatch/cli/cli_utils.py
def request_confirmation(message: str, auto_approve: bool = False) -> bool:
    """Request user confirmation with non-TTY support following Hatch patterns.

    Args:
        message: The confirmation message to display
        auto_approve: If True, automatically approve without prompting

    Returns:
        bool: True if confirmed, False otherwise
    """
    # Check for auto-approve first
    if auto_approve or os.getenv("HATCH_AUTO_APPROVE", "").lower() in (
        "1",
        "true",
        "yes",
    ):
        return True

    # Interactive mode - request user input (works in both TTY and test environments)
    try:
        while True:
            response = input(f"{message} [y/N]: ").strip().lower()
            if response in ["y", "yes"]:
                return True
            elif response in ["n", "no", ""]:
                return False
            else:
                print("Please enter 'y' for yes or 'n' for no.")
    except (EOFError, KeyboardInterrupt):
        # Only auto-approve on EOF/interrupt if not in TTY (non-interactive environment)
        if not sys.stdin.isatty():
            return True
        return False