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

+198
View File
@@ -0,0 +1,198 @@
import logging
import re
import subprocess
import time
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
from jinja2 import Environment, FileSystemLoader
def ensure_log_directory() -> Path:
log_dir = Path.home() / ".cache" / "devgen"
log_dir.mkdir(parents=True, exist_ok=True)
return log_dir
def get_main_log_path() -> Path:
return ensure_log_directory() / "devgen.log"
def get_commit_dry_run_path() -> Path:
return ensure_log_directory() / "commit_dry_run.md"
def is_file_recent(file_path: Path | str, max_age_minutes: int = 120) -> bool:
path = Path(file_path)
if not path.exists():
return False
return (time.time() - path.stat().st_mtime) <= max_age_minutes * 60
def sanitize_ai_commit_message(raw_text: str) -> str:
lines = raw_text.strip().split("\n")
cleaned_lines = []
in_block = False
# Regex for conventional commit header
header_pattern = re.compile(
r"^(feat|fix|chore|refactor|docs|style|test|build|ci)(\(.*\))?!?: .*"
)
for line in lines:
stripped = line.strip()
if in_block:
if header_pattern.match(stripped) or "**Sponsor**" in line:
break
cleaned_lines.append(line)
elif header_pattern.match(stripped):
in_block = True
cleaned_lines.append(line)
return "\n".join(cleaned_lines).strip() if cleaned_lines else ""
def parse_markdown_sections(
filepath: Path | str, marker_pattern: str
) -> dict[str, str]:
path = Path(filepath)
if not path.exists():
return {}
with path.open(encoding="utf-8") as f:
content = f.read()
results = {}
matches = re.findall(marker_pattern, content, re.DOTALL)
for key, value in matches:
results[key] = value.strip()
return results
def extract_commit_messages(filepath: Path | str) -> dict[str, str]:
pattern = r"## Group: `(.*?)`\s*```md\n(.*?)\n```"
return parse_markdown_sections(filepath, pattern)
def configure_logger(
name: str = "devgen", log_file: Optional[Path | str] = None
) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if logger.hasHandlers():
logger.handlers.clear()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Console handler
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
# File handler
if log_file:
path = Path(log_file)
path.parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(path, mode="w", encoding="utf-8")
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger
def get_git_staged_files() -> list[str]:
try:
res = subprocess.run(
["git", "diff", "--name-only", "--cached"],
capture_output=True,
text=True,
check=True,
)
return [f for f in res.stdout.splitlines() if f.strip()]
except subprocess.CalledProcessError:
return []
def read_file_content(filepath: Path | str) -> Optional[str]:
path = Path(filepath)
if path.exists():
return path.read_text(encoding="utf-8")
return None
def delete_file(filepath: Path | str) -> bool:
path = Path(filepath)
if path.exists():
path.unlink()
return True
return False
def load_template_env(sub_dir: str) -> Environment:
template_dir = Path(__file__).parent / "prompt_templates" / sub_dir
return Environment(loader=FileSystemLoader(template_dir))
def load_config() -> Dict[str, Any]:
config_path = Path.home() / ".devgen.yaml"
if not config_path.exists():
default_config = {
"provider": "gemini",
"model": "gemini-2.5-flash",
"api_key": "",
"emoji": True,
}
try:
with config_path.open("w", encoding="utf-8") as f:
yaml.dump(default_config, f, default_flow_style=False)
# We don't print here to avoid noise during normal execution
except Exception as e:
print(f"Warning: Failed to create default config at {config_path}: {e}")
return {}
try:
with config_path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except Exception as e:
print(f"Warning: Failed to load config from {config_path}: {e}")
return {}
__all__ = [
"ensure_log_directory",
"get_main_log_path",
"get_commit_dry_run_path",
"is_file_recent",
"sanitize_ai_commit_message",
"extract_commit_messages",
"configure_logger",
"get_git_staged_files",
"read_file_content",
"delete_file",
"load_template_env",
"load_config",
"get_questionary_style",
]
def get_questionary_style():
from questionary import Style
return Style(
[
("qmark", "fg:#673ab7 bold"), # Token.QuestionMark
("question", "bold"), # Token.Question
("answer", "fg:#f44336 bold"), # Token.Answer
("pointer", "fg:#673ab7 bold"), # Token.Pointer
("highlighted", "fg:#673ab7 bold"), # Token.Selected
("selected", "fg:#cc5454"), # Token.SelectedItem
("separator", "fg:#cc5454"), # Token.Separator
("instruction", ""), # Token.Instruction
("text", ""), # Token.Text
("disabled", "fg:#858585 italic"), # Token.Disabled
]
)