Initial upload with full project structure
This commit is contained in:
295
app/src/confgen/cli.py
Normal file
295
app/src/confgen/cli.py
Normal 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)
|
||||
Reference in New Issue
Block a user