gac 0.17.2__py3-none-any.whl → 3.6.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.
- gac/__version__.py +1 -1
- gac/ai.py +69 -123
- gac/ai_utils.py +227 -0
- gac/auth_cli.py +69 -0
- gac/cli.py +87 -19
- gac/config.py +13 -7
- gac/config_cli.py +26 -5
- gac/constants.py +176 -5
- gac/errors.py +14 -0
- gac/git.py +207 -11
- gac/init_cli.py +52 -29
- gac/language_cli.py +378 -0
- gac/main.py +922 -189
- gac/model_cli.py +374 -0
- gac/oauth/__init__.py +1 -0
- gac/oauth/claude_code.py +397 -0
- gac/preprocess.py +5 -5
- gac/prompt.py +656 -219
- gac/providers/__init__.py +88 -0
- gac/providers/anthropic.py +51 -0
- gac/providers/azure_openai.py +97 -0
- gac/providers/cerebras.py +38 -0
- gac/providers/chutes.py +71 -0
- gac/providers/claude_code.py +102 -0
- gac/providers/custom_anthropic.py +133 -0
- gac/providers/custom_openai.py +98 -0
- gac/providers/deepseek.py +38 -0
- gac/providers/fireworks.py +38 -0
- gac/providers/gemini.py +87 -0
- gac/providers/groq.py +63 -0
- gac/providers/kimi_coding.py +63 -0
- gac/providers/lmstudio.py +59 -0
- gac/providers/minimax.py +38 -0
- gac/providers/mistral.py +38 -0
- gac/providers/moonshot.py +38 -0
- gac/providers/ollama.py +50 -0
- gac/providers/openai.py +38 -0
- gac/providers/openrouter.py +58 -0
- gac/providers/replicate.py +98 -0
- gac/providers/streamlake.py +51 -0
- gac/providers/synthetic.py +42 -0
- gac/providers/together.py +38 -0
- gac/providers/zai.py +59 -0
- gac/security.py +293 -0
- gac/utils.py +243 -4
- gac/workflow_utils.py +222 -0
- gac-3.6.0.dist-info/METADATA +281 -0
- gac-3.6.0.dist-info/RECORD +53 -0
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/WHEEL +1 -1
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/licenses/LICENSE +1 -1
- gac-0.17.2.dist-info/METADATA +0 -221
- gac-0.17.2.dist-info/RECORD +0 -20
- {gac-0.17.2.dist-info → gac-3.6.0.dist-info}/entry_points.txt +0 -0
gac/language_cli.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""CLI for selecting commit message language interactively."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import unicodedata
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import questionary
|
|
9
|
+
from dotenv import load_dotenv, set_key
|
|
10
|
+
|
|
11
|
+
from gac.constants import Languages
|
|
12
|
+
|
|
13
|
+
GAC_ENV_PATH = Path.home() / ".gac.env"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure_language_init_workflow(existing_env_path: Path | str) -> bool:
|
|
17
|
+
"""Configure language as part of init workflow.
|
|
18
|
+
|
|
19
|
+
This is used by init_cli.py to handle language configuration
|
|
20
|
+
when the init command is run.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
existing_env_path: Path to the environment file
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if language configuration succeeded, False if cancelled
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Use the provided path instead of our default GAC_ENV_PATH
|
|
30
|
+
temp_env_path = Path(existing_env_path) if isinstance(existing_env_path, str) else existing_env_path
|
|
31
|
+
|
|
32
|
+
# If no env file, create it and proceed directly to language selection
|
|
33
|
+
if not temp_env_path.exists():
|
|
34
|
+
language_value = _run_language_selection_flow(temp_env_path)
|
|
35
|
+
return language_value is not None
|
|
36
|
+
|
|
37
|
+
# Clear any existing environ state to avoid cross-test contamination
|
|
38
|
+
env_keys_to_clear = [k for k in os.environ.keys() if k.startswith("GAC_")]
|
|
39
|
+
for key in env_keys_to_clear:
|
|
40
|
+
del os.environ[key]
|
|
41
|
+
|
|
42
|
+
# File exists - check for existing language
|
|
43
|
+
load_dotenv(temp_env_path)
|
|
44
|
+
existing_language = os.getenv("GAC_LANGUAGE")
|
|
45
|
+
|
|
46
|
+
if existing_language:
|
|
47
|
+
# Language already exists - ask what to do
|
|
48
|
+
preserve_action = questionary.select(
|
|
49
|
+
f"Found existing language: {existing_language}. How would you like to proceed?",
|
|
50
|
+
choices=[
|
|
51
|
+
f"Keep existing language ({existing_language})",
|
|
52
|
+
"Select new language",
|
|
53
|
+
],
|
|
54
|
+
use_shortcuts=True,
|
|
55
|
+
use_arrow_keys=True,
|
|
56
|
+
use_jk_keys=False,
|
|
57
|
+
).ask()
|
|
58
|
+
|
|
59
|
+
if not preserve_action:
|
|
60
|
+
click.echo("Language configuration cancelled. Proceeding with init...")
|
|
61
|
+
return True # Continue with init, just skip language part
|
|
62
|
+
|
|
63
|
+
if preserve_action.startswith("Keep existing language"):
|
|
64
|
+
click.echo(f"Keeping existing language: {existing_language}")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# User wants to select new language
|
|
68
|
+
language_value = _run_language_selection_flow(temp_env_path)
|
|
69
|
+
if language_value is None:
|
|
70
|
+
click.echo("Language selection cancelled. Proceeding with init...")
|
|
71
|
+
return True # Continue with init, just skip language part
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
# No existing language
|
|
75
|
+
language_value = _run_language_selection_flow(temp_env_path)
|
|
76
|
+
return language_value is not None
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
click.echo(f"Language configuration error: {e}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _run_language_selection_flow(env_path: Path) -> str | None:
|
|
84
|
+
"""Run the language selection flow and return the selected language.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
env_path: Path to the environment file
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Selected language value, or None if cancelled
|
|
91
|
+
"""
|
|
92
|
+
display_names = [lang[0] for lang in Languages.LANGUAGES]
|
|
93
|
+
language_selection = questionary.select(
|
|
94
|
+
"Select a language for commit messages:",
|
|
95
|
+
choices=display_names,
|
|
96
|
+
use_shortcuts=True,
|
|
97
|
+
use_arrow_keys=True,
|
|
98
|
+
use_jk_keys=False,
|
|
99
|
+
).ask()
|
|
100
|
+
|
|
101
|
+
if not language_selection:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Handle English - set explicitly
|
|
105
|
+
if language_selection == "English":
|
|
106
|
+
set_key(str(env_path), "GAC_LANGUAGE", "English")
|
|
107
|
+
set_key(str(env_path), "GAC_TRANSLATE_PREFIXES", "false")
|
|
108
|
+
click.echo("Set GAC_LANGUAGE='English'")
|
|
109
|
+
click.echo("Set GAC_TRANSLATE_PREFIXES='false'")
|
|
110
|
+
return "English"
|
|
111
|
+
|
|
112
|
+
# Handle custom input
|
|
113
|
+
if language_selection == "Custom":
|
|
114
|
+
language_value = _handle_custom_language_input()
|
|
115
|
+
else:
|
|
116
|
+
# Find the English name for the selected language
|
|
117
|
+
language_value = next(lang[1] for lang in Languages.LANGUAGES if lang[0] == language_selection)
|
|
118
|
+
|
|
119
|
+
if language_value is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# Check if language is RTL and handle warning
|
|
123
|
+
if is_rtl_text(language_value):
|
|
124
|
+
if not should_show_rtl_warning():
|
|
125
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
126
|
+
else:
|
|
127
|
+
if not show_rtl_warning(language_value, env_path):
|
|
128
|
+
return None # User cancelled
|
|
129
|
+
|
|
130
|
+
# Ask about prefix translation
|
|
131
|
+
translate_prefixes = _ask_about_prefix_translation(language_value)
|
|
132
|
+
if translate_prefixes is None:
|
|
133
|
+
return None # User cancelled
|
|
134
|
+
|
|
135
|
+
# Set the language and prefix translation preference
|
|
136
|
+
set_key(str(env_path), "GAC_LANGUAGE", language_value)
|
|
137
|
+
set_key(str(env_path), "GAC_TRANSLATE_PREFIXES", "true" if translate_prefixes else "false")
|
|
138
|
+
click.echo(f"Set GAC_LANGUAGE='{language_value}'")
|
|
139
|
+
click.echo(f"Set GAC_TRANSLATE_PREFIXES={'true' if translate_prefixes else 'false'}")
|
|
140
|
+
|
|
141
|
+
return language_value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _handle_custom_language_input() -> str | None:
|
|
145
|
+
"""Handle custom language input from user.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Custom language value, or None if cancelled/empty
|
|
149
|
+
"""
|
|
150
|
+
custom_language: str | None = questionary.text(
|
|
151
|
+
"Enter the language name (e.g., 'Spanish', 'Français', '日本語'):",
|
|
152
|
+
use_shortcuts=True,
|
|
153
|
+
use_arrow_keys=True,
|
|
154
|
+
use_jk_keys=False,
|
|
155
|
+
).ask()
|
|
156
|
+
|
|
157
|
+
if not custom_language or not custom_language.strip():
|
|
158
|
+
return None
|
|
159
|
+
return custom_language.strip()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _ask_about_prefix_translation(language_value: str) -> bool | None:
|
|
163
|
+
"""Ask user about prefix translation preference.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
language_value: The selected language
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if translate prefixes, False if keep English, None if cancelled
|
|
170
|
+
"""
|
|
171
|
+
prefix_choice: str | None = questionary.select(
|
|
172
|
+
"How should conventional commit prefixes be handled?",
|
|
173
|
+
choices=[
|
|
174
|
+
"Keep prefixes in English (feat:, fix:, etc.)",
|
|
175
|
+
f"Translate prefixes into {language_value}",
|
|
176
|
+
],
|
|
177
|
+
use_shortcuts=True,
|
|
178
|
+
use_arrow_keys=True,
|
|
179
|
+
use_jk_keys=False,
|
|
180
|
+
).ask()
|
|
181
|
+
|
|
182
|
+
if not prefix_choice:
|
|
183
|
+
return None
|
|
184
|
+
return prefix_choice.startswith("Translate prefixes")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def should_show_rtl_warning() -> bool:
|
|
188
|
+
"""Check if RTL warning should be shown based on saved preference.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if warning should be shown, False if user previously confirmed
|
|
192
|
+
"""
|
|
193
|
+
# Load the current config to check RTL confirmation
|
|
194
|
+
if GAC_ENV_PATH.exists():
|
|
195
|
+
load_dotenv(GAC_ENV_PATH)
|
|
196
|
+
rtl_confirmed = os.getenv("GAC_RTL_CONFIRMED", "false").lower() in ("true", "1", "yes", "on")
|
|
197
|
+
return not rtl_confirmed
|
|
198
|
+
return True # Show warning if no config exists
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def is_rtl_text(text: str) -> bool:
|
|
202
|
+
"""Detect if text contains RTL characters or is a known RTL language.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
text: Text to analyze
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if text contains RTL script characters or is RTL language
|
|
209
|
+
"""
|
|
210
|
+
# Known RTL language names (case insensitive)
|
|
211
|
+
rtl_languages = {
|
|
212
|
+
"arabic",
|
|
213
|
+
"ar",
|
|
214
|
+
"العربية",
|
|
215
|
+
"hebrew",
|
|
216
|
+
"he",
|
|
217
|
+
"עברית",
|
|
218
|
+
"persian",
|
|
219
|
+
"farsi",
|
|
220
|
+
"fa",
|
|
221
|
+
"urdu",
|
|
222
|
+
"ur",
|
|
223
|
+
"اردو",
|
|
224
|
+
"pashto",
|
|
225
|
+
"ps",
|
|
226
|
+
"kurdish",
|
|
227
|
+
"ku",
|
|
228
|
+
"کوردی",
|
|
229
|
+
"yiddish",
|
|
230
|
+
"yi",
|
|
231
|
+
"ייִדיש",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Check if it's a known RTL language name or code (case insensitive)
|
|
235
|
+
if text.lower().strip() in rtl_languages:
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
rtl_scripts = {"Arabic", "Hebrew", "Thaana", "Nko", "Syriac", "Mandeic", "Samaritan", "Mongolian", "Phags-Pa"}
|
|
239
|
+
|
|
240
|
+
for char in text:
|
|
241
|
+
if unicodedata.name(char, "").startswith(("ARABIC", "HEBREW")):
|
|
242
|
+
return True
|
|
243
|
+
script = unicodedata.name(char, "").split()[0] if unicodedata.name(char, "") else ""
|
|
244
|
+
if script in rtl_scripts:
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def center_text(text: str, width: int = 80) -> str:
|
|
250
|
+
"""Center text within specified width, handling display width properly.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
text: Text to center
|
|
254
|
+
width: Terminal width to center within (default 80)
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Centered text with proper padding
|
|
258
|
+
"""
|
|
259
|
+
import unicodedata
|
|
260
|
+
|
|
261
|
+
def get_display_width(s: str) -> int:
|
|
262
|
+
"""Get the display width of a string, accounting for wide characters."""
|
|
263
|
+
width = 0
|
|
264
|
+
for char in s:
|
|
265
|
+
# East Asian characters are typically 2 columns wide
|
|
266
|
+
if unicodedata.east_asian_width(char) in ("W", "F"):
|
|
267
|
+
width += 2
|
|
268
|
+
else:
|
|
269
|
+
width += 1
|
|
270
|
+
return width
|
|
271
|
+
|
|
272
|
+
# Handle multi-line text
|
|
273
|
+
lines = text.split("\n")
|
|
274
|
+
centered_lines = []
|
|
275
|
+
|
|
276
|
+
for line in lines:
|
|
277
|
+
# Strip existing whitespace to avoid double padding
|
|
278
|
+
stripped_line = line.strip()
|
|
279
|
+
if stripped_line:
|
|
280
|
+
# Calculate padding using display width for accurate centering
|
|
281
|
+
display_width = get_display_width(stripped_line)
|
|
282
|
+
padding = max(0, (width - display_width) // 2)
|
|
283
|
+
centered_line = " " * padding + stripped_line
|
|
284
|
+
centered_lines.append(centered_line)
|
|
285
|
+
else:
|
|
286
|
+
centered_lines.append("")
|
|
287
|
+
|
|
288
|
+
return "\n".join(centered_lines)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_terminal_width() -> int:
|
|
292
|
+
"""Get the current terminal width.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Terminal width in characters, or default if can't be determined
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
import shutil
|
|
299
|
+
|
|
300
|
+
return shutil.get_terminal_size().columns
|
|
301
|
+
except (OSError, AttributeError):
|
|
302
|
+
return 80 # Fallback to 80 columns
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def show_rtl_warning(language_name: str, env_path: Path | None = None) -> bool:
|
|
306
|
+
"""Show RTL language warning and ask for confirmation.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
language_name: Name of the RTL language
|
|
310
|
+
env_path: Path to environment file (defaults to GAC_ENV_PATH)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
True if user wants to proceed, False if they cancel
|
|
314
|
+
"""
|
|
315
|
+
if env_path is None:
|
|
316
|
+
env_path = GAC_ENV_PATH
|
|
317
|
+
terminal_width = get_terminal_width()
|
|
318
|
+
|
|
319
|
+
# Center just the title
|
|
320
|
+
title = center_text("⚠️ RTL Language Detected", terminal_width)
|
|
321
|
+
|
|
322
|
+
click.echo()
|
|
323
|
+
click.echo(click.style(title, fg="yellow", bold=True))
|
|
324
|
+
click.echo()
|
|
325
|
+
click.echo("Right-to-left (RTL) languages may not display correctly in gac due to terminal limitations.")
|
|
326
|
+
click.echo("However, the commit messages will work fine and should be readable in Git clients")
|
|
327
|
+
click.echo("that properly support RTL text (like most web interfaces and modern tools).\n")
|
|
328
|
+
|
|
329
|
+
proceed = questionary.confirm("Do you want to proceed anyway?").ask()
|
|
330
|
+
if proceed:
|
|
331
|
+
# Remember that user has confirmed RTL acceptance
|
|
332
|
+
set_key(str(env_path), "GAC_RTL_CONFIRMED", "true")
|
|
333
|
+
click.echo("✓ RTL preference saved - you won't see this warning again")
|
|
334
|
+
return proceed if proceed is not None else False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@click.command()
|
|
338
|
+
def language() -> None:
|
|
339
|
+
"""Set the language for commit messages interactively."""
|
|
340
|
+
click.echo("Select a language for your commit messages:\n")
|
|
341
|
+
|
|
342
|
+
# Ensure .gac.env exists
|
|
343
|
+
if not GAC_ENV_PATH.exists():
|
|
344
|
+
GAC_ENV_PATH.touch()
|
|
345
|
+
click.echo(f"Created {GAC_ENV_PATH}")
|
|
346
|
+
|
|
347
|
+
language_value = _run_language_selection_flow(GAC_ENV_PATH)
|
|
348
|
+
|
|
349
|
+
if language_value is None:
|
|
350
|
+
click.echo("Language selection cancelled.")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Find the display name for output
|
|
354
|
+
try:
|
|
355
|
+
display_name = next(lang[0] for lang in Languages.LANGUAGES if lang[1] == language_value)
|
|
356
|
+
except StopIteration:
|
|
357
|
+
display_name = language_value # Custom language
|
|
358
|
+
|
|
359
|
+
# If custom language, check if it appears to be RTL for display purposes
|
|
360
|
+
if display_name == language_value and is_rtl_text(language_value):
|
|
361
|
+
# This is a custom RTL language that was handled in _run_language_selection_flow
|
|
362
|
+
if not should_show_rtl_warning():
|
|
363
|
+
click.echo(f"\nℹ️ Using RTL language {language_value} (RTL warning previously confirmed)")
|
|
364
|
+
|
|
365
|
+
click.echo(f"\n✓ Set language to {display_name}")
|
|
366
|
+
click.echo(f" GAC_LANGUAGE={language_value}")
|
|
367
|
+
|
|
368
|
+
# Check prefix translation setting
|
|
369
|
+
load_dotenv(GAC_ENV_PATH)
|
|
370
|
+
translate_prefixes = os.getenv("GAC_TRANSLATE_PREFIXES", "false").lower() in ("true", "1", "yes", "on")
|
|
371
|
+
if translate_prefixes:
|
|
372
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=true")
|
|
373
|
+
click.echo("\n Prefixes will be translated (e.g., 'corrección:' instead of 'fix:')")
|
|
374
|
+
else:
|
|
375
|
+
click.echo(" GAC_TRANSLATE_PREFIXES=false")
|
|
376
|
+
click.echo(f"\n Prefixes will remain in English (e.g., 'fix: <{language_value} description>')")
|
|
377
|
+
|
|
378
|
+
click.echo(f"\n Configuration saved to {GAC_ENV_PATH}")
|