gac 3.6.0__py3-none-any.whl → 3.10.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. gac/__init__.py +4 -6
  2. gac/__version__.py +1 -1
  3. gac/ai_utils.py +59 -43
  4. gac/auth_cli.py +181 -36
  5. gac/cli.py +26 -9
  6. gac/commit_executor.py +59 -0
  7. gac/config.py +81 -2
  8. gac/config_cli.py +19 -7
  9. gac/constants/__init__.py +34 -0
  10. gac/constants/commit.py +63 -0
  11. gac/constants/defaults.py +40 -0
  12. gac/constants/file_patterns.py +110 -0
  13. gac/constants/languages.py +119 -0
  14. gac/diff_cli.py +0 -22
  15. gac/errors.py +8 -2
  16. gac/git.py +6 -6
  17. gac/git_state_validator.py +193 -0
  18. gac/grouped_commit_workflow.py +458 -0
  19. gac/init_cli.py +2 -1
  20. gac/interactive_mode.py +179 -0
  21. gac/language_cli.py +0 -1
  22. gac/main.py +231 -926
  23. gac/model_cli.py +67 -11
  24. gac/model_identifier.py +70 -0
  25. gac/oauth/__init__.py +26 -0
  26. gac/oauth/claude_code.py +89 -22
  27. gac/oauth/qwen_oauth.py +327 -0
  28. gac/oauth/token_store.py +81 -0
  29. gac/oauth_retry.py +161 -0
  30. gac/postprocess.py +155 -0
  31. gac/prompt.py +21 -479
  32. gac/prompt_builder.py +88 -0
  33. gac/providers/README.md +437 -0
  34. gac/providers/__init__.py +70 -78
  35. gac/providers/anthropic.py +12 -46
  36. gac/providers/azure_openai.py +48 -88
  37. gac/providers/base.py +329 -0
  38. gac/providers/cerebras.py +10 -33
  39. gac/providers/chutes.py +16 -62
  40. gac/providers/claude_code.py +64 -87
  41. gac/providers/custom_anthropic.py +51 -81
  42. gac/providers/custom_openai.py +29 -83
  43. gac/providers/deepseek.py +10 -33
  44. gac/providers/error_handler.py +139 -0
  45. gac/providers/fireworks.py +10 -33
  46. gac/providers/gemini.py +66 -63
  47. gac/providers/groq.py +10 -58
  48. gac/providers/kimi_coding.py +19 -55
  49. gac/providers/lmstudio.py +64 -43
  50. gac/providers/minimax.py +10 -33
  51. gac/providers/mistral.py +10 -33
  52. gac/providers/moonshot.py +10 -33
  53. gac/providers/ollama.py +56 -33
  54. gac/providers/openai.py +30 -36
  55. gac/providers/openrouter.py +15 -52
  56. gac/providers/protocol.py +71 -0
  57. gac/providers/qwen.py +64 -0
  58. gac/providers/registry.py +58 -0
  59. gac/providers/replicate.py +140 -82
  60. gac/providers/streamlake.py +26 -46
  61. gac/providers/synthetic.py +35 -37
  62. gac/providers/together.py +10 -33
  63. gac/providers/zai.py +29 -57
  64. gac/py.typed +0 -0
  65. gac/security.py +1 -1
  66. gac/templates/__init__.py +1 -0
  67. gac/templates/question_generation.txt +60 -0
  68. gac/templates/system_prompt.txt +224 -0
  69. gac/templates/user_prompt.txt +28 -0
  70. gac/utils.py +36 -6
  71. gac/workflow_context.py +162 -0
  72. gac/workflow_utils.py +3 -8
  73. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/METADATA +6 -4
  74. gac-3.10.10.dist-info/RECORD +79 -0
  75. gac/constants.py +0 -321
  76. gac-3.6.0.dist-info/RECORD +0 -53
  77. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/WHEEL +0 -0
  78. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/entry_points.txt +0 -0
  79. {gac-3.6.0.dist-info → gac-3.10.10.dist-info}/licenses/LICENSE +0 -0
