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/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-3-5-haiku-latest')."
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
- logger.error(str(e))
713
- console.print(f"[red]Failed to generate commit message: {str(e)}[/red]")
714
- sys.exit(1)
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."""