updates
This commit is contained in:
73 files changed
+3290
-4522
No files matched your search
Whitespace-only changes.
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user