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