Files
Christian LempaandGitHub f856aef030 Release v0.1.0 (#1562)
* updated version number

* chore(config): apply ruff formatting to release/v0.1.0

- Run ruff check --fix to remove unused imports and fix code issues
- Run ruff format to apply PEP 8 formatting (4 spaces for Python)
- Fix TemplateRenderError import and remove unused defaults variable

* fix(core): required sections ignore toggle and always enabled

- Modified VariableSection.is_enabled() to return True for required sections
- Hide toggle variables from display in required sections
- Add warnings when attempting to disable required section toggles via config or CLI
- Updated pihole template with better defaults and required network section

Fixes #1411

* feat(core): comprehensive improvements to variable dependencies and prompts

Major enhancements:
- Sort variables by dependencies within sections for logical display/prompt order
- Skip prompting for variables with unsatisfied needs
- Hide toggle variables in required sections from display
- Use standard prompt logic for toggle variables (supports extra text)
- Add warnings when setting values for variables with unsatisfied needs via config/CLI
- Fix section merge to preserve needs from module spec when template doesn't override
- Support semicolon-separated multiple AND conditions in needs syntax
  Example: needs: 'traefik_enabled=true;network_mode=bridge,macvlan'

Fixes and improvements for issues with:
- Required sections incorrectly showing as disabled
- Variables displayed/prompted in illogical order
- Macvlan variables prompted when network_mode=bridge
- Toggle descriptions not showing extra text
- Section needs being cleared during template merge

Related to #1411

* fix(pihole): handle host network mode correctly

Remove dependency on network_enabled toggle since Network section is now required.
Template now directly checks network_mode value for host/bridge/macvlan logic.

* feat(core): remove Jinja2 default() filter extraction (#1410) (#1416)

- Removed _extract_jinja_default_values() method from Template class
- Removed merge logic for Jinja2 defaults in variables property
- Fixed validate command to handle 3-tuple from LibraryManager.find()
- All defaults must now be explicitly defined in template/module specs
- Updated CHANGELOG.md with removal notice

* feat(core): add --var-file support for loading variables from YAML (#1331)

- Added _load_var_file() method to parse YAML variable files
- Added _apply_var_file() method to apply var file variables with proper precedence
- Added --var-file/-f parameter to generate command
- Supports both flat (var: value) and nested (section: {var: value}) YAML structures
- Proper precedence chain: module < template < config < var-file < CLI --var
- Comprehensive error handling for file not found, invalid YAML, and type errors
- Updated documentation and examples
- All existing templates validate successfully

* update changelog

* docs(quality): add comprehensive code quality analysis

* refactor(collection): add iter_active_sections() helper method

- Adds centralized iterator for sections with proper filtering
- Eliminates duplicate iteration logic in prompt.py
- Supports include_disabled and include_unsatisfied flags
- Reduces code duplication by ~30 lines

Related to #1364 (High Priority #1)

* refactor(module): deduplicate template loading logic

- Adds _load_all_templates() helper method with optional filtering
- Updates list(), search(), and validate() to use centralized helper
- Eliminates ~90 lines of duplicate code
- Improves maintainability with single source of truth for template loading

Related to #1364 (High Priority #3)

* docs: add comprehensive naming and API improvement analysis

- Identifies 6 major improvement opportunities
- Prioritizes by impact (High/Medium/Low)
- Proposes CRUD standardization across ConfigManager
- Recommends consolidating duplicate methods
- Includes breaking change mitigation strategies
- Estimates +1.0 code quality score improvement

Related to #1364

* docs: remove analysis documents (not needed for PR)

* refactor(display): split DisplayManager into specialized managers

- Refactored monolithic DisplayManager (971 lines) into 4 specialized managers:
  * VariableDisplayManager - variable and section rendering
  * TemplateDisplayManager - template display and file trees
  * StatusDisplayManager - status messages and errors
  * TableDisplayManager - all table types

- Maintained 100% backward compatibility via delegation methods
- All existing code works without modifications
- Follows Single Responsibility Principle
- Updated AGENTS.md with new architecture documentation

Previous improvements included in this commit:
- Renamed display methods for consistency (display_template, display_section)
- Reduced variable map lookups in reset_disabled_bool_variables()
- Improved exception hierarchy (VariableValidationError, VariableError)
- Extracted error context building to TemplateErrorHandler class
- Fixed VariableSection forward reference in variable.py

All ruff checks pass. Tested with compose list and compose show commands.

Relates to #1364

* refactor(display): complete optimization with settings, helpers, and method splitting

Major improvements to display.py architecture:

1. DisplaySettings Class (65 lines):
   - Centralized all hardcoded values (colors, styles, layouts, text labels)
   - Easy customization via single class
   - Constants: colors, styles, padding, sizes, labels, etc.

2. Helper Methods in DisplayManager:
   - _format_library_display() - eliminates duplicate library formatting
   - _truncate_value() - centralized value truncation logic
   - _format_file_size() - human-readable size formatting (B, KB, MB)

3. Split render_variables_table() (91 → 56 lines):
   - Extracted _render_section_header() (20 lines)
   - Extracted _render_variable_row() (35 lines)
   - Main method now cleaner coordinator logic

4. Updated All Managers to Use Settings:
   - VariableDisplayManager: uses all style/color/text constants
   - TemplateDisplayManager: uses settings and _format_library_display()
   - StatusDisplayManager: uses color scheme constants
   - TableDisplayManager: uses helpers and settings throughout

5. Removed Code Duplication:
   - Library display logic (was in 2 places)
   - File size formatting (was in 1 place)
   - Value truncation (was in 2 places with different logic)
   - Sensitive masking (consolidated)

Benefits:
- Single source of truth for all display configuration
- Easy to theme/customize CLI appearance
- Better testability (helpers can be unit tested)
- Reduced duplication
- More maintainable (change color scheme in one place)

File stats: 1343 → 1337 lines (-6 lines despite adding 65-line settings class)
All tests pass: compose list, compose show traefik
Linting: ruff checks passed

Relates to #1364

* refactor(display): split display.py into separate manager modules

- Created cli/core/display/ package structure
- Split DisplayManager into specialized managers:
  - VariableDisplayManager: variable rendering
  - TemplateDisplayManager: template display
  - StatusDisplayManager: status messages and errors
  - TableDisplayManager: table rendering
- Moved DisplaySettings and IconManager to __init__.py
- Maintained backward compatibility through delegation methods
- All imports remain unchanged (from cli.core.display import DisplayManager)
- Follows Single Responsibility Principle for better maintainability

* docs(changelog): add display module refactoring entry

* refactor(display): separate DisplaySettings, IconManager, and DisplayManager into individual files

- Moved DisplaySettings to display_settings.py
- Moved IconManager to icon_manager.py
- Moved DisplayManager to display_manager.py
- Updated __init__.py to only handle imports/exports (27 lines vs 526)
- Each file now has single, clear responsibility
- Better adherence to Single Responsibility Principle
- Updated AGENTS.md with new structure and ruff formatting instructions

* style: apply ruff formatting to entire codebase

* fix(display): simplify section disabled label logic and fix table row styling

- Fixed table row styling being added as 5th column instead of style parameter
- Simplified disabled label logic: show (disabled) if section has toggle and is not enabled
- Removed redundant has_dependencies parameter from _render_section_header
- Now all disabled toggle sections consistently show (disabled) label

* refactor(display): standardize table header styling across CLI

- Enforce consistent STYLE_TABLE_HEADER ('bold blue') for all tables via _print_table()
- Remove optional style parameter logic from table header styling
- Remove separate heading() calls before tables for cleaner output
- Ensure uniform table appearance throughout compose, repo, and config commands

* updated changelog

* updated changelog and description for code quality

* feature(ci): Add Ruff linting configuration and GitHub Actions workflow

- Add Ruff configuration to pyproject.toml with PEP 8 compliance
  - Line length: 88 characters
  - Indentation: 4 spaces (PEP 8 standard)
  - Enable comprehensive rule sets (pycodestyle, pyflakes, isort, pylint, etc.)
- Create .github/workflows/codequality-ruff.yaml
  - Runs on PRs to main and pushes to main/release/* branches
  - Checks both linting and formatting (blocking)
- Fix yamllint errors in config.yaml and release workflow
- Remove whitespace from table_display.py

Related to #1318

* fixed some ruff errors

* feat(compose): add --var and --var-file support to show command (#1421)

- Add --var and --var-file options to show command
- Apply same variable precedence as generate command
- Update CHANGELOG.md with feature description
- Users can now preview variable overrides before generating files

* started developing new functions

* critical updates to templates

* updates to the template tags

* n8n template preparation

* traefik security headers

* version pinning for twingate-connector

* template fix

* recent template updates

* traefik template publish

* n8n tags

* prometheus update

* fix(compose): use CF_API_TOKEN_FILE for Cloudflare API token in Traefik

* fix bug in schema 1.1

* renovate draft template

* make updates to renovate

* code quality updates

* feat: Add schema 1.2 with dedicated volume and resources sections

- Add spec_v1_2.py with new volume and resources sections
- Volume section: Replaces swarm_volume_* vars, works universally
- Resources section: CPU/memory limits for production deployments
- Ports section: Add ports_http and ports_https variables
- Update compose module to support schema 1.2
- Create new v2 archetypes:
  - service-volumes-v2.j2: Uses volume_mode
  - volumes-v2.j2: Top-level volumes with new section
  - service-resources-v1.j2: Resource limits

Related: #1519

* fixed #1522 and archetype improvements

* fixed issues in archetypes

* prepare other templates

* prepare other templates

* fix(variable): correct email validation regex

Fixed malformed email validation regex that was matching literal
backslash-s characters instead of whitespace. Changed from
r"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$" to
r"^[^@\s]+@[^@\s]+\.[^@\s]+$"

Fixes #1481

* fix(variable): replace regex with RFC-compliant email validation

Replaced regex-based email validation with email-validator library.
Regex cannot properly validate emails per RFC 5322/5321 - it fails
on valid addresses like "John Doe"@example.com, user+tag@example.com.

Changes:
- Added email-validator>=2.0.0 dependency to pyproject.toml
- Removed EMAIL_REGEX constant
- Updated _convert_email() to use validate_email() function
- Returns normalized email addresses
- Provides better error messages for invalid emails

Fixes #1481

* prepare migration for other modules

* ruff fixes

* fix(template): gracefully handle missing 'needs' dependencies

When a section's 'needs' dependency references a non-existent section,
the CLI now logs a warning instead of raising an error. This allows
templates to be modified without breaking when dependencies are removed.

Closes #1428

* fix(template): skip empty files during generation (#1518) (#1530)

* big update

* docs: add markdown support to changelog (#1471)

* feature(install): add auto-install for dependencies on Linux and macOS

- Auto-detects OS and Linux distribution
- Installs python3, pip, git, and pipx if missing
- Supports Ubuntu, Debian, Fedora, RHEL, CentOS, Rocky, AlmaLinux, openSUSE, Arch, Manjaro, Alpine, and macOS
- Adds --no-auto-install flag to skip automatic installation
- Improves error messages with clear installation instructions

Closes #1517

* fix(install): handle PEP 668 externally-managed environments

- Try installing pipx from system packages first
- Fall back to pip with --break-system-packages flag for PEP 668
- Add pipx package names for each distro
- Improve pipx ensurepath handling

* fix(install): improve pipx installation error handling

- Properly suppress stderr when trying system package installation
- Better conditional logic for pip installation with --break-system-packages
- Add success logging for each installation method

* fix(install): suppress pip error output by checking success

- Use grep to check for 'Successfully installed' instead of exit codes
- This suppresses PEP 668 error output when trying pip methods
- Provides helpful error message suggesting manual apt install

* fix(install): handle distros without VERSION_ID in os-release

- Arch Linux and some other rolling distros don't have VERSION_ID
- Use parameter expansion to set empty default

* fix(install): add support for archarm distribution

- OrbStack Arch Linux uses 'archarm' as distribution ID
- Add it to the Arch Linux case pattern

* chore: add build/ and dist/ to .gitignore

- Ignore Python build artifacts
- Ignore distribution packages

* fix: add 'boilerplates' prefix to command suggestions in help text

- Update help messages to show 'boilerplates repo update' instead of 'repo update'
- Makes it clearer that commands should be run with the boilerplates CLI prefix
- Addresses user feedback from issue #1517

* feat(gitlab): integrate improvements from template/1372 with schema 1.2

- Add .env.j2 for environment variables (root password)
- Add container_hostname, root_email, root_password variables
- Add initial root user configuration to gitlab.rb
- Add default_theme, default_color_mode, disable_usage_data settings
- Improve template description and next_steps documentation
- Add env_file and swarm configs/secrets support
- Update to use schema 1.2 volume section (volume_mode instead of swarm_volume_*)
- Fix registry port from 5678 to standard 5000
- Add swarm placement constraints support
- Update ports section to include ports_https

* archetype validation testing

* schema1.2-traefik_domain

* feat(traefik): add multiple DNS challenge providers

- Add support for Porkbun, GoDaddy, DigitalOcean, Route53 (AWS), Azure, GCP, and Namecheap
- Add provider-specific credential variables with conditional visibility
- Support both standard and Docker Swarm modes for all providers
- Update environment variable handling for each provider

Closes #1478

* fix repo and changelog

* feature(docs): add GitHub Action to auto-generate wiki variable documentation

- Created .github/scripts/generate_wiki_docs.py script

- Generates markdown documentation for all module variables

- Uses latest schema version for each module

- Created workflow to auto-update wiki on schema changes

- Workflow triggers on changes to module specs and script

- Runs on release/v0.1.0 branch (will switch to main later)

Relates to #1316

* docs(wiki): add prominent Contributing section with CONTRIBUTING.md link

- Added Contributing section in Developer Documentation area

- Links directly to CONTRIBUTING.md in repository

- Highlights key points: CLI requires Discord, templates welcome PRs

Relates to #1316

* Documentation

* template updates

* template updates

* fix(compose): add Loki batching configuration to Alloy template

- Add batch_wait (5s) and batch_size (1MB) to reduce request volume
- Add max_backoff (5m) and min_backoff (500ms) for retry reliability
- Prevents ingestion rate limit errors with multiple Alloy instances
- Reduces HTTP overhead and improves compression efficiency

Fixes #1556

* updates

* update schema

* update

* fix ruff

* update wiki

* fix wiki

* fix wiki

* refactor(workflows): rename and extend wiki sync workflow

- Rename docs-update-wiki-variables.yaml to docs-update-wiki.yaml
- Add syncing of static wiki pages from .wiki/ directory
- Add .wiki/** to workflow triggers
- Improve commit message and workflow description

* fix(workflows): improve wiki branch detection for new wikis

- Add fallback to current branch if symbolic ref doesn't exist
- Prevents 'invalid refspec' error on newly created wikis

* fix(workflows): simplify wiki workflow by assuming master branch

- Remove complex branch detection that was causing empty variable issues
- Hardcode master branch (GitHub wikis default)
- Remove unnecessary initialization check (wiki must exist for checkout to succeed)
- Simplify commit message

* update email settings

* fix wiki

* big template updates 1

* big template updates 1

* template refactoring 2

* working on templates 2

* working on templates 3

* refactoring updates

* release-test-1

* release-test-2

* fix(templates): resolve yamllint errors - add missing newlines and remove duplicate key

* fix(templates): resolve yamllint line-length warnings in descriptions
2025-12-10 16:02:30 +01:00

304 lines
11 KiB
Python

from __future__ import annotations
import logging
import re
from typing import TYPE_CHECKING
from rich import box
from rich._loop import loop_first
from rich.console import Console, ConsoleOptions, RenderResult
from rich.markdown import Heading, ListItem, Markdown
from rich.panel import Panel
from rich.segment import Segment
from rich.text import Text
from .display_icons import IconManager
from .display_settings import DisplaySettings
if TYPE_CHECKING:
from .display_base import BaseDisplay
logger = logging.getLogger(__name__)
console_err = Console(stderr=True) # Keep for error output
class LeftAlignedHeading(Heading):
"""Custom Heading element with left alignment and no extra spacing."""
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
text = self.text
text.justify = "left" # Override center justification
if self.tag == "h1":
# Draw a border around h1s (left-aligned)
yield Panel(
text,
box=box.HEAVY,
style="markdown.h1.border",
)
else:
# Styled text for h2 and beyond (no blank line before h2)
yield text
class IconListItem(ListItem):
"""Custom list item that replaces bullets with colored icons from shortcodes."""
def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:
"""Render list item with icon replacement if text starts with :shortcode:."""
# Get the text content from elements
text_content = ""
for element in self.elements:
if hasattr(element, "text"):
text_content = element.text.plain
break
icon_used = None
icon_color = "cyan" # Default color for icons
shortcode_found = None
# Scan for shortcode at the beginning
for shortcode, icon in IconManager.SHORTCODES.items():
if text_content.strip().startswith(shortcode):
icon_used = icon
shortcode_found = shortcode
# Map shortcodes to colors
shortcode_colors = {
":warning:": "yellow",
":error:": "red",
":check:": "green",
":success:": "green",
":info:": "blue",
":docker:": "blue",
":kubernetes:": "blue",
":rocket:": "magenta",
":star:": "yellow",
":lightning:": "yellow",
}
icon_color = shortcode_colors.get(shortcode, "cyan")
break
if icon_used and shortcode_found:
# Remove the shortcode from the text in all elements
for element in self.elements:
if hasattr(element, "text"):
# Replace the shortcode in the Text object
plain_text = element.text.plain
new_text = plain_text.replace(shortcode_found, "", 1).lstrip()
# Reconstruct the Text object with the same style
element.text = Text(new_text, style=element.text.style)
# Render with custom colored icon instead of bullet
render_options = options.update(width=options.max_width - 3)
lines = console.render_lines(self.elements, render_options, style=self.style)
bullet_style = console.get_style(icon_color, default="none")
bullet = Segment(f" {icon_used} ", bullet_style)
padding = Segment(" " * 3)
new_line = Segment("\n")
for first, line in loop_first(lines):
yield bullet if first else padding
yield from line
yield new_line
else:
# No icon found, use default list item rendering
yield from super().render_bullet(console, options)
class LeftAlignedMarkdown(Markdown):
"""Custom Markdown renderer with left-aligned headings and icon list items."""
def __init__(self, markup: str, **kwargs):
"""Initialize with custom heading and list item elements."""
super().__init__(markup, **kwargs)
# Replace heading element to use left alignment
self.elements["heading_open"] = LeftAlignedHeading
# Replace list item element to use icon replacement
self.elements["list_item_open"] = IconListItem
class StatusDisplay:
"""Status messages and error display.
Provides methods for displaying success, error, warning,
and informational messages with consistent formatting.
"""
def __init__(self, settings: DisplaySettings, quiet: bool, base: BaseDisplay):
"""Initialize StatusDisplay.
Args:
settings: Display settings for formatting
quiet: If True, suppress non-error output
base: BaseDisplay instance
"""
self.settings = settings
self.quiet = quiet
self.base = base
def _display_message(self, level: str, message: str, context: str | None = None) -> None:
"""Display a message with consistent formatting.
Args:
level: Message level (error, warning, success, info)
message: The message to display
context: Optional context information
"""
# Errors and warnings always go to stderr, even in quiet mode
# Success and info respect quiet mode and go to stdout
use_stderr = level in ("error", "warning")
should_print = use_stderr or not self.quiet
if not should_print:
return
settings = self.settings
colors = {
"error": settings.COLOR_ERROR,
"warning": settings.COLOR_WARNING,
"success": settings.COLOR_SUCCESS,
}
color = colors.get(level)
# Format message based on context
if context:
text = (
f"{level.capitalize()} in {context}: {message}"
if level in {"error", "warning"}
else f"{context}: {message}"
)
else:
text = f"{level.capitalize()}: {message}" if level in {"error", "warning"} else message
# Only use icons and colors for actual status indicators (error, warning, success)
# Plain info messages use default terminal color (no markup)
if level in {"error", "warning", "success"}:
icon = IconManager.get_status_icon(level)
formatted_text = f"[{color}]{icon} {text}[/{color}]"
else:
formatted_text = text
if use_stderr:
console_err.print(formatted_text)
else:
self.base.text(formatted_text)
# Log appropriately
log_message = f"{context}: {message}" if context else message
log_methods = {
"error": logger.error,
"warning": logger.warning,
"success": logger.info,
"info": logger.info,
}
log_methods.get(level, logger.info)(log_message)
def error(self, message: str, context: str | None = None, details: str | None = None) -> None:
"""Display an error message.
Args:
message: Error message
context: Optional context
details: Optional additional details (shown in dim style on same line)
"""
if details:
# Combine message and details on same line with different formatting
settings = self.settings
color = settings.COLOR_ERROR
icon = IconManager.get_status_icon("error")
# Format: Icon Error: Message (details in dim)
formatted = f"[{color}]{icon} Error: {message}[/{color}] [dim]({details})[/dim]"
console_err.print(formatted)
# Log at debug level to avoid duplicate console output (already printed to stderr)
logger.debug(f"Error displayed: {message} ({details})")
else:
# No details, use standard display
self._display_message("error", message, context)
def warning(self, message: str, context: str | None = None, details: str | None = None) -> None:
"""Display a warning message.
Args:
message: Warning message
context: Optional context
details: Optional additional details (shown in dim style on same line)
"""
if details:
# Combine message and details on same line with different formatting
settings = self.settings
color = settings.COLOR_WARNING
icon = IconManager.get_status_icon("warning")
# Format: Icon Warning: Message (details in dim)
formatted = f"[{color}]{icon} Warning: {message}[/{color}] [dim]({details})[/dim]"
console_err.print(formatted)
# Log at debug level to avoid duplicate console output (already printed to stderr)
logger.debug(f"Warning displayed: {message} ({details})")
else:
# No details, use standard display
self._display_message("warning", message, context)
def success(self, message: str, context: str | None = None) -> None:
"""Display a success message.
Args:
message: Success message
context: Optional context
"""
self._display_message("success", message, context)
def info(self, message: str, context: str | None = None) -> None:
"""Display an informational message.
Args:
message: Info message
context: Optional context
"""
self._display_message("info", message, context)
def skipped(self, message: str, reason: str | None = None) -> None:
"""Display a skipped/disabled message.
Args:
message: The main message to display
reason: Optional reason why it was skipped
"""
if reason:
self.base.text(f"\n{message} (skipped - {reason})", style="dim")
else:
self.base.text(f"\n{message} (skipped)", style="dim")
def markdown(self, content: str) -> None:
"""Render markdown content with left-aligned headings.
Replaces emoji-style shortcodes (e.g., :warning:, :info:) with Nerd Font icons
before rendering, EXCEPT for shortcodes at the start of list items which are
handled by IconListItem to replace the bullet.
Args:
content: Markdown-formatted text to render (may contain shortcodes)
"""
if not self.quiet:
# Replace shortcodes with Nerd Font icons, but preserve list item shortcodes
# Pattern: "- :shortcode:" at start of line should NOT be replaced
lines = content.split("\n")
processed_lines = []
for line in lines:
# Check if line is a list item starting with a shortcode
if re.match(r"^\s*-\s+:[a-z]+:", line):
# Keep the line as-is, IconListItem will handle it
processed_lines.append(line)
else:
# Replace shortcodes normally
processed_lines.append(IconManager.replace_shortcodes(line))
processed_content = "\n".join(processed_lines)
self.base._print_markdown(LeftAlignedMarkdown(processed_content))