Initial upload with full project structure

This commit is contained in:
2026-02-01 20:48:59 +00:00
parent cf4bd96c42
commit 1e04f28ee9

295
app/src/confgen/cli.py Normal file
View File

@@ -0,0 +1,295 @@
"""Command-line interface for confgen."""
from pathlib import Path
from typing import Optional
import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from .core import ConfgenCore
from .editor import InteractiveEditor
from .validator import SchemaValidator
console = Console()
@click.group()
@click.option(
"--config-dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
help="Directory containing confgen.yaml config",
)
@click.option(
"--templates-dir",
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
help="Directory containing template files",
)
@click.option(
"--output-dir",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
help="Directory for generated configs",
)
@click.option(
"--environment",
"-e",
default="dev",
help="Default environment (dev/staging/prod)",
)
@click.version_option(version="0.1.0")
@click.pass_context
def main(
ctx: click.Context,
config_dir: Optional[Path],
templates_dir: Optional[Path],
output_dir: Optional[Path],
environment: str,
) -> None:
"""Generate, validate, and manage environment-specific configuration files."""
ctx.ensure_object(dict)
ctx.obj["config_dir"] = config_dir or Path.cwd()
ctx.obj["templates_dir"] = templates_dir or Path.cwd() / "templates"
ctx.obj["output_dir"] = output_dir or Path.cwd() / "output"
ctx.obj["environment"] = environment
ctx.obj["core"] = ConfgenCore(
config_dir=ctx.obj["config_dir"],
templates_dir=ctx.obj["templates_dir"],
output_dir=ctx.obj["output_dir"],
environment=environment,
)
@main.command()
@click.argument("template_name", type=str)
@click.option(
"--environment",
"-e",
help="Environment name (overrides default)",
)
@click.option(
"--output",
"-o",
type=click.Path(path_type=Path),
help="Output file path",
)
@click.option(
"--format",
"-f",
type=click.Choice(["json", "yaml", "toml"]),
help="Output format",
)
@click.option(
"--validate/--no-validate",
default=True,
help="Validate against schema",
)
@click.option(
"--diff",
is_flag=True,
help="Show diff before applying",
)
@click.option(
"--edit",
is_flag=True,
help="Open interactive editor",
)
@click.pass_context
def generate(
ctx: click.Context,
template_name: str,
environment: Optional[str],
output: Optional[Path],
format: Optional[str],
validate: bool,
diff: bool,
edit: bool,
) -> None:
"""Generate a configuration file from a template."""
core = ctx.obj["core"]
env = environment or ctx.obj["environment"]
try:
if edit:
editor = InteractiveEditor(core, template_name, env)
result = editor.run()
if result:
console.print(Panel("[green]Configuration saved successfully![/green]"))
return
current_config = None
if output and output.exists():
current_config = output.read_text()
generated = core.generate_config(template_name, env, format)
if diff and current_config:
from .diff import ConfigDiff
diff_viewer = ConfigDiff()
diff_viewer.show_diff(current_config, generated.content)
if not click.confirm("Apply these changes?"):
console.print("[yellow]Operation cancelled.[/yellow]")
return
if output:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(generated.content)
console.print(f"[green]Generated: {output}[/green]")
else:
console.print(generated.content)
if validate and generated.schema_validated:
console.print("[green]Schema validation passed.[/green]")
elif validate and not generated.schema_validated:
console.print("[yellow]Schema validation skipped (no schema defined).[/yellow]")
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
raise click.Exit(1)
@main.command()
@click.option("--templates", is_flag=True, help="List available templates")
@click.option("--environments", is_flag=True, help="List available environments")
@click.option("--env", type=str, help="Show variables for specific environment")
@click.pass_context
def list(ctx: click.Context, templates: bool, environments: bool, env: Optional[str]) -> None:
"""List available templates and environments."""
core = ctx.obj["core"]
if templates or not environments:
table = Table(title="Templates")
table.add_column("Name")
table.add_column("Path")
table.add_column("Format")
for name, tmpl in core.config.templates.items():
table.add_row(name, str(tmpl.path), tmpl.format or "auto")
console.print(table)
if environments or env:
table = Table(title="Environments")
table.add_column("Name")
table.add_column("Variables")
for name, env_config in core.config.environments.items():
if env is None or name == env:
var_count = len(env_config.variables)
table.add_row(name, str(var_count) + " variables")
console.print(table)
if env:
console.print(f"\n[bold]Variables for {env}:[/bold]")
env_config = core.config.environments.get(env)
if env_config:
for key, value in env_config.variables.items():
if "SECRET" in key or "PASSWORD" in key or "API_KEY" in key:
console.print(f" {key}: *****")
else:
console.print(f" {key}: {value}")
@main.command()
@click.argument("config_file", type=click.Path(exists=True, path_type=Path))
@click.option(
"--schema",
"-s",
type=click.Path(exists=True, path_type=Path),
help="Schema file path",
)
@click.option(
"--template",
"-t",
type=str,
help="Template name to use its schema",
)
@click.pass_context
def validate(
ctx: click.Context,
config_file: Path,
schema: Optional[Path],
template: Optional[str],
) -> None:
"""Validate a configuration file against a schema."""
core = ctx.obj["core"]
if template and not schema:
tmpl = core.config.templates.get(template)
if tmpl and tmpl.schema:
schema = Path(tmpl.schema)
if not schema:
console.print("[red]No schema specified. Use --schema or --template.[/red]")
raise click.Exit(1)
validator = SchemaValidator(schema)
content = config_file.read_text()
if config_file.suffix == ".json":
import json
config_data = json.loads(content)
elif config_file.suffix in (".yaml", ".yml"):
import yaml
config_data = yaml.safe_load(content)
elif config_file.suffix == ".toml":
import toml
config_data = toml.loads(content)
else:
console.print("[red]Unsupported config format.[/red]")
raise click.Exit(1)
is_valid, errors = validator.validate(config_data)
if is_valid:
console.print("[green]Validation passed![/green]")
else:
console.print("[red]Validation failed:[/red]")
for error in errors:
console.print(f" - {error}")
raise click.Exit(1)
@main.command()
@click.argument("template_name", type=str)
@click.option(
"--environment",
"-e",
help="Environment name",
)
@click.option(
"--output",
"-o",
type=click.Path(path_type=Path),
help="Output file path",
)
@click.pass_context
def edit(
ctx: click.Context,
template_name: str,
environment: Optional[str],
output: Optional[Path],
) -> None:
"""Open interactive editor for editing config values."""
core = ctx.obj["core"]
env = environment or ctx.obj["environment"]
try:
editor = InteractiveEditor(core, template_name, env)
result = editor.run()
if result and output:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(result)
console.print(f"[green]Saved to: {output}[/green]")
elif result:
console.print(result)
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
raise click.Exit(1)