gac/config.py CHANGED
@@ -5,13 +5,89 @@ Handles environment variable and .gac.env file precedence for application settin
5
5
 
6
6
  import os
7
7
  from pathlib import Path
8
+ from typing import TypedDict
8
9
 
9
10
  from dotenv import load_dotenv
10
11
 
11
12
  from gac.constants import EnvDefaults, Logging
13
+ from gac.errors import ConfigError
12
14
 
13
15
 
14
- def load_config() -> dict[str, str | int | float | bool | None]:
16
+ class GACConfig(TypedDict, total=False):
17
+ """TypedDict for GAC configuration values.
18
+
19
+ Fields that can be None or omitted are marked with total=False.
20
+ """
21
+
22
+ model: str | None
23
+ temperature: float
24
+ max_output_tokens: int
25
+ max_retries: int
26
+ log_level: str
27
+ warning_limit_tokens: int
28
+ always_include_scope: bool
29
+ skip_secret_scan: bool
30
+ no_tiktoken: bool
31
+ no_verify_ssl: bool
32
+ verbose: bool
33
+ system_prompt_path: str | None
34
+ language: str | None
35
+ translate_prefixes: bool
36
+ rtl_confirmed: bool
37
+ hook_timeout: int
38
+
39
+
40
+ def validate_config(config: GACConfig) -> None:
41
+ """Validate configuration values at load time.
42
+
43
+ Args:
44
+ config: Configuration dictionary to validate
45
+
46
+ Raises:
47
+ ConfigError: If any configuration value is invalid
48
+ """
49
+ # Validate temperature (0.0 to 2.0)
50
+ if config.get("temperature") is not None:
51
+ temp = config["temperature"]
52
+ if not isinstance(temp, (int, float)):
53
+ raise ConfigError(f"temperature must be a number, got {type(temp).__name__}")
54
+ if not 0.0 <= temp <= 2.0:
55
+ raise ConfigError(f"temperature must be between 0.0 and 2.0, got {temp}")
56
+
57
+ # Validate max_output_tokens (1 to 100000)
58
+ if config.get("max_output_tokens") is not None:
59
+ tokens = config["max_output_tokens"]
60
+ if not isinstance(tokens, int):
61
+ raise ConfigError(f"max_output_tokens must be an integer, got {type(tokens).__name__}")
62
+ if tokens < 1 or tokens > 100000:
63
+ raise ConfigError(f"max_output_tokens must be between 1 and 100000, got {tokens}")
64
+
65
+ # Validate max_retries (1 to 10)
66
+ if config.get("max_retries") is not None:
67
+ retries = config["max_retries"]
68
+ if not isinstance(retries, int):
69
+ raise ConfigError(f"max_retries must be an integer, got {type(retries).__name__}")
70
+ if retries < 1 or retries > 10:
71
+ raise ConfigError(f"max_retries must be between 1 and 10, got {retries}")
72
+
73
+ # Validate warning_limit_tokens (must be positive)
74
+ if config.get("warning_limit_tokens") is not None:
75
+ warning_limit = config["warning_limit_tokens"]
76
+ if not isinstance(warning_limit, int):
77
+ raise ConfigError(f"warning_limit_tokens must be an integer, got {type(warning_limit).__name__}")
78
+ if warning_limit < 1:
79
+ raise ConfigError(f"warning_limit_tokens must be positive, got {warning_limit}")
80
+
81
+ # Validate hook_timeout (must be positive)
82
+ if config.get("hook_timeout") is not None:
83
+ hook_timeout = config["hook_timeout"]
84
+ if not isinstance(hook_timeout, int):
85
+ raise ConfigError(f"hook_timeout must be an integer, got {type(hook_timeout).__name__}")
86
+ if hook_timeout < 1:
87
+ raise ConfigError(f"hook_timeout must be positive, got {hook_timeout}")
88
+
89
+
90
+ def load_config() -> GACConfig:
15
91
  """Load configuration from $HOME/.gac.env, then ./.gac.env, then environment variables."""
