- Fix KeyError 'root' in FileGrouper when merging groups with no root-level files - Fix changelog commit parsing breaking on multiline bodies (use %x00 separator) - Add empty choices/content checks in OpenAI, Anthropic, OpenRouter providers - Fix config edit crash when config is empty - Fix validate command using wrong color for 'no staged files' - Add type check for corrupted YAML config (non-dict returns empty dict)
229 lines
6.7 KiB
Python
229 lines
6.7 KiB
Python
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())
|
|
if not choices:
|
|
typer.secho(
|
|
"No configuration keys found. Run `devgen setup config` first.",
|
|
fg=typer.colors.YELLOW,
|
|
)
|
|
raise typer.Exit()
|
|
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",
|
|
"ollama",
|
|
],
|
|
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",
|
|
"ollama",
|
|
],
|
|
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"
|
|
|
|
ollama_host = config.get("ollama_host", "http://localhost:11434")
|
|
if provider == "ollama":
|
|
ollama_host_input = questionary.text(
|
|
"Ollama server URL:",
|
|
default=ollama_host,
|
|
style=style,
|
|
).ask()
|
|
if ollama_host_input is None:
|
|
raise typer.Exit(code=130)
|
|
ollama_host = ollama_host_input.strip() or ollama_host
|
|
|
|
# Save Config
|
|
new_config = {
|
|
"provider": provider,
|
|
"model": model,
|
|
"api_key": api_key,
|
|
"emoji": emoji,
|
|
"ollama_host": ollama_host,
|
|
}
|
|
|
|
# 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))
|
|
|
|
|
|
@app.command("info")
|
|
def config_info() -> None:
|
|
"""Show information about configuration options and templates."""
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
table = Table(title="Custom Template Variables", box=None)
|
|
table.add_column("Variable", style="cyan")
|
|
table.add_column("Description", style="white")
|
|
|
|
table.add_row("{{ group_name }}", "The folder name being committed (or 'root').")
|
|
table.add_row("{{ diff_text }}", "The git diff of the changes.")
|
|
table.add_row("{{ context }}", "Project context (manifest files content).")
|
|
|
|
console.print(table)
|
|
console.print(
|
|
"\n[bold]Tip:[/bold] If you hardcode emojis in your template, the AI will likely include them regardless of the 'emoji' setting.",
|
|
style="yellow",
|
|
)
|
|
console.print("\n[bold]Example Template:[/bold]")
|
|
console.print(
|
|
"custom_template: |\n [type]: [desc]\n \n Diff: {{ diff_text }}\n",
|
|
style="dim",
|
|
)
|