gac 2.3.0__py3-none-any.whl → 2.7.0__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.
Potentially problematic release.
This version of gac might be problematic. Click here for more details.
- gac/__version__.py +1 -1
- gac/ai.py +4 -2
- gac/ai_utils.py +1 -0
- gac/auth_cli.py +69 -0
- gac/cli.py +14 -1
- gac/config.py +2 -0
- gac/constants.py +1 -0
- gac/git.py +69 -12
- gac/init_cli.py +175 -19
- gac/language_cli.py +170 -2
- gac/main.py +57 -8
- gac/oauth/__init__.py +1 -0
- gac/oauth/claude_code.py +397 -0
- gac/providers/__init__.py +2 -0
- gac/providers/claude_code.py +102 -0
- gac/providers/custom_anthropic.py +1 -1
- gac/utils.py +104 -3
- gac/workflow_utils.py +5 -2
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/METADATA +29 -10
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/RECORD +23 -19
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/WHEEL +0 -0
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/entry_points.txt +0 -0
- {gac-2.3.0.dist-info → gac-2.7.0.dist-info}/licenses/LICENSE +0 -0
gac/language_cli.py
CHANGED
|
@@ -1,16 +1,165 @@
|
|
|
1
1
|
"""CLI for selecting commit message language interactively."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import unicodedata
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
import click
|
|
6
8
|
import questionary
|
|
7
|
-
from dotenv import set_key
|
|
9
|
+
from dotenv import load_dotenv, set_key
|
|
8
10
|
|
|
9
11
|
from gac.constants import Languages
|
|
10
12
|
|
|
11
13
|
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
def should_show_rtl_warning() -> bool:
|
|
17
|
+
"""Check if RTL warning should be shown based on saved preference.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if warning should be shown, False if user previously confirmed
|
|
21
|
+
"""
|
|
22
|
+
# Load the current config to check RTL confirmation
|
|
23
|
+
if GAC_ENV_PATH.exists():
|
|
24
|
+
load_dotenv(GAC_ENV_PATH)
|
|
25
|
+
rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
|
|
26
|
+
return not rtl_confirmed
|
|
27
|
+
return True # Show warning if no config exists
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_rtl_text(text: str) -> bool:
|
|
31
|
+
"""Detect if text contains RTL characters or is a known RTL language.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
text: Text to analyze
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if text contains RTL script characters or is RTL language
|
|
38
|
+
"""
|
|
39
|
+
# Known RTL language names (case insensitive)
|
|
40
|
+
rtl_languages = {
|
|
41
|
+
"arabic",
|
|
42
|
+
"ar",
|
|
43
|
+
"العربية",
|
|
44
|
+
"hebrew",
|
|
45
|
+
"he",
|
|
46
|
+
"עברית",
|
|
47
|
+
"persian",
|
|
48
|
+
"farsi",
|
|
49
|
+
"fa",
|
|
50
|
+
"urdu",
|
|
51
|
+
"ur",
|
|
52
|
+
"اردو",
|
|
53
|
+
"pashto",
|
|
54
|
+
"ps",
|
|
55
|
+
"kurdish",
|
|
56
|
+
"ku",
|
|
57
|
+
"کوردی",
|
|
58
|
+
"yiddish",
|
|
59
|
+
"yi",
|
|
60
|
+
"ייִדיש",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Check if it's a known RTL language name or code (case insensitive)
|
|
64
|
+
if text.lower().strip() in rtl_languages:
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
rtl_scripts = {"Arabic", "Hebrew", "Thaana", "Nko", "Syriac", "Mandeic", "Samaritan", "Mongolian", "Phags-Pa"}
|
|
68
|
+
|
|
69
|
+
for char in text:
|
|
70
|
+
if unicodedata.name(char, "").startswith(("ARABIC", "HEBREW")):
|
|
71
|
+
return True
|
|
72
|
+
script = unicodedata.name(char, "").split()[0] if unicodedata.name(char, "") else ""
|
|
73
|
+
if script in rtl_scripts:
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def center_text(text: str, width: int = 80) -> str:
|
|
79
|
+
"""Center text within specified width, handling display width properly.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
text: Text to center
|
|
83
|
+
width: Terminal width to center within (default 80)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Centered text with proper padding
|
|
87
|
+
"""
|
|
88
|
+
import unicodedata
|
|
89
|
+
|
|
90
|
+
def get_display_width(s: str) -> int:
|
|
91
|
+
"""Get the display width of a string, accounting for wide characters."""
|
|
92
|
+
width = 0
|
|
93
|
+
for char in s:
|
|
94
|
+
# East Asian characters are typically 2 columns wide
|
|
95
|
+
if unicodedata.east_asian_width(char) in ("W", "F"):
|
|
96
|
+
width += 2
|
|
97
|
+
else:
|
|
98
|
+
width += 1
|
|
99
|
+
return width
|
|
100
|
+
|
|
101
|
+
# Handle multi-line text
|
|
102
|
+
lines = text.split("\n")
|
|
103
|
+
centered_lines = []
|
|
104
|
+
|
|
105
|
+
for line in lines:
|
|
106
|
+
# Strip existing whitespace to avoid double padding
|
|
107
|
+
stripped_line = line.strip()
|
|
108
|
+
if stripped_line:
|
|
109
|
+
# Calculate padding using display width for accurate centering
|
|
110
|
+
display_width = get_display_width(stripped_line)
|
|
111
|
+
padding = max(0, (width - display_width) // 2)
|
|
112
|
+
centered_line = " " * padding + stripped_line
|
|
113
|
+
centered_lines.append(centered_line)
|
|
114
|
+
else:
|
|
115
|
+
centered_lines.append("")
|
|
116
|
+
|
|
117
|
+
return "\n".join(centered_lines)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_terminal_width() -> int:
|
|
121
|
+
"""Get the current terminal width.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Terminal width in characters, or default if can't be determined
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
import shutil
|
|
128
|
+
|
|
129
|
+
return shutil.get_terminal_size().columns
|
|
130
|
+
except (OSError, AttributeError):
|
|
131
|
+
return 80 # Fallback to 80 columns
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def show_rtl_warning(language_name: str) -> bool:
|
|
135
|
+
"""Show RTL language warning and ask for confirmation.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
language_name: Name of the RTL language
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if user wants to proceed, False if they cancel
|
|
142
|
+
"""
|
|
143
|
+
terminal_width = get_terminal_width()
|
|
144
|
+
|
|
145
|
+
# Center just the title
|
|
146
|
+
title = center_text("⚠️ RTL Language Detected", terminal_width)
|
|
147
|
+
|
|
148
|
+
click.echo()
|
|
149
|
+
click.echo(click.style(title, fg="yellow", bold=True))
|
|
150
|
+
click.echo()
|
|
151
|
+
click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
|
|
152
|
+
click.echo("However, the commit messages will work fine and should be readable in Git clients")
|
|
153
|
+
click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
|
|
154
|
+
|
|
155
|
+
proceed = questionary.confirm("Do you want to proceed anyway?").ask()
|
|
156
|
+
if proceed:
|
|
157
|
+
# Remember that user has confirmed RTL acceptance
|
|
158
|
+
set_key(str(GAC_ENV_PATH), "GAC_RTL_CONFIRMED", "true")
|
|
159
|
+
click.echo("✓ RTL preference saved - you won't see this warning again")
|
|
160
|
+
return proceed if proceed is not None else False
|
|
161
|
+
|
|
162
|
+
|
|
14
163
|
@click.command()
|
|
15
164
|
def language() -> None:
|
|
16
165
|
"""Set the language for commit messages interactively."""
|
|
@@ -18,7 +167,7 @@ def language() -> None:
|
|
|
18
167
|
|
|
19
168
|
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
20
169
|
selection = questionary.select(
|
|
21
|
-
"Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
|
|
170
|
+
"Choose your language:", choices=display_names, use_shortcuts=True, use_arrow_keys=True, use_jk_keys=False
|
|
22
171
|
).ask()
|
|
23
172
|
|
|
24
173
|
if not selection:
|
|
@@ -47,10 +196,29 @@ def language() -> None:
|
|
|
47
196
|
click.echo("No language entered. Cancelled.")
|
|
48
197
|
return
|
|
49
198
|
language_value = custom_language.strip()
|
|
199
|
+
|
|
200
|
+
# Check if the custom language appears to be RTL
|
|
201
|
+
if is_rtl_text(language_value):
|
|
202
|
+
if not should_show_rtl_warning():
|
|
203
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
204
|
+
else:
|
|
205
|
+
if not show_rtl_warning(language_value):
|
|
206
|
+
click.echo("Language selection cancelled.")
|
|
207
|
+
return
|
|
208
|
+
|
|
50
209
|
else:
|
|
51
210
|
# Find the English name for the selected language
|
|
52
211
|
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == selection)
|
|
53
212
|
|
|
213
|
+
# Check if predefined language is RTL
|
|
214
|
+
if is_rtl_text(language_value):
|
|
215
|
+
if not should_show_rtl_warning():
|
|
216
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
217
|
+
else:
|
|
218
|
+
if not show_rtl_warning(language_value):
|
|
219
|
+
click.echo("Language selection cancelled.")
|
|
220
|
+
return
|
|
221
|
+
|
|
54
222
|
# Ask about prefix translation
|
|
55
223
|
click.echo() # Blank line for spacing
|
|
56
224
|
prefix_choice = questionary.select(
|
gac/main.py
CHANGED
|
@@ -87,7 +87,7 @@ def _parse_model_identifier(model: str) -> tuple[str, str]:
|
|
|
87
87
|
if not provider or not model_name:
|
|
88
88
|
message = (
|
|
89
89
|
f"Invalid model format: '{model}'. Both provider and model name are required "
|
|
90
|
-
"(example: 'anthropic:claude-
|
|
90
|
+
"(example: 'anthropic:claude-haiku-4-5')."
|
|
91
91
|
)
|
|
92
92
|
logger.error(message)
|
|
93
93
|
console.print(f"[red]{message}[/red]")
|
|
@@ -134,6 +134,7 @@ def execute_grouped_commits_workflow(
|
|
|
134
134
|
dry_run: bool,
|
|
135
135
|
push: bool,
|
|
136
136
|
show_prompt: bool,
|
|
137
|
+
hook_timeout: int = 120,
|
|
137
138
|
) -> None:
|
|
138
139
|
"""Execute the grouped commits workflow."""
|
|
139
140
|
import json
|
|
@@ -339,7 +340,7 @@ def execute_grouped_commits_workflow(
|
|
|
339
340
|
try:
|
|
340
341
|
for file_path in commit["files"]:
|
|
341
342
|
run_git_command(["add", "-A", file_path])
|
|
342
|
-
execute_commit(commit["message"], no_verify)
|
|
343
|
+
execute_commit(commit["message"], no_verify, hook_timeout)
|
|
343
344
|
console.print(f"[green]✓ Commit {idx}/{num_commits} created[/green]")
|
|
344
345
|
except Exception as e:
|
|
345
346
|
console.print(f"[red]✗ Failed at commit {idx}/{num_commits}: {e}[/red]")
|
|
@@ -389,6 +390,7 @@ def execute_single_commit_workflow(
|
|
|
389
390
|
dry_run: bool,
|
|
390
391
|
push: bool,
|
|
391
392
|
show_prompt: bool,
|
|
393
|
+
hook_timeout: int = 120,
|
|
392
394
|
) -> None:
|
|
393
395
|
if show_prompt:
|
|
394
396
|
full_prompt = f"SYSTEM PROMPT:\n{system_prompt}\n\nUSER PROMPT:\n{user_prompt}"
|
|
@@ -446,7 +448,7 @@ def execute_single_commit_workflow(
|
|
|
446
448
|
console.print(f"Would commit {len(staged_files)} files")
|
|
447
449
|
logger.info(f"Would commit {len(staged_files)} files")
|
|
448
450
|
else:
|
|
449
|
-
execute_commit(commit_message, no_verify)
|
|
451
|
+
execute_commit(commit_message, no_verify, hook_timeout)
|
|
450
452
|
|
|
451
453
|
if push:
|
|
452
454
|
try:
|
|
@@ -497,6 +499,7 @@ def main(
|
|
|
497
499
|
no_verify: bool = False,
|
|
498
500
|
skip_secret_scan: bool = False,
|
|
499
501
|
language: str | None = None,
|
|
502
|
+
hook_timeout: int = 120,
|
|
500
503
|
) -> None:
|
|
501
504
|
"""Main application logic for gac."""
|
|
502
505
|
try:
|
|
@@ -549,12 +552,12 @@ def main(
|
|
|
549
552
|
sys.exit(0)
|
|
550
553
|
|
|
551
554
|
if not no_verify and not dry_run:
|
|
552
|
-
if not run_lefthook_hooks():
|
|
555
|
+
if not run_lefthook_hooks(hook_timeout):
|
|
553
556
|
console.print("[red]Lefthook hooks failed. Please fix the issues and try again.[/red]")
|
|
554
557
|
console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
|
|
555
558
|
sys.exit(1)
|
|
556
559
|
|
|
557
|
-
if not run_pre_commit_hooks():
|
|
560
|
+
if not run_pre_commit_hooks(hook_timeout):
|
|
558
561
|
console.print("[red]Pre-commit hooks failed. Please fix the issues and try again.[/red]")
|
|
559
562
|
console.print("[yellow]You can use --no-verify to skip pre-commit and lefthook hooks.[/yellow]")
|
|
560
563
|
sys.exit(1)
|
|
@@ -687,6 +690,7 @@ def main(
|
|
|
687
690
|
dry_run=dry_run,
|
|
688
691
|
push=push,
|
|
689
692
|
show_prompt=show_prompt,
|
|
693
|
+
hook_timeout=hook_timeout,
|
|
690
694
|
)
|
|
691
695
|
except AIError as e:
|
|
692
696
|
logger.error(str(e))
|
|
@@ -707,11 +711,56 @@ def main(
|
|
|
707
711
|
dry_run=dry_run,
|
|
708
712
|
push=push,
|
|
709
713
|
show_prompt=show_prompt,
|
|
714
|
+
hook_timeout=hook_timeout,
|
|
710
715
|
)
|
|
711
716
|
except AIError as e:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
# Check if this is a Claude Code OAuth token expiration
|
|
718
|
+
if (
|
|
719
|
+
e.error_type == "authentication"
|
|
720
|
+
and model.startswith("claude-code:")
|
|
721
|
+
and ("expired" in str(e).lower() or "oauth" in str(e).lower())
|
|
722
|
+
):
|
|
723
|
+
logger.error(str(e))
|
|
724
|
+
console.print("[yellow]⚠ Claude Code OAuth token has expired[/yellow]")
|
|
725
|
+
console.print("[cyan]🔐 Starting automatic re-authentication...[/cyan]")
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
from gac.oauth.claude_code import authenticate_and_save
|
|
729
|
+
|
|
730
|
+
if authenticate_and_save(quiet=quiet):
|
|
731
|
+
console.print("[green]✓ Re-authentication successful![/green]")
|
|
732
|
+
console.print("[cyan]Retrying commit...[/cyan]\n")
|
|
733
|
+
|
|
734
|
+
# Retry the commit workflow
|
|
735
|
+
execute_single_commit_workflow(
|
|
736
|
+
system_prompt=system_prompt,
|
|
737
|
+
user_prompt=user_prompt,
|
|
738
|
+
model=model,
|
|
739
|
+
temperature=temperature,
|
|
740
|
+
max_output_tokens=max_output_tokens,
|
|
741
|
+
max_retries=max_retries,
|
|
742
|
+
require_confirmation=require_confirmation,
|
|
743
|
+
quiet=quiet,
|
|
744
|
+
no_verify=no_verify,
|
|
745
|
+
dry_run=dry_run,
|
|
746
|
+
push=push,
|
|
747
|
+
show_prompt=show_prompt,
|
|
748
|
+
hook_timeout=hook_timeout,
|
|
749
|
+
)
|
|
750
|
+
return # Success!
|
|
751
|
+
else:
|
|
752
|
+
console.print("[red]Re-authentication failed.[/red]")
|
|
753
|
+
console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
|
|
754
|
+
sys.exit(1)
|
|
755
|
+
except Exception as auth_error:
|
|
756
|
+
console.print(f"[red]Re-authentication error: {auth_error}[/red]")
|
|
757
|
+
console.print("[yellow]Run 'gac model' to re-authenticate manually.[/yellow]")
|
|
758
|
+
sys.exit(1)
|
|
759
|
+
else:
|
|
760
|
+
# Non-Claude Code error or non-auth error
|
|
761
|
+
logger.error(str(e))
|
|
762
|
+
console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
|
|
763
|
+
sys.exit(1)
|
|
715
764
|
|
|
716
765
|
|
|
717
766
|
if __name__ == "__main__":
|
gac/oauth/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OAuth authentication utilities for GAC."""
|