16
92
  user_config = Path.home() / ".gac.env"
17
93
  if user_config.exists():
@@ -23,7 +99,7 @@ def load_config() -> dict[str, str | int | float | bool | None]:
23
99
  if project_gac_env.exists():
24
100
  load_dotenv(project_gac_env, override=True)
25
101
 
26
- config = {
102
+ config: GACConfig = {
27
103
  "model": os.getenv("GAC_MODEL"),
28
104
  "temperature": float(os.getenv("GAC_TEMPERATURE", EnvDefaults.TEMPERATURE)),
29
105
  "max_output_tokens": int(os.getenv("GAC_MAX_OUTPUT_TOKENS", EnvDefaults.MAX_OUTPUT_TOKENS)),
@@ -35,6 +111,8 @@ def load_config() -> dict[str, str | int | float | bool | None]:
35
111
  "skip_secret_scan": os.getenv("GAC_SKIP_SECRET_SCAN", str(EnvDefaults.SKIP_SECRET_SCAN)).lower()
36
112
  in ("true", "1", "yes", "on"),
37
113
  "no_tiktoken": os.getenv("GAC_NO_TIKTOKEN", str(EnvDefaults.NO_TIKTOKEN)).lower() in ("true", "1", "yes", "on"),
114
+ "no_verify_ssl": os.getenv("GAC_NO_VERIFY_SSL", str(EnvDefaults.NO_VERIFY_SSL)).lower()
115
+ in ("true", "1", "yes", "on"),
38
116
  "verbose": os.getenv("GAC_VERBOSE", str(EnvDefaults.VERBOSE)).lower() in ("true", "1", "yes", "on"),
39
117
  "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
40
118
  "language": os.getenv("GAC_LANGUAGE"),
@@ -43,4 +121,5 @@ def load_config() -> dict[str, str | int | float | bool | None]:
43
121
  "hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
44
122
  }
45
123
 
124
+ validate_config(config)
46
125
  return config
gac/config_cli.py CHANGED
@@ -10,7 +10,7 @@ GAC_ENV_PATH = Path.home() / ".gac.env"
10
10
 
11
11
 
12
12
  @click.group()
13
- def config():
13
+ def config() -> None:
14
14
  """Manage gac configuration."""
15
15
  pass
16
16
 
@@ -18,6 +18,8 @@ def config():
18
18
  @config.command()
19
19
  def show() -> None:
20
20
  """Show all current config values."""
21
+ from dotenv import dotenv_values
22
+
21
23
  project_env_path = Path(".gac.env")
22
24
  user_exists = GAC_ENV_PATH.exists()
23
25
  project_exists = project_env_path.exists()
@@ -29,9 +31,14 @@ def show() -> None:
29
31
 
30
32
  if user_exists:
31
33
  click.echo(f"User config ({GAC_ENV_PATH}):")
32
- with open(GAC_ENV_PATH, encoding="utf-8") as f:
33
- for line in f:
34
- click.echo(line.rstrip())
34
+ user_config = dotenv_values(str(GAC_ENV_PATH))
35
+ for key, value in sorted(user_config.items()):
36
+ if value is not None:
37
+ if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
38
+ display_value = "***hidden***"
39
+ else:
40
+ display_value = value
41
+ click.echo(f" {key}={display_value}")
35
42
  else:
36
43
  click.echo("No $HOME/.gac.env found.")
37
44
 
@@ -39,9 +46,14 @@ def show() -> None:
39
46
  if user_exists:
40
47
  click.echo("")
41
48
  click.echo("Project config (./.gac.env):")
42
- with open(project_env_path, encoding="utf-8") as f:
43
- for line in f:
44
- click.echo(line.rstrip())
49
+ project_config = dotenv_values(str(project_env_path))
50
+ for key, value in sorted(project_config.items()):
51
+ if value is not None:
52
+ if any(sensitive in key.lower() for sensitive in ["key", "token", "secret"]):
53
+ display_value = "***hidden***"
54
+ else:
55
+ display_value = value
56
+ click.echo(f" {key}={display_value}")
45
57
  click.echo("")
46
58
  click.echo("Note: Project-level .gac.env overrides $HOME/.gac.env values for any duplicated variables.")
47
59
  else:
@@ -0,0 +1,34 @@
1
+ """Constants for the Git Auto Commit (gac) project.
2
+
3
+ This package provides all constants used throughout gac, organized into
4
+ logical modules:
5
+
6
+ - defaults: Environment defaults, provider defaults, logging, and utility constants
7
+ - file_patterns: File pattern matching and importance weighting
8
+ - languages: Language code mappings for internationalization
9
+ - commit: Git file status and commit message constants
10
+
11
+ All constants are re-exported from this package for backward compatibility.
12
+ """
13
+
14
+ from gac.constants.commit import CommitMessageConstants, FileStatus
15
+ from gac.constants.defaults import EnvDefaults, Logging, ProviderDefaults, Utility
16
+ from gac.constants.file_patterns import CodePatternImportance, FilePatterns, FileTypeImportance
17
+ from gac.constants.languages import Languages
18
+
19
+ __all__ = [
20
+ # From defaults
21
+ "EnvDefaults",
22
+ "ProviderDefaults",
23
+ "Logging",
24
+ "Utility",
25
+ # From file_patterns
26
+ "FilePatterns",
27
+ "FileTypeImportance",
28
+ "CodePatternImportance",
29
+ # From languages
30
+ "Languages",
31
+ # From commit
32
+ "FileStatus",
33
+ "CommitMessageConstants",
34
+ ]
@@ -0,0 +1,63 @@
1
+ """Constants for git operations and commit message generation."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class FileStatus(Enum):
7
+ """File status for Git operations."""
8
+
9
+ MODIFIED = "M"
10
+ ADDED = "A"
11
+ DELETED = "D"
12
+ RENAMED = "R"
13
+ COPIED = "C"
14
+ UNTRACKED = "?"
15
+
16
+
17
+ class CommitMessageConstants:
18
+ """Constants for commit message generation and cleaning."""
19
+
20
+ # Conventional commit type prefixes
21
+ CONVENTIONAL_PREFIXES: list[str] = [
22
+ "feat",
23
+ "fix",
24
+ "docs",
25
+ "style",
26
+ "refactor",
27
+ "perf",
28
+ "test",
29
+ "build",
30
+ "ci",
31
+ "chore",
32
+ ]
33
+
34
+ # XML tags that may leak from prompt templates into AI responses
35
+ XML_TAGS_TO_REMOVE: list[str] = [
36
+ "<git-status>",
37
+ "</git-status>",
38
+ "<git_status>",
39
+ "</git_status>",
40
+ "<git-diff>",
41
+ "</git-diff>",
42
+ "<git_diff>",
43
+ "</git_diff>",
44
+ "<repository_context>",
45
+ "</repository_context>",
46
+ "<instructions>",
47
+ "</instructions>",
48
+ "<format>",
49
+ "</format>",
50
+ "<conventions>",
51
+ "</conventions>",
52
+ ]
53
+
54
+ # Indicators that mark the start of the actual commit message in AI responses
55
+ COMMIT_INDICATORS: list[str] = [
56
+ "# Your commit message:",
57
+ "Your commit message:",
58
+ "The commit message is:",
59
+ "Here's the commit message:",
60
+ "Commit message:",
61
+ "Final commit message:",
62
+ "# Commit Message",
63
+ ]
@@ -0,0 +1,40 @@
1
+ """Default values for environment variables and provider configurations."""
2
+
3
+ import os
4
+
5
+
6
+ class EnvDefaults:
7
+ """Default values for environment variables."""
8
+
9
+ MAX_RETRIES: int = 3
10
+ TEMPERATURE: float = 1
11
+ MAX_OUTPUT_TOKENS: int = 4096 # includes reasoning tokens
12
+ WARNING_LIMIT_TOKENS: int = 32768
13
+ ALWAYS_INCLUDE_SCOPE: bool = False
14
+ SKIP_SECRET_SCAN: bool = False
15
+ VERBOSE: bool = False
16
+ NO_TIKTOKEN: bool = False
17
+ NO_VERIFY_SSL: bool = False # Skip SSL certificate verification (for corporate proxies)
18
+ HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
19
+
20
+
21
+ class ProviderDefaults:
22
+ """Default values for provider configurations."""
23
+
24
+ HTTP_TIMEOUT: int = 120 # seconds - timeout for HTTP requests to LLM providers
25
+
26
+
27
+ class Logging:
28
+ """Logging configuration constants."""
29
+
30
+ DEFAULT_LEVEL: str = "WARNING"
31
+ LEVELS: list[str] = ["DEBUG", "INFO", "WARNING", "ERROR"]
32
+
33
+
34
+ class Utility:
35
+ """General utility constants."""
36
+
37
+ DEFAULT_ENCODING: str = "cl100k_base" # llm encoding
38
+ DEFAULT_DIFF_TOKEN_LIMIT: int = 15000 # Maximum tokens for diff processing
39
+ MAX_WORKERS: int = os.cpu_count() or 4 # Maximum number of parallel workers
40
+ MAX_DISPLAYED_SECRET_LENGTH: int = 50 # Maximum length for displaying secrets
@@ -0,0 +1,110 @@
1
+ """File pattern constants for identifying special file types and importance weighting."""
2
+
3
+
4
+ class FilePatterns:
5
+ """Patterns for identifying special file types."""
6
+
7
+ # Regex patterns to detect binary file changes in git diffs (e.g., images or other non-text files)
8
+ BINARY: list[str] = [
9
+ r"Binary files .* differ",
10
+ r"GIT binary patch",
11
+ ]
12
+
13
+ # Regex patterns to detect minified files in git diffs (e.g., JavaScript or CSS files)
14
+ MINIFIED_EXTENSIONS: list[str] = [
15
+ ".min.js",
16
+ ".min.css",
17
+ ".bundle.js",
18
+ ".bundle.css",
19
+ ".compressed.js",
20
+ ".compressed.css",
21
+ ".opt.js",
22
+ ".opt.css",
23
+ ]
24
+
25
+ # Regex patterns to detect build directories in git diffs (e.g., dist, build, vendor, etc.)
26
+ BUILD_DIRECTORIES: list[str] = [
27
+ "/dist/",
28
+ "/build/",
29
+ "/vendor/",
30
+ "/node_modules/",
31
+ "/assets/vendor/",
32
+ "/public/build/",
33
+ "/static/dist/",
34
+ ]
35
+
36
+
37
+ class FileTypeImportance:
38
+ """Importance multipliers for different file types."""
39
+
40
+ EXTENSIONS: dict[str, float] = {
41
+ # Programming languages
42
+ ".py": 5.0, # Python
43
+ ".js": 4.5, # JavaScript
44
+ ".ts": 4.5, # TypeScript
45
+ ".jsx": 4.8, # React JS
46
+ ".tsx": 4.8, # React TS
47
+ ".go": 4.5, # Go
48
+ ".rs": 4.5, # Rust
49
+ ".java": 4.2, # Java
50
+ ".c": 4.2, # C
51
+ ".h": 4.2, # C/C++ header
52
+ ".cpp": 4.2, # C++
53
+ ".rb": 4.2, # Ruby
54
+ ".php": 4.0, # PHP
55
+ ".scala": 4.0, # Scala
56
+ ".swift": 4.0, # Swift
57
+ ".kt": 4.0, # Kotlin
58
+ # Configuration
59
+ ".json": 3.5, # JSON config
60
+ ".yaml": 3.8, # YAML config
61
+ ".yml": 3.8, # YAML config
62
+ ".toml": 3.8, # TOML config
63
+ ".ini": 3.5, # INI config
64
+ ".env": 3.5, # Environment variables
65
+ # Documentation
66
+ ".md": 2.5, # Markdown (reduced to prioritize code changes)
67
+ ".rst": 2.5, # reStructuredText (reduced to prioritize code changes)
68
+ # Web
69
+ ".html": 3.5, # HTML
70
+ ".css": 3.5, # CSS
71
+ ".scss": 3.5, # SCSS
72
+ ".svg": 2.5, # SVG graphics
73
+ # Build & CI
74
+ "Dockerfile": 4.0, # Docker
75
+ ".github/workflows": 4.0, # GitHub Actions
76
+ "CMakeLists.txt": 3.8, # CMake
77
+ "Makefile": 3.8, # Make
78
+ "package.json": 4.2, # NPM package
79
+ "pyproject.toml": 4.2, # Python project
80
+ "requirements.txt": 4.0, # Python requirements
81
+ }
82
+
83
+
84
+ class CodePatternImportance:
85
+ """Importance multipliers for different code patterns."""
86
+
87
+ # Regex patterns to detect code structure changes in git diffs (e.g., class, function, import)
88
+ # Note: The patterns are prefixed with "+" to match only added and modified lines
89
+ PATTERNS: dict[str, float] = {
90
+ # Structure changes
91
+ r"\+\s*(class|interface|enum)\s+\w+": 1.8, # Class/interface/enum definitions
92
+ r"\+\s*(def|function|func)\s+\w+\s*\(": 1.5, # Function definitions
93
+ r"\+\s*(import|from .* import)": 1.3, # Imports
94
+ r"\+\s*(public|private|protected)\s+\w+": 1.2, # Access modifiers
95
+ # Configuration changes
96
+ r"\+\s*\"(dependencies|devDependencies)\"": 1.4, # Package dependencies
97
+ r"\+\s*version[\"\s:=]+[0-9.]+": 1.3, # Version changes
98
+ # Logic changes
99
+ r"\+\s*(if|else|elif|switch|case|for|while)[\s(]": 1.2, # Control structures
100
+ r"\+\s*(try|catch|except|finally)[\s:]": 1.2, # Exception handling
101
+ r"\+\s*return\s+": 1.1, # Return statements
102
+ r"\+\s*await\s+": 1.1, # Async/await
103
+ # Comments & docs
104
+ r"\+\s*(//|#|/\*|\*\*)\s*TODO": 1.2, # TODOs
105
+ r"\+\s*(//|#|/\*|\*\*)\s*FIX": 1.3, # FIXes
106
+ r"\+\s*(\"\"\"|\'\'\')": 1.1, # Docstrings
107
+ # Test code
108
+ r"\+\s*(test|describe|it|should)\s*\(": 1.1, # Test definitions
109
+ r"\+\s*(assert|expect)": 1.0, # Assertions
110
+ }
@@ -0,0 +1,119 @@
1
+ """Language code mappings and utilities for internationalization."""
2
+
3
+
4
+ class Languages:
5
+ """Language code mappings and utilities."""
6
+
7
+ # Language code to full name mapping
8
+ # Supports ISO 639-1 codes and common variants
9
+ CODE_MAP: dict[str, str] = {
10
+ # English
11
+ "en": "English",
12
+ # Chinese
13
+ "zh": "Simplified Chinese",
14
+ "zh-cn": "Simplified Chinese",
15
+ "zh-hans": "Simplified Chinese",
16
+ "zh-tw": "Traditional Chinese",
17
+ "zh-hant": "Traditional Chinese",
18
+ # Japanese
19
+ "ja": "Japanese",
20
+ # Korean
21
+ "ko": "Korean",
22
+ # Spanish
23
+ "es": "Spanish",
24
+ # Portuguese
25
+ "pt": "Portuguese",
26
+ # French
27
+ "fr": "French",
28
+ # German
29
+ "de": "German",
30
+ # Russian
31
+ "ru": "Russian",
32
+ # Hindi
33
+ "hi": "Hindi",
34
+ # Italian
35
+ "it": "Italian",
36
+ # Polish
37
+ "pl": "Polish",
38
+ # Turkish
39
+ "tr": "Turkish",
40
+ # Dutch
41
+ "nl": "Dutch",
42
+ # Vietnamese
43
+ "vi": "Vietnamese",
44
+ # Thai
45
+ "th": "Thai",
46
+ # Indonesian
47
+ "id": "Indonesian",
48
+ # Swedish
49
+ "sv": "Swedish",
50
+ # Arabic
51
+ "ar": "Arabic",
52
+ # Hebrew
53
+ "he": "Hebrew",
54
+ # Greek
55
+ "el": "Greek",
56
+ # Danish
57
+ "da": "Danish",
58
+ # Norwegian
59
+ "no": "Norwegian",
60
+ "nb": "Norwegian",
61
+ "nn": "Norwegian",
62
+ # Finnish
63
+ "fi": "Finnish",
64
+ }
65
+
66
+ # List of languages with display names and English names for CLI selection
67
+ # Format: (display_name, english_name)
68
+ LANGUAGES: list[tuple[str, str]] = [
69
+ ("English", "English"),
70
+ ("简体中文", "Simplified Chinese"),
71
+ ("繁體中文", "Traditional Chinese"),
72
+ ("日本語", "Japanese"),
73
+ ("한국어", "Korean"),
74
+ ("Español", "Spanish"),
75
+ ("Português", "Portuguese"),
76
+ ("Français", "French"),
77
+ ("Deutsch", "German"),
78
+ ("Русский", "Russian"),
79
+ ("हिन्दी", "Hindi"),
80
+ ("Italiano", "Italian"),
81
+ ("Polski", "Polish"),
82
+ ("Türkçe", "Turkish"),
83
+ ("Nederlands", "Dutch"),
84
+ ("Tiếng Việt", "Vietnamese"),
85
+ ("ไทย", "Thai"),
86
+ ("Bahasa Indonesia", "Indonesian"),
87
+ ("Svenska", "Swedish"),
88
+ ("العربية", "Arabic"),
89
+ ("עברית", "Hebrew"),
90
+ ("Ελληνικά", "Greek"),
91
+ ("Dansk", "Danish"),
92
+ ("Norsk", "Norwegian"),
93
+ ("Suomi", "Finnish"),
94
+ ("Custom", "Custom"),
95
+ ]
96
+
97
+ @staticmethod
98
+ def resolve_code(language: str) -> str:
99
+ """Resolve a language code to its full name.
100
+
101
+ Args:
102
+ language: Language name or code (e.g., 'Spanish', 'es', 'zh-CN')
103
+
104
+ Returns:
105
+ Full language name (e.g., 'Spanish', 'Simplified Chinese')
106
+
107
+ If the input is already a full language name, it's returned as-is.
108
+ If it's a recognized code, it's converted to the full name.
109
+ Otherwise, the input is returned unchanged (for custom languages).
110
+ """
111
+ # Normalize the code to lowercase for lookup
112
+ code_lower = language.lower().strip()
113
+
114
+ # Check if it's a recognized code
115
+ if code_lower in Languages.CODE_MAP:
116
+ return Languages.CODE_MAP[code_lower]
117
+
118
+ # Return as-is (could be a full name or custom language)
119
+ return language
gac/diff_cli.py CHANGED
@@ -157,25 +157,3 @@ def diff(
157
157
  commit1=commit1,
158
158
  commit2=commit2,
159
159
  )
160
-
161
-
162
- # Function for testing only
163
- def _callback_for_testing(
164
- filter: bool,
165
- truncate: bool,
166
- max_tokens: int | None,
167
- staged: bool,
168
- color: bool,
169
- commit1: str | None = None,
170
- commit2: str | None = None,
171
- ) -> None:
172
- """A version of the diff command callback that can be called directly from tests."""
173
- _diff_implementation(
174
- filter=filter,
175
- truncate=truncate,
176
- max_tokens=max_tokens,
177
- staged=staged,
178
- color=color,
179
- commit1=commit1,
180
- commit2=commit2,
181
- )
gac/errors.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  import sys
5
5
  from collections.abc import Callable
6
- from typing import TypeVar
6
+ from typing import Any, TypeVar
7
7
 
8
8
  from rich.console import Console
9
9
 
@@ -113,6 +113,12 @@ class SecurityError(GacError):
113
113
  exit_code = 6
114
114
 
115
115
 
116
+ class HookError(GacError):
117
+ """Error when pre-commit or lefthook hooks fail."""
118
+
119
+ exit_code = 1
120
+
121
+
116
122
  # Simplified error hierarchy - we use a single AIError class with error codes
117
123
  # instead of multiple subclasses for better maintainability
118
124
 
@@ -216,7 +222,7 @@ def with_error_handling(
216
222
  """
217
223
 
218
224
  def decorator(func: Callable[..., T]) -> Callable[..., T | None]:
219
- def wrapper(*args, **kwargs) -> T | None:
225
+ def wrapper(*args: Any, **kwargs: Any) -> T | None:
220
226
  try:
221
227
  return func(*args, **kwargs)
222
228
  except Exception as e:
gac/git.py CHANGED
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
16
16
 
17
17
  def run_subprocess_with_encoding_fallback(
18
18
  command: list[str], silent: bool = False, timeout: int = 60
19
- ) -> subprocess.CompletedProcess:
19
+ ) -> subprocess.CompletedProcess[str]:
20
20
  """Run subprocess with encoding fallback, returning full CompletedProcess object.
21
21
 
22
22
  This is used for cases where we need both stdout and stderr separately,
@@ -57,7 +57,7 @@ def run_subprocess_with_encoding_fallback(
57
57
  continue
58
58
  except subprocess.TimeoutExpired:
59
59
  raise
60
- except Exception as e:
60
+ except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
61
61
  if not silent:
62
62
  logger.debug(f"Command error: {e}")
63
63
  # Try next encoding for non-timeout errors
@@ -182,7 +182,7 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
182
182
 
183
183
  output = run_git_command(args)
184
184
  return output
185
- except Exception as e:
185
+ except (subprocess.SubprocessError, OSError, FileNotFoundError) as e:
186
186
  logger.error(f"Failed to get diff: {str(e)}")
187
187
  raise GitError(f"Failed to get diff: {str(e)}") from e
188
188
 
@@ -246,7 +246,7 @@ def run_pre_commit_hooks(hook_timeout: int = 120) -> bool:
246
246
  else:
247
247
  logger.error(f"Pre-commit hooks failed with exit code {result.returncode}")
248
248
  return False
249
- except Exception as e:
249
+ except (subprocess.SubprocessError, OSError, FileNotFoundError, PermissionError) as e:
250
250
  logger.debug(f"Error running pre-commit: {e}")
251
251
  # If pre-commit isn't available, don't block the commit
252
252
  return True
@@ -296,7 +296,7 @@ def run_lefthook_hooks(hook_timeout: int = 120) -> bool:
296
296
  else:
297
297
  logger.error(f"Lefthook hooks failed with exit code {result.returncode}")
298
298
  return False
299
- except Exception as e:
299
+ except (subprocess.SubprocessError, OSError, FileNotFoundError, PermissionError) as e:
300
300
  logger.debug(f"Error running Lefthook: {e}")
301
301
  # If lefthook isn't available, don't block the commit
302
302
  return True
@@ -320,7 +320,7 @@ def push_changes() -> bool:
320
320
  else:
321
321
  logger.error(f"Failed to push changes: {error_msg}")
322
322
  return False
323
- except Exception as e:
323
+ except (subprocess.SubprocessError, OSError, ConnectionError) as e:
324
324
  logger.error(f"Failed to push changes: {e}")
325
325
  return False
326
326