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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.7.0"
gac/ai.py CHANGED
@@ -13,6 +13,7 @@ from gac.providers import (
13
13
  call_anthropic_api,
14
14
  call_cerebras_api,
15
15
  call_chutes_api,
16
+ call_claude_code_api,
16
17
  call_custom_anthropic_api,
17
18
  call_custom_openai_api,
18
19
  call_deepseek_api,
@@ -48,7 +49,7 @@ def generate_commit_message(
48
49
  """Generate a commit message using direct API calls to AI providers.
49
50
 
50
51
  Args:
51
- model: The model to use in provider:model_name format (e.g., 'anthropic:claude-3-5-haiku-latest')
52
+ model: The model to use in provider:model_name format (e.g., 'anthropic:claude-haiku-4-5')
52
53
  prompt: Either a string prompt (for backward compatibility) or tuple of (system_prompt, user_prompt)
53
54
  temperature: Controls randomness (0.0-1.0), lower values are more deterministic
54
55
  max_tokens: Maximum tokens in the response
@@ -62,7 +63,7 @@ def generate_commit_message(
62
63
  AIError: If generation fails after max_retries attempts
63
64
 
64
65
  Example:
65
- >>> model = "anthropic:claude-3-5-haiku-latest"
66
+ >>> model = "anthropic:claude-haiku-4-5"
66
67
  >>> system_prompt, user_prompt = build_prompt("On branch main", "diff --git a/README.md b/README.md")
67
68
  >>> generate_commit_message(model, (system_prompt, user_prompt))
68
69
  'docs: Update README with installation instructions'
@@ -88,6 +89,7 @@ def generate_commit_message(
88
89
  provider_funcs = {
89
90
  "anthropic": call_anthropic_api,
90
91
  "cerebras": call_cerebras_api,
92
+ "claude-code": call_claude_code_api,
91
93
  "chutes": call_chutes_api,
92
94
  "custom-anthropic": call_custom_anthropic_api,
93
95
  "custom-openai": call_custom_openai_api,
gac/ai_utils.py CHANGED
@@ -98,6 +98,7 @@ def generate_with_retries(
98
98
  "anthropic",
99
99
  "cerebras",
100
100
  "chutes",
101
+ "claude-code",
101
102
  "deepseek",
102
103
  "fireworks",
103
104
  "gemini",
gac/auth_cli.py ADDED
@@ -0,0 +1,69 @@
1
+ """CLI for authenticating Claude Code OAuth tokens.
2
+
3
+ Provides a command to authenticate and re-authenticate Claude Code subscriptions.
4
+ """
5
+
6
+ import logging
7
+
8
+ import click
9
+
10
+ from gac.oauth.claude_code import authenticate_and_save, load_stored_token
11
+ from gac.utils import setup_logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @click.command()
17
+ @click.option(
18
+ "--quiet",
19
+ "-q",
20
+ is_flag=True,
21
+ help="Suppress non-error output",
22
+ )
23
+ @click.option(
24
+ "--log-level",
25
+ default="INFO",
26
+ type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], case_sensitive=False),
27
+ help="Set log level (default: INFO)",
28
+ )
29
+ def auth(quiet: bool = False, log_level: str = "INFO") -> None:
30
+ """Authenticate Claude Code OAuth token.
31
+
32
+ This command allows you to authenticate or re-authenticate your
33
+ Claude Code OAuth token when it expires or you want to refresh it.
34
+ It opens a browser window for the OAuth flow and saves the token
35
+ to ~/.gac.env.
36
+
37
+ The token is used by the Claude Code provider to access your
38
+ Claude Code subscription instead of requiring an Anthropic API key.
39
+ """
40
+ # Setup logging
41
+ if quiet:
42
+ effective_log_level = "ERROR"
43
+ else:
44
+ effective_log_level = log_level
45
+ setup_logging(effective_log_level)
46
+
47
+ # Check if there's an existing token
48
+ existing_token = load_stored_token()
49
+ if existing_token and not quiet:
50
+ click.echo("✓ Found existing Claude Code access token.")
51
+ click.echo()
52
+
53
+ if not quiet:
54
+ click.echo("🔐 Starting Claude Code OAuth authentication...")
55
+ click.echo(" Your browser will open automatically")
56
+ click.echo(" (Waiting up to 3 minutes for callback)")
57
+ click.echo()
58
+
59
+ # Perform OAuth authentication
60
+ success = authenticate_and_save(quiet=quiet)
61
+
62
+ if success:
63
+ if not quiet:
64
+ click.echo("✅ Claude Code authentication completed successfully!")
65
+ click.echo(" Your new token has been saved and is ready to use.")
66
+ else:
67
+ click.echo("❌ Claude Code authentication failed.")
68
+ click.echo(" Please try again or check your network connection.")
69
+ raise click.ClickException("Claude Code authentication failed")
gac/cli.py CHANGED
@@ -11,12 +11,14 @@ import sys
11
11
  import click
12
12
 
13
13
  from gac import __version__
14
+ from gac.auth_cli import auth as auth_cli
14
15
  from gac.config import load_config
15
16
  from gac.config_cli import config as config_cli
16
17
  from gac.constants import Languages, Logging
17
18
  from gac.diff_cli import diff as diff_cli
18
19
  from gac.errors import handle_error
19
20
  from gac.init_cli import init as init_cli
21
+ from gac.init_cli import model as model_cli
20
22
  from gac.language_cli import language as language_cli
21
23
  from gac.main import main
22
24
  from gac.utils import setup_logging
@@ -65,6 +67,12 @@ logger = logging.getLogger(__name__)
65
67
  # Advanced options
66
68
  @click.option("--no-verify", is_flag=True, help="Skip pre-commit and lefthook hooks when committing")
67
69
  @click.option("--skip-secret-scan", is_flag=True, help="Skip security scan for secrets in staged changes")
70
+ @click.option(
71
+ "--hook-timeout",
72
+ type=int,
73
+ default=0,
74
+ help="Timeout for pre-commit and lefthook hooks in seconds (0 to use configuration)",
75
+ )
68
76
  # Other options
69
77
  @click.option("--version", is_flag=True, help="Show the version of the Git Auto Commit (gac) tool")
70
78
  @click.pass_context
@@ -87,6 +95,7 @@ def cli(
87
95
  verbose: bool = False,
88
96
  no_verify: bool = False,
89
97
  skip_secret_scan: bool = False,
98
+ hook_timeout: int = 0,
90
99
  ) -> None:
91
100
  """Git Auto Commit - Generate commit messages with AI."""
92
101
  if ctx.invoked_subcommand is None:
@@ -125,6 +134,7 @@ def cli(
125
134
  no_verify=no_verify,
126
135
  skip_secret_scan=skip_secret_scan or bool(config.get("skip_secret_scan", False)),
127
136
  language=resolved_language,
137
+ hook_timeout=hook_timeout if hook_timeout > 0 else int(config.get("hook_timeout", 120) or 120),
128
138
  )
129
139
  except Exception as e:
130
140
  handle_error(e, exit_program=True)
@@ -150,13 +160,16 @@ def cli(
150
160
  "verbose": verbose,
151
161
  "no_verify": no_verify,
152
162
  "skip_secret_scan": skip_secret_scan,
163
+ "hook_timeout": hook_timeout,
153
164
  }
154
165
 
155
166
 
167
+ cli.add_command(auth_cli)
156
168
  cli.add_command(config_cli)
169
+ cli.add_command(diff_cli)
157
170
  cli.add_command(init_cli)
158
171
  cli.add_command(language_cli)
159
- cli.add_command(diff_cli)
172
+ cli.add_command(model_cli)
160
173
 
161
174
 
162
175
  @click.command(context_settings=language_cli.context_settings)
gac/config.py CHANGED
@@ -41,6 +41,8 @@ def load_config() -> dict[str, str | int | float | bool | None]:
41
41
  "system_prompt_path": os.getenv("GAC_SYSTEM_PROMPT_PATH"),
42
42
  "language": os.getenv("GAC_LANGUAGE"),
43
43
  "translate_prefixes": os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on"),
44
+ "rtl_confirmed": os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on"),
45
+ "hook_timeout": int(os.getenv("GAC_HOOK_TIMEOUT", EnvDefaults.HOOK_TIMEOUT)),
44
46
  }
45
47
 
46
48
  return config
gac/constants.py CHANGED
@@ -25,6 +25,7 @@ class EnvDefaults:
25
25
  ALWAYS_INCLUDE_SCOPE: bool = False
26
26
  SKIP_SECRET_SCAN: bool = False
27
27
  VERBOSE: bool = False
28
+ HOOK_TIMEOUT: int = 120 # Timeout for pre-commit and lefthook hooks in seconds
28
29
 
29
30
 
30
31
  class Logging:
gac/git.py CHANGED
@@ -14,6 +14,63 @@ from gac.utils import run_subprocess
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
+ def run_subprocess_with_encoding_fallback(
18
+ command: list[str], silent: bool = False, timeout: int = 60
19
+ ) -> subprocess.CompletedProcess:
20
+ """Run subprocess with encoding fallback, returning full CompletedProcess object.
21
+
22
+ This is used for cases where we need both stdout and stderr separately,
23
+ like pre-commit and lefthook hook execution.
24
+
25
+ Args:
26
+ command: List of command arguments
27
+ silent: If True, suppress debug logging
28
+ timeout: Command timeout in seconds
29
+
30
+ Returns:
31
+ CompletedProcess object with stdout, stderr, and returncode
32
+ """
33
+ from gac.utils import get_safe_encodings
34
+
35
+ encodings = get_safe_encodings()
36
+ last_exception: Exception | None = None
37
+
38
+ for encoding in encodings:
39
+ try:
40
+ if not silent:
41
+ logger.debug(f"Running command: {' '.join(command)} (encoding: {encoding})")
42
+
43
+ result = subprocess.run(
44
+ command,
45
+ capture_output=True,
46
+ text=True,
47
+ check=False,
48
+ timeout=timeout,
49
+ encoding=encoding,
50
+ errors="replace",
51
+ )
52
+ return result
53
+ except UnicodeError as e:
54
+ last_exception = e
55
+ if not silent:
56
+ logger.debug(f"Failed to decode with {encoding}: {e}")
57
+ continue
58
+ except subprocess.TimeoutExpired:
59
+ raise
60
+ except Exception as e:
61
+ if not silent:
62
+ logger.debug(f"Command error: {e}")
63
+ # Try next encoding for non-timeout errors
64
+ last_exception = e
65
+ continue
66
+
67
+ # If we get here, all encodings failed
68
+ if last_exception:
69
+ raise subprocess.CalledProcessError(1, command, "", f"Encoding error: {last_exception}") from last_exception
70
+ else:
71
+ raise subprocess.CalledProcessError(1, command, "", "All encoding attempts failed")
72
+
73
+
17
74
  def run_git_command(args: list[str], silent: bool = False, timeout: int = 30) -> str:
18
75
  """Run a git command and return the output."""
19
76
  command = ["git"] + args
@@ -132,23 +189,23 @@ def get_diff(staged: bool = True, color: bool = True, commit1: str | None = None
132
189
 
133
190
  def get_repo_root() -> str:
134
191
  """Get absolute path of repository root."""
135
- result = subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
136
- return result.decode().strip()
192
+ result = run_git_command(["rev-parse", "--show-toplevel"])
193
+ return result
137
194
 
138
195
 
139
196
  def get_current_branch() -> str:
140
197
  """Get name of current git branch."""
141
- result = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
142
- return result.decode().strip()
198
+ result = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
199
+ return result
143
200
 
144
201
 
145
202
  def get_commit_hash() -> str:
146
203
  """Get SHA-1 hash of current commit."""
147
- result = subprocess.check_output(["git", "rev-parse", "HEAD"])
148
- return result.decode().strip()
204
+ result = run_git_command(["rev-parse", "HEAD"])
205
+ return result
149
206
 
150
207
 
151
- def run_pre_commit_hooks() -> bool:
208
+ def run_pre_commit_hooks(hook_timeout: int = 120) -> bool:
152
209
  """Run pre-commit hooks if they exist.
153
210
 
154
211
  Returns:
@@ -168,9 +225,9 @@ def run_pre_commit_hooks() -> bool:
168
225
  return True
169
226
 
170
227
  # Run pre-commit hooks on staged files
171
- logger.info("Running pre-commit hooks...")
228
+ logger.info(f"Running pre-commit hooks with {hook_timeout}s timeout...")
172
229
  # Run pre-commit and capture both stdout and stderr
173
- result = subprocess.run(["pre-commit", "run"], capture_output=True, text=True, check=False)
230
+ result = run_subprocess_with_encoding_fallback(["pre-commit", "run"], timeout=hook_timeout)
174
231
 
175
232
  if result.returncode == 0:
176
233
  # All hooks passed
@@ -195,7 +252,7 @@ def run_pre_commit_hooks() -> bool:
195
252
  return True
196
253
 
197
254
 
198
- def run_lefthook_hooks() -> bool:
255
+ def run_lefthook_hooks(hook_timeout: int = 120) -> bool:
199
256
  """Run Lefthook hooks if they exist.
200
257
 
201
258
  Returns:
@@ -218,9 +275,9 @@ def run_lefthook_hooks() -> bool:
218
275
  return True
219
276
 
220
277
  # Run lefthook hooks on staged files
221
- logger.info("Running Lefthook hooks...")
278
+ logger.info(f"Running Lefthook hooks with {hook_timeout}s timeout...")
222
279
  # Run lefthook and capture both stdout and stderr
223
- result = subprocess.run(["lefthook", "run", "pre-commit"], capture_output=True, text=True, check=False)
280
+ result = run_subprocess_with_encoding_fallback(["lefthook", "run", "pre-commit"], timeout=hook_timeout)
224
281
 
225
282
  if result.returncode == 0:
226
283
  # All hooks passed
gac/init_cli.py CHANGED
@@ -1,16 +1,58 @@
1
1
  """CLI for initializing gac configuration interactively."""
2
2
 
3
+ import os
3
4
  from pathlib import Path
4
5
 
5
6
  import click
6
7
  import questionary
7
- from dotenv import dotenv_values, set_key
8
+ from dotenv import dotenv_values, load_dotenv, set_key
8
9
 
9
10
  from gac.constants import Languages
10
11
 
11
12
  GAC_ENV_PATH = Path.home() / ".gac.env"
12
13
 
13
14
 
15
+ def _should_show_rtl_warning_for_init() -> bool:
16
+ """Check if RTL warning should be shown based on init's GAC_ENV_PATH.
17
+
18
+ Returns:
19
+ True if warning should be shown, False if user previously confirmed
20
+ """
21
+ if GAC_ENV_PATH.exists():
22
+ load_dotenv(GAC_ENV_PATH)
23
+ rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
24
+ return not rtl_confirmed
25
+ return True # Show warning if no config exists
26
+
27
+
28
+ def _show_rtl_warning_for_init(language_name: str) -> bool:
29
+ """Show RTL language warning for init command and save preference to GAC_ENV_PATH.
30
+
31
+ Args:
32
+ language_name: Name of the RTL language
33
+
34
+ Returns:
35
+ True if user wants to proceed, False if they cancel
36
+ """
37
+
38
+ terminal_width = 80 # Use default width
39
+ title = "⚠️ RTL Language Detected".center(terminal_width)
40
+
41
+ click.echo()
42
+ click.echo(click.style(title, fg="yellow", bold=True))
43
+ click.echo()
44
+ click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
45
+ click.echo("However, the commit messages will work fine and should be readable in Git clients")
46
+ click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
47
+
48
+ proceed = questionary.confirm("Do you want to proceed anyway?").ask()
49
+ if proceed:
50
+ # Remember that user has confirmed RTL acceptance
51
+ set_key(str(GAC_ENV_PATH), "GAC_RTL_CONFIRMED", "true")
52
+ click.echo("✓ RTL preference saved - you won't see this warning again")
53
+ return proceed if proceed is not None else False
54
+
55
+
14
56
  def _prompt_required_text(prompt: str) -> str | None:
15
57
  """Prompt until a non-empty string is provided or the user cancels."""
16
58
  while True:
@@ -23,24 +65,25 @@ def _prompt_required_text(prompt: str) -> str | None:
23
65
  click.echo("A value is required. Please try again.")
24
66
 
25
67
 
26
- @click.command()
27
- def init() -> None:
28
- """Interactively set up $HOME/.gac.env for gac."""
29
- click.echo("Welcome to gac initialization!\n")
30
-
31
- # Load existing environment values
32
- existing_env = {}
68
+ def _load_existing_env() -> dict[str, str]:
69
+ """Ensure the env file exists and return its current values."""
70
+ existing_env: dict[str, str] = {}
33
71
  if GAC_ENV_PATH.exists():
34
72
  click.echo(f"$HOME/.gac.env already exists at {GAC_ENV_PATH}. Values will be updated.")
35
- existing_env = dict(dotenv_values(str(GAC_ENV_PATH)))
73
+ existing_env = {k: v for k, v in dotenv_values(str(GAC_ENV_PATH)).items() if v is not None}
36
74
  else:
37
75
  GAC_ENV_PATH.touch()
38
76
  click.echo(f"Created $HOME/.gac.env at {GAC_ENV_PATH}.")
77
+ return existing_env
78
+
39
79
 
80
+ def _configure_model(existing_env: dict[str, str]) -> bool:
81
+ """Run the provider/model/API key configuration flow."""
40
82
  providers = [
41
83
  ("Anthropic", "claude-haiku-4-5"),
42
84
  ("Cerebras", "zai-glm-4.6"),
43
85
  ("Chutes", "zai-org/GLM-4.6-FP8"),
86
+ ("Claude Code", "claude-sonnet-4-5"),
44
87
  ("Custom (Anthropic)", ""),
45
88
  ("Custom (OpenAI)", ""),
46
89
  ("DeepSeek", "deepseek-chat"),
@@ -63,13 +106,14 @@ def init() -> None:
63
106
  provider = questionary.select("Select your provider:", choices=provider_names).ask()
64
107
  if not provider:
65
108
  click.echo("Provider selection cancelled. Exiting.")
66
- return
109
+ return False
67
110
  provider_key = provider.lower().replace(".", "").replace(" ", "-").replace("(", "").replace(")", "")
68
111
 
69
112
  is_ollama = provider_key == "ollama"
70
113
  is_lmstudio = provider_key == "lm-studio"
71
114
  is_streamlake = provider_key == "streamlake"
72
115
  is_zai = provider_key in ("zai", "zai-coding")
116
+ is_claude_code = provider_key == "claude-code"
73
117
  is_custom_anthropic = provider_key == "custom-anthropic"
74
118
  is_custom_openai = provider_key == "custom-openai"
75
119
 
@@ -77,7 +121,7 @@ def init() -> None:
77
121
  endpoint_id = _prompt_required_text("Enter the Streamlake inference endpoint ID (required):")
78
122
  if endpoint_id is None:
79
123
  click.echo("Streamlake configuration cancelled. Exiting.")
80
- return
124
+ return False
81
125
  model_to_save = endpoint_id
82
126
  else:
83
127
  model_suggestion = dict(providers)[provider]
@@ -88,7 +132,7 @@ def init() -> None:
88
132
  model = questionary.text(model_prompt, default=model_suggestion).ask()
89
133
  if model is None:
90
134
  click.echo("Model entry cancelled. Exiting.")
91
- return
135
+ return False
92
136
  model_to_save = model.strip() if model.strip() else model_suggestion
93
137
 
94
138
  set_key(str(GAC_ENV_PATH), "GAC_MODEL", f"{provider_key}:{model_to_save}")
@@ -98,7 +142,7 @@ def init() -> None:
98
142
  base_url = _prompt_required_text("Enter the custom Anthropic-compatible base URL (required):")
99
143
  if base_url is None:
100
144
  click.echo("Custom Anthropic base URL entry cancelled. Exiting.")
101
- return
145
+ return False
102
146
  set_key(str(GAC_ENV_PATH), "CUSTOM_ANTHROPIC_BASE_URL", base_url)
103
147
  click.echo(f"Set CUSTOM_ANTHROPIC_BASE_URL={base_url}")
104
148
 
@@ -112,7 +156,7 @@ def init() -> None:
112
156
  base_url = _prompt_required_text("Enter the custom OpenAI-compatible base URL (required):")
113
157
  if base_url is None:
114
158
  click.echo("Custom OpenAI base URL entry cancelled. Exiting.")
115
- return
159
+ return False
116
160
  set_key(str(GAC_ENV_PATH), "CUSTOM_OPENAI_BASE_URL", base_url)
117
161
  click.echo(f"Set CUSTOM_OPENAI_BASE_URL={base_url}")
118
162
  elif is_ollama:
@@ -120,7 +164,7 @@ def init() -> None:
120
164
  url = questionary.text(f"Enter the Ollama API URL (default: {url_default}):", default=url_default).ask()
121
165
  if url is None:
122
166
  click.echo("Ollama URL entry cancelled. Exiting.")
123
- return
167
+ return False
124
168
  url_to_save = url.strip() if url.strip() else url_default
125
169
  set_key(str(GAC_ENV_PATH), "OLLAMA_API_URL", url_to_save)
126
170
  click.echo(f"Set OLLAMA_API_URL={url_to_save}")
@@ -129,11 +173,46 @@ def init() -> None:
129
173
  url = questionary.text(f"Enter the LM Studio API URL (default: {url_default}):", default=url_default).ask()
130
174
  if url is None:
131
175
  click.echo("LM Studio URL entry cancelled. Exiting.")
132
- return
176
+ return False
133
177
  url_to_save = url.strip() if url.strip() else url_default
134
178
  set_key(str(GAC_ENV_PATH), "LMSTUDIO_API_URL", url_to_save)
135
179
  click.echo(f"Set LMSTUDIO_API_URL={url_to_save}")
136
180
 
181
+ # Handle Claude Code OAuth separately
182
+ if is_claude_code:
183
+ from gac.oauth.claude_code import authenticate_and_save, load_stored_token
184
+
185
+ existing_token = load_stored_token()
186
+ if existing_token:
187
+ click.echo("\n✓ Claude Code access token already configured.")
188
+ action = questionary.select(
189
+ "What would you like to do?",
190
+ choices=[
191
+ "Keep existing token",
192
+ "Re-authenticate (get new token)",
193
+ ],
194
+ ).ask()
195
+
196
+ if action is None or action.startswith("Keep existing"):
197
+ if action is None:
198
+ click.echo("Claude Code configuration cancelled. Keeping existing token.")
199
+ else:
200
+ click.echo("Keeping existing Claude Code token")
201
+ return True
202
+ else:
203
+ click.echo("\n🔐 Starting Claude Code OAuth authentication...")
204
+ if not authenticate_and_save(quiet=False):
205
+ click.echo("❌ Claude Code authentication failed. Keeping existing token.")
206
+ return False
207
+ return True
208
+ else:
209
+ click.echo("\n🔐 Starting Claude Code OAuth authentication...")
210
+ click.echo(" (Your browser will open automatically)\n")
211
+ if not authenticate_and_save(quiet=False):
212
+ click.echo("\n❌ Claude Code authentication failed. Exiting.")
213
+ return False
214
+ return True
215
+
137
216
  # Determine API key name based on provider
138
217
  if is_lmstudio:
139
218
  api_key_name = "LMSTUDIO_API_KEY"
@@ -189,7 +268,13 @@ def init() -> None:
189
268
  else:
190
269
  click.echo("No API key entered. You can add one later by editing ~/.gac.env")
191
270
 
192
- # Language selection
271
+ return True
272
+
273
+
274
+ def _configure_language(existing_env: dict[str, str]) -> None:
275
+ """Run the language configuration flow."""
276
+ from gac.language_cli import is_rtl_text
277
+
193
278
  click.echo("\n")
194
279
  existing_language = existing_env.get("GAC_LANGUAGE")
195
280
 
@@ -216,7 +301,11 @@ def init() -> None:
216
301
  # Proceed with language selection
217
302
  display_names = [lang[0] for lang in Languages.LANGUAGES]
218
303
  language_selection = questionary.select(
219
- "Select a language for commit messages:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
304
+ "Select a language for commit messages:",
305
+ choices=display_names,
306
+ use_shortcuts=True,
307
+ use_arrow_keys=True,
308
+ use_jk_keys=False,
220
309
  ).ask()
221
310
 
222
311
  if not language_selection:
@@ -237,10 +326,30 @@ def init() -> None:
237
326
  language_value = None
238
327
  else:
239
328
  language_value = custom_language.strip()
329
+
330
+ # Check if the custom language appears to be RTL
331
+ if is_rtl_text(language_value):
332
+ if not _should_show_rtl_warning_for_init():
333
+ click.echo(
334
+ f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)"
335
+ )
336
+ else:
337
+ if not _show_rtl_warning_for_init(language_value):
338
+ click.echo("Language selection cancelled. Keeping existing language.")
339
+ language_value = None
240
340
  else:
241
341
  # Find the English name for the selected language
242
342
  language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
243
343
 
344
+ # Check if predefined language is RTL
345
+ if is_rtl_text(language_value):
346
+ if not _should_show_rtl_warning_for_init():
347
+ click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
348
+ else:
349
+ if not _show_rtl_warning_for_init(language_value):
350
+ click.echo("Language selection cancelled. Keeping existing language.")
351
+ language_value = None
352
+
244
353
  if language_value:
245
354
  # Ask about prefix translation
246
355
  prefix_choice = questionary.select(
@@ -266,7 +375,11 @@ def init() -> None:
266
375
  # No existing language - proceed with normal flow
267
376
  display_names = [lang[0] for lang in Languages.LANGUAGES]
268
377
  language_selection = questionary.select(
269
- "Select a language for commit messages:", choices=display_names, use_shortcuts=True, use_arrow_keys=True
378
+ "Select a language for commit messages:",
379
+ choices=display_names,
380
+ use_shortcuts=True,
381
+ use_arrow_keys=True,
382
+ use_jk_keys=False,
270
383
  ).ask()
271
384
 
272
385
  if not language_selection:
@@ -287,10 +400,28 @@ def init() -> None:
287
400
  language_value = None
288
401
  else:
289
402
  language_value = custom_language.strip()
403
+
404
+ # Check if the custom language appears to be RTL
405
+ if is_rtl_text(language_value):
406
+ if not _should_show_rtl_warning_for_init():
407
+ click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
408
+ else:
409
+ if not _show_rtl_warning_for_init(language_value):
410
+ click.echo("Language selection cancelled. Using English (default).")
411
+ language_value = None
290
412
  else:
291
413
  # Find the English name for the selected language
292
414
  language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
293
415
 
416
+ # Check if predefined language is RTL
417
+ if is_rtl_text(language_value):
418
+ if not _should_show_rtl_warning_for_init():
419
+ click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
420
+ else:
421
+ if not _show_rtl_warning_for_init(language_value):
422
+ click.echo("Language selection cancelled. Using English (default).")
423
+ language_value = None
424
+
294
425
  if language_value:
295
426
  # Ask about prefix translation
296
427
  prefix_choice = questionary.select(
@@ -313,4 +444,29 @@ def init() -> None:
313
444
  click.echo(f"Set GAC_LANGUAGE={language_value}")
314
445
  click.echo(f"Set GAC_TRANSLATE_PREFIXES={'true' if translate_prefixes else 'false'}")
315
446
 
447
+ return
448
+
449
+
450
+ @click.command()
451
+ def init() -> None:
452
+ """Interactively set up $HOME/.gac.env for gac."""
453
+ click.echo("Welcome to gac initialization!\n")
454
+
455
+ existing_env = _load_existing_env()
456
+ if not _configure_model(existing_env):
457
+ return
458
+ _configure_language(existing_env)
459
+
316
460
  click.echo(f"\ngac environment setup complete. You can edit {GAC_ENV_PATH} to update values later.")
461
+
462
+
463
+ @click.command()
464
+ def model() -> None:
465
+ """Interactively update provider/model/API key without language prompts."""
466
+ click.echo("Welcome to gac model configuration!\n")
467
+
468
+ existing_env = _load_existing_env()
469
+ if not _configure_model(existing_env):
470
+ return
471
+
472
+ click.echo(f"\nModel configuration complete. You can edit {GAC_ENV_PATH} to update values later.")