| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- """
- Compose module commands and functionality.
- Manage Compose configurations and services and template operations.
- """
- import re
- import typer
- from pathlib import Path
- from rich.console import Console
- from rich.table import Table
- from rich.syntax import Syntax
- from typing import List, Optional, Set, Dict, Any
- from ...core.command import BaseModule
- from ...core.helpers import find_boilerplates
- from .variables import ComposeVariables
- class ComposeModule(BaseModule):
- """Module for managing compose boilerplates."""
- compose_filenames = ["compose.yaml", "docker-compose.yaml", "compose.yml", "docker-compose.yml"]
- _library_path = Path(__file__).parent.parent.parent.parent / "library" / "compose"
- def __init__(self):
- super().__init__(name="compose", icon="🐳", description="Manage Compose Templates and Configurations")
- # Core BaseModule integration
- @property
- def template_paths(self) -> List[str]:
- # Prefer compose.yaml as default per project rules
- return self.compose_filenames
- @property
- def library_path(self) -> Path:
- return self._library_path
- @property
- def variable_handler_class(self):
- return ComposeVariables
-
- def _get_variable_details(self) -> Dict[str, Dict[str, Any]]:
- """Get detailed information about variables for display."""
- variables = ComposeVariables()
- details = {}
- for var_name, (set_name, var_meta) in variables._declared.items():
- details[var_name] = {
- 'set': set_name,
- 'type': var_meta.get('type', 'str'),
- 'display_name': var_meta.get('display_name', var_name),
- 'default': var_meta.get('default'),
- 'prompt': var_meta.get('prompt', '')
- }
- return details
- def _add_custom_commands(self, app: typer.Typer) -> None:
- """Add compose-specific commands to the app."""
- @app.command("list", help="List all compose boilerplates")
- def list():
- """List all compose boilerplates from library/compose directory."""
- bps = find_boilerplates(self.library_path, self.compose_filenames)
- if not bps:
- self.console.print("[yellow]No compose boilerplates found.[/yellow]")
- return
- table = Table(title="🐳 Available Compose Boilerplates", title_style="bold blue")
- table.add_column("Name", style="cyan", no_wrap=True)
- table.add_column("Module", style="magenta")
- table.add_column("Path", style="green")
- table.add_column("Size", justify="right", style="yellow")
- table.add_column("Description", style="dim")
- for bp in bps:
- if bp.size < 1024:
- size_str = f"{bp.size} B"
- elif bp.size < 1024 * 1024:
- size_str = f"{bp.size // 1024} KB"
- else:
- size_str = f"{bp.size // (1024 * 1024)} MB"
- table.add_row(
- bp.name,
- bp.module,
- str(bp.file_path.relative_to(self.library_path)),
- size_str,
- bp.description[:50] + "..." if len(bp.description) > 50 else bp.description
- )
- self.console.print(table)
- @app.command("show", help="Show details about a compose boilerplate")
- def show(name: str, raw: bool = typer.Option(False, "--raw", help="Output only the raw boilerplate content")):
- """Show details about a compose boilerplate by name."""
- bps = find_boilerplates(self.library_path, self.compose_filenames)
- # Match by directory name (parent folder of the compose file) instead of frontmatter 'name'
- bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
- if not bp:
- self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
- return
- if raw:
- # Output only the raw boilerplate content
- print(bp.content)
- return
- # Print frontmatter info in a clean, readable format
- from rich.text import Text
- from rich.console import Group
-
- info = bp.to_dict()
-
- # Create a clean header
- header = Text()
- header.append("🐳 Boilerplate: ", style="bold")
- header.append(f"{info['name']}", style="bold blue")
- header.append(f" ({info['version']})", style="magenta")
- header.append("\n", style="bold")
- header.append(f"{info['description']}", style="dim white")
-
- # Create metadata section with clean formatting
- metadata = Text()
- metadata.append("\nDetails:\n", style="bold cyan")
- metadata.append("─" * 40 + "\n", style="dim cyan")
-
- # Format each field with consistent styling
- fields = [
- ("Tags", ", ".join(info['tags']), "cyan"),
- ("Author", info['author'], "dim white"),
- ("Date", info['date'], "dim white"),
- ("Size", info['size'], "dim white"),
- ("Path", info['path'], "dim white")
- ]
-
- for label, value, color in fields:
- metadata.append(f"{label}: ")
- metadata.append(f"{value}\n", style=color)
-
- # Handle files list if present
- if info['files'] and len(info['files']) > 0:
- metadata.append(" Files: ")
- files_str = ", ".join(info['files'][:3]) # Show first 3
- if len(info['files']) > 3:
- files_str += f" ... and {len(info['files']) - 3} more"
- metadata.append(f"{files_str}\n", style="green")
-
- # Display everything as a group
- display_group = Group(header, metadata)
- self.console.print(display_group)
- # Show the content of the boilerplate file in a cleaner form
- from rich.panel import Panel
- from rich.syntax import Syntax
- # Detect if content contains Jinja2 templating
- has_jinja = bool(re.search(r'\{\{.*\}\}|\{\%.*\%\}|\{\#.*\#\}', bp.content))
-
- # Use appropriate lexer based on content
- # Use yaml+jinja for combined YAML and Jinja2 highlighting when Jinja2 is present
- lexer = "yaml+jinja" if has_jinja else "yaml"
- syntax = Syntax(bp.content, lexer, theme="monokai", line_numbers=True, word_wrap=True)
- panel = Panel(syntax, title=f"{bp.file_path.name}", border_style="blue", padding=(1,2))
- self.console.print(panel)
- @app.command("search", help="Search compose boilerplates")
- def search(query: str):
- pass
- @app.command("generate", help="Generate a compose file from a boilerplate and write to --out")
- def generate(
- name: str,
- out: Optional[Path] = typer.Option(None, "--out", "-o", help="Output path to write rendered boilerplate (prints to stdout when omitted)"),
- values_file: Optional[Path] = typer.Option(None, "--values-file", "-f", help="Load values from YAML/JSON file"),
- cli_values: Optional[List[str]] = typer.Option(None, "--values", help="Set values (format: key=value)")
- ):
- """Render a compose boilerplate interactively and write output to --out."""
- from ...core import template, values as values_mod, render
- from ...core.config import ConfigManager
- # Find and validate boilerplate
- bps = find_boilerplates(self.library_path, self.compose_filenames)
- bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
- if not bp:
- self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
- raise typer.Exit(code=1)
- # Clean template content and find variables
- cv = ComposeVariables()
- cleaned_content = template.clean_template_content(bp.content)
- matched_sets, used_vars = cv.determine_variable_sets(cleaned_content)
- # If no variables used, return original content
- if not used_vars:
- rendered = bp.content
- else:
- # Validate template syntax
- is_valid, error = template.validate_template(cleaned_content, bp.file_path)
- if not is_valid:
- self.console.print(f"[red]{error}[/red]")
- raise typer.Exit(code=2)
- # Extract defaults and variable metadata
- template_defaults = cv.extract_template_defaults(cleaned_content)
- try:
- meta_overrides = cv.extract_variable_meta_overrides(bp.content)
- # Merge overrides into declared metadata
- for var_name, overrides in meta_overrides.items():
- if var_name in cv._declared and isinstance(overrides, dict):
- existing = cv._declared[var_name][1]
- existing.update(overrides)
- except Exception:
- meta_overrides = {}
- # Get subscript keys and load values from all sources
- used_subscripts = cv.find_used_subscript_keys(bp.content)
- config_manager = ConfigManager(self.name)
- try:
- merged_values = values_mod.load_and_merge_values(
- values_file=values_file,
- cli_values=cli_values,
- config_values=config_manager.list_all(),
- defaults=template_defaults
- )
- except Exception as e:
- self.console.print(f"[red]{str(e)}[/red]")
- raise typer.Exit(code=1)
- # Collect final values and render template
- values_dict = cv.collect_values(used_vars, merged_values, used_subscripts)
- success, rendered, error = template.render_template(
- cleaned_content,
- values_dict
- )
- if not success:
- self.console.print(f"[red]{error}[/red]")
- raise typer.Exit(code=2)
- # Output the rendered content
- output_handler = render.RenderOutput(self.console)
- output_handler.output_rendered_content(
- rendered,
- out,
- "yaml",
- bp.name
- )
|