This commit is contained in:
sankalp committed 2025-12-12 11:10:30 +05:45
1 parent b5362ade00
commit dec9c15b7a
73 files changed
+3290 -4522

No files matched your search

View File
Whitespace-only changes.
+38
View File
@@ -0,0 +1,38 @@
import typer
from typing_extensions import Annotated
from devgen.modules.changelog_generator import ChangelogGenerator
app = typer.Typer(
name="changelog",
help="📝 Generate changelogs from git history.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("generate")
def generate_changelog(
output: Annotated[
str,
typer.Option(
"--output",
"-o",
help="Output file path.",
),
] = "CHANGELOG.md",
from_ref: Annotated[
str,
typer.Option(
"--from",
"-f",
help="Starting git reference (tag or hash). Defaults to last tag.",
),
] = "",
) -> None:
generator = ChangelogGenerator()
try:
generator.run(output_file=output, from_ref=from_ref)
except Exception as e:
typer.secho(f"Error generating changelog: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
+96
View File
@@ -0,0 +1,96 @@
from typing import Annotated
import typer
from devgen.modules.commit_generator import run_commit_engine
from devgen.utils import (
configure_logger,
delete_file,
get_commit_dry_run_path,
get_git_staged_files,
get_main_log_path,
read_file_content,
)
app = typer.Typer(
name="commit",
help="🚀 AI-powered semantic commit message generator.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("run")
def run_commit(
dry_run: Annotated[
bool,
typer.Option(
"--dry-run",
help="Simulate the commit process without making changes.",
),
] = False,
push: Annotated[
bool,
typer.Option(
"--push",
help="Automatically push changes to the remote repository.",
),
] = False,
debug: Annotated[
bool,
typer.Option(
"--debug",
help="Enable debug mode for detailed logging.",
),
] = False,
force_rebuild: Annotated[
bool,
typer.Option(
"--force-rebuild",
help="Force regeneration of commit messages.",
),
] = False,
) -> None:
log_file = get_main_log_path()
logger = configure_logger("devgen.cli.commit", log_file)
logger.info(f"Log file: {log_file}")
logger.info(
f"Options: dry_run={dry_run}, push={push}, debug={debug}, force={force_rebuild}"
)
run_commit_engine(
dry_run=dry_run,
push=push,
debug=debug,
force_rebuild=force_rebuild,
logger=logger,
)
@app.command("clear-cache")
def clear_cache() -> None:
if delete_file(get_commit_dry_run_path()):
typer.secho("Cache cleared.", fg=typer.colors.GREEN)
else:
typer.secho("[i] No cache found.", fg=typer.colors.YELLOW)
@app.command("list-cached")
def list_cached() -> None:
content = read_file_content(get_commit_dry_run_path())
if content:
typer.secho("--- Cached Dry-Run ---", fg=typer.colors.CYAN)
typer.echo(content)
else:
typer.secho("[i] No cache found.", fg=typer.colors.YELLOW)
@app.command("validate")
def validate() -> None:
staged = get_git_staged_files()
if staged:
typer.secho(f"{len(staged)} staged file(s):", fg=typer.colors.GREEN)
for f in staged:
typer.echo(f"- {f}")
else:
typer.secho("[i] No staged files.", fg=typer.colors.RED)
+133
View File
@@ -0,0 +1,133 @@
from typing import List, Optional
import questionary
import typer
from typing_extensions import Annotated
from devgen.modules.gitignore_generator import GitignoreGenerator
app = typer.Typer(
name="gitignore",
help="🙈 Generate .gitignore files from GitHub templates.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("list")
def list_templates(
cached: Annotated[
bool, typer.Option("--cached", "-c", help="List only cached templates.")
] = False,
) -> None:
generator = GitignoreGenerator()
try:
if cached:
templates = generator.list_cached_templates()
typer.secho("Cached Templates:", fg=typer.colors.CYAN, bold=True)
else:
typer.secho("Fetching available templates...", fg=typer.colors.YELLOW)
templates = generator.list_available_templates()
typer.secho("Available Templates:", fg=typer.colors.CYAN, bold=True)
if not templates:
typer.echo("No templates found.")
return
# Print in columns or just list
typer.echo(", ".join(templates))
except Exception as e:
typer.secho(f"Error: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
@app.command("generate")
def generate_gitignore(
templates: Annotated[
Optional[List[str]],
typer.Argument(
help="Names of templates to include (e.g. Python Node). If empty, interactive mode is used."
),
] = None,
output: Annotated[
str,
typer.Option("--output", "-o", help="Output file path."),
] = ".gitignore",
append: Annotated[
bool,
typer.Option(
"--append/--overwrite",
"-a/-w",
help="Append to existing file or overwrite.",
),
] = True,
offline: Annotated[
bool,
typer.Option("--offline", help="Use only cached templates."),
] = False,
) -> None:
generator = GitignoreGenerator()
if not templates:
# Interactive mode
try:
if offline:
available = generator.list_cached_templates()
else:
typer.secho("Fetching templates list...", fg=typer.colors.YELLOW)
available = generator.list_available_templates()
if not available:
typer.secho("No templates available.", fg=typer.colors.RED)
raise typer.Exit(code=1)
from devgen.utils import get_questionary_style
style = get_questionary_style()
# Interactive search loop
selected_templates = []
while True:
# Filter out already selected
choices = [t for t in available if t not in selected_templates]
# Add "Done" option at the top
choices.insert(0, "Done (Finish selection)")
choice = questionary.autocomplete(
"Search for a template (type to search, select 'Done' to finish):",
choices=choices,
ignore_case=True,
match_middle=True,
validate=lambda x: x in choices or x == "",
style=style,
).ask()
if not choice or choice.startswith("Done"):
break
selected_templates.append(choice)
typer.secho(f"Added '{choice}'", fg=typer.colors.GREEN)
templates = selected_templates
if not templates:
typer.secho("No templates selected.", fg=typer.colors.YELLOW)
raise typer.Exit()
except typer.Exit:
raise
except KeyboardInterrupt:
typer.secho("\n Cancelled by user", fg=typer.colors.YELLOW)
raise typer.Exit()
except Exception as e:
typer.secho(f"Error fetching templates: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
try:
generator.generate(
templates, output_file=output, append=append, offline=offline
)
except Exception as e:
typer.secho(f"Error generating .gitignore: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
+91
View File
@@ -0,0 +1,91 @@
from datetime import datetime
from pathlib import Path
import questionary
import typer
from typing_extensions import Annotated
from devgen.modules.license_generator import LicenseGenerator
app = typer.Typer(
name="license",
help="📄 Generate open source licenses.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("generate")
def generate_license(
output: Annotated[
str,
typer.Option(
"--output",
"-o",
help="Output file path.",
),
] = "LICENSE",
) -> None:
generator = LicenseGenerator()
licenses = generator.list_licenses()
if not licenses:
typer.secho("❌ No license templates found.", fg=typer.colors.RED)
raise typer.Exit(code=1)
from devgen.utils import get_questionary_style
style = get_questionary_style()
# 1. Select License
choices = [
questionary.Choice(
title=f"{lic['name']} ({lic['key']})",
value=lic["key"],
description=lic["description"],
)
for lic in licenses
]
license_key = questionary.select(
"Select a License:",
choices=choices,
use_indicator=True,
use_shortcuts=True,
style=style,
).ask()
if not license_key:
raise typer.Exit()
# 2. Enter Author Name
# Try to guess from git config if possible, but for now just empty default
author = questionary.text("Enter Author Name:", style=style).ask()
if not author:
typer.secho("❌ Author name is required.", fg=typer.colors.RED)
raise typer.Exit(code=1)
# 3. Enter Year
current_year = str(datetime.now().year)
year = questionary.text("Enter Year:", default=current_year, style=style).ask()
# Generate
try:
content = generator.render_license(license_key, author, year)
output_path = Path(output)
if output_path.exists():
overwrite = questionary.confirm(
f"File {output} already exists. Overwrite?", default=False, style=style
).ask()
if not overwrite:
typer.secho("Operation cancelled.", fg=typer.colors.YELLOW)
raise typer.Exit()
output_path.write_text(content, encoding="utf-8")
typer.secho(f"\n License generated at {output_path}", fg=typer.colors.GREEN)
except Exception as e:
typer.secho(f"\n Failed to generate license: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)
+99
View File
@@ -0,0 +1,99 @@
from pathlib import Path
from typing import Annotated
import toml
import typer
from devgen.cli.changelog import app as changelog_app
from devgen.cli.commit import app as commit_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
from devgen.cli.setup import app as setup_app
app = typer.Typer(
name="devgen",
help="devgen-py: AI-Powered Git Commit & Release Automation.",
add_completion=False,
no_args_is_help=True,
rich_markup_mode="markdown",
)
def _version_callback(value: bool) -> None:
"""Displays the application version retrieved from pyproject.toml if the input value is True, then exits the program. If the version information cannot be found or the file is missing or malformed, outputs an error message before exiting.
Args:
value (bool): A boolean flag indicating whether to display the version and exit.
Returns:
None
Raises:
typer.Exit: Exits the program after attempting to display the version or an error message.
"""
if value:
try:
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
pyproject_data = toml.load(pyproject_path)
# Try standard PEP 621 first, then fallback
version = pyproject_data.get("project", {}).get("version")
if not version:
version = (
pyproject_data.get("tool", {}).get("poetry", {}).get("version")
)
if version:
typer.echo(f"devgen version: {version}")
else:
typer.secho(
"Error: Version not found in pyproject.toml",
fg=typer.colors.RED,
err=True,
)
except (FileNotFoundError, KeyError, Exception) as e:
typer.secho(
f"Error: Could not determine version: {e}",
fg=typer.colors.RED,
err=True,
)
raise typer.Exit()
@app.callback()
def main_callback(
version: Annotated[
bool | None,
typer.Option(
"--version",
"-v",
callback=_version_callback,
is_eager=True,
help="Show the application version and exit.",
),
] = None,
) -> None:
"""Displays the application version information when the --version or -v option is used. If the flag is set, shows the version and exits; otherwise, provides guidance on using other commands with devgen.
Args:
version (bool or None, optional): A flag indicating whether to display the application version, set via command-line options --version or -v. Defaults to None.
Returns:
None
"""
pass
app.add_typer(commit_app, name="commit")
app.add_typer(setup_app, name="setup")
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")
if __name__ == "__main__":
try:
app()
except KeyboardInterrupt:
print("\n\n Operation cancelled by user.")
raise typer.Exit(code=130)
+30
View File
@@ -0,0 +1,30 @@
import typer
from typing_extensions import Annotated
from devgen.modules.release_note_generator import ReleaseNotesGenerator
app = typer.Typer(
name="release",
help="🚀 Generate release notes from git history.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("notes")
def generate_release_notes(
version: Annotated[
str,
typer.Option("--version", "-v", help="Version for this release (ex: 1.4.0)"),
] = "Unreleased",
output: Annotated[
str,
typer.Option("--output", "-o", help="Output file path."),
] = "RELEASE-NOTES.md",
from_ref: Annotated[
str,
typer.Option("--from", "-f", help="Start reference. Defaults to last tag."),
] = "",
):
generator = ReleaseNotesGenerator()
generator.run(output_file=output, version=version, from_ref=from_ref)
+76
View File
@@ -0,0 +1,76 @@
from pathlib import Path
import questionary
import typer
import yaml
app = typer.Typer(
name="setup",
help="⚙️ Setup and configuration management.",
no_args_is_help=True,
rich_markup_mode="markdown",
)
@app.command("config")
def setup_config() -> None:
typer.secho("🛠️ Interactive Configuration Setup", fg=typer.colors.CYAN, bold=True)
config_path = Path.home() / ".devgen.yaml"
current_config = {}
if config_path.exists():
try:
with config_path.open("r", encoding="utf-8") as f:
current_config = yaml.safe_load(f) or {}
except Exception:
pass
from devgen.utils import get_questionary_style
style = get_questionary_style()
# Questions
provider = questionary.select(
"Select AI Provider:",
choices=["gemini", "openai", "huggingface", "openrouter", "anthropic"],
default=current_config.get("provider", "gemini"),
style=style,
).ask()
model_default = current_config.get("model", "gemini-2.5-flash")
model = questionary.text(
"Enter Model Name:", default=model_default, style=style
).ask()
api_key = questionary.password(
"Enter API Key (leave empty to keep existing or none):", style=style
).ask()
if not api_key:
api_key = current_config.get("api_key", "")
emoji_choice = questionary.select(
"Use Emojis in Commit Messages?",
choices=["Yes", "No"],
default="Yes" if current_config.get("emoji", True) else "No",
style=style,
).ask()
emoji = emoji_choice == "Yes"
# Save Config
new_config = {
"provider": provider,
"model": model,
"api_key": api_key,
"emoji": emoji,
}
try:
with config_path.open("w", encoding="utf-8") as f:
yaml.dump(new_config, f, default_flow_style=False)
typer.secho(f"\n Configuration saved to {config_path}", fg=typer.colors.GREEN)
typer.echo(yaml.dump(new_config, default_flow_style=False))
except Exception as e:
typer.secho(f"\n Failed to save configuration: {e}", fg=typer.colors.RED)
raise typer.Exit(code=1)