diff --git a/devgen/cli/config.py b/devgen/cli/config.py new file mode 100644 index 0000000..633fe87 --- /dev/null +++ b/devgen/cli/config.py @@ -0,0 +1,169 @@ +from pathlib import Path +from typing import Any, Optional + +import questionary +import typer +import yaml +from typing_extensions import Annotated + +from devgen.utils import get_questionary_style, load_config + +app = typer.Typer( + name="config", + help="🔧 Manage configuration values.", + no_args_is_help=True, + rich_markup_mode="markdown", +) + + +def _save_config(config: dict[str, Any]) -> None: + config_path = Path.home() / ".devgen.yaml" + try: + with config_path.open("w", encoding="utf-8") as f: + yaml.dump(config, f, default_flow_style=False) + except Exception as e: + typer.secho(f"❌ Failed to save config: {e}", fg=typer.colors.RED) + raise typer.Exit(code=1) + + +@app.command("list") +def list_config() -> None: + """Show all configuration values.""" + config = load_config() + if not config: + typer.echo("Configuration is empty.") + return + + typer.echo(yaml.dump(config, default_flow_style=False)) + + +@app.command("edit") +def edit_config( + key: Annotated[Optional[str], typer.Argument(help="Configuration key.")] = None, + value: Annotated[Optional[str], typer.Argument(help="New value.")] = None, +) -> None: + """Edit a specific configuration value.""" + config = load_config() + style = get_questionary_style() + + if not key: + choices = list(config.keys()) + key = questionary.select( + "Select key to update:", choices=choices, style=style + ).ask() + + if not key: + raise typer.Exit() + + if value is None: + # Check if existing value is boolean to offer select + current_val = config.get(key) + + if key == "provider": + value = questionary.select( + "Select AI Provider:", + choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"], + default=str(current_val) if current_val else "gemini", + style=style, + ).ask() + if value is None: + raise typer.Exit(code=130) + elif isinstance(current_val, bool): + val_choice = questionary.select( + f"Select value for '{key}':", + choices=["True", "False"], + default=str(current_val), + style=style, + ).ask() + if val_choice is None: + raise typer.Exit(code=130) + value = val_choice + else: + value = questionary.text( + f"Enter value for '{key}':", + default=str(current_val) if current_val is not None else "", + style=style, + ).ask() + + if value is None: + raise typer.Exit(code=130) + + # Basic type inference + if isinstance(value, str): + if value.lower() == "true": + val = True + elif value.lower() == "false": + val = False + elif value.isdigit(): + val = int(value) + else: + val = value + else: + val = value + + config[key] = val + _save_config(config) + typer.secho(f" Set '{key}' to '{val}'", fg=typer.colors.GREEN) + + +@app.command("set") +def set_config() -> None: + """Run the interactive configuration wizard.""" + typer.secho("🛠️ Interactive Configuration Setup", fg=typer.colors.CYAN, bold=True) + + config = load_config() + style = get_questionary_style() + + # Questions + provider = questionary.select( + "Select AI Provider:", + choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"], + default=config.get("provider", "gemini"), + style=style, + ).ask() + if provider is None: + raise typer.Exit(code=130) + + model_default = config.get("model", "gemini-2.5-flash") + + model = questionary.text( + "Enter Model Name:", default=model_default, style=style + ).ask() + if model is None: + raise typer.Exit(code=130) + + api_key = questionary.password( + "Enter API Key (leave empty to keep existing or none):", style=style + ).ask() + if api_key is None: + raise typer.Exit(code=130) + + if not api_key: + api_key = config.get("api_key", "") + + emoji_choice = questionary.select( + "Use Emojis in Commit Messages?", + choices=["Yes", "No"], + default="Yes" if config.get("emoji", True) else "No", + style=style, + ).ask() + if emoji_choice is None: + raise typer.Exit(code=130) + emoji = emoji_choice == "Yes" + + # Save Config + new_config = { + "provider": provider, + "model": model, + "api_key": api_key, + "emoji": emoji, + } + + # Merge with existing config to preserve other keys? + # The setup wizard usually overwrites or defines the core keys. + # Let's preserve other keys. + config.update(new_config) + + _save_config(config) + typer.secho("\nConfiguration saved.", fg=typer.colors.GREEN) + typer.echo(yaml.dump(new_config, default_flow_style=False)) diff --git a/devgen/cli/main.py b/devgen/cli/main.py index c25fbf6..e640b71 100644 --- a/devgen/cli/main.py +++ b/devgen/cli/main.py @@ -6,6 +6,7 @@ import typer from devgen.cli.changelog import app as changelog_app from devgen.cli.commit import app as commit_app +from devgen.cli.config import app as config_app from devgen.cli.gitignore import app as gitignore_app from devgen.cli.license import app as license_app from devgen.cli.release import app as release_app @@ -90,6 +91,7 @@ app.add_typer(changelog_app, name="changelog") app.add_typer(license_app, name="license") app.add_typer(gitignore_app, name="gitignore") app.add_typer(release_app, name="release") +app.add_typer(config_app, name="config") if __name__ == "__main__": try: