comit-cli 1.0.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.
- comit/__init__.py +1 -0
- comit/ai/__init__.py +39 -0
- comit/ai/base.py +42 -0
- comit/ai/factory.py +59 -0
- comit/ai/gemini_provider.py +125 -0
- comit/ai/groq_provider.py +121 -0
- comit/ai/ollama_provider.py +107 -0
- comit/ai/openai_provider.py +107 -0
- comit/cli.py +446 -0
- comit/commit/__init__.py +21 -0
- comit/commit/context.py +60 -0
- comit/commit/validator.py +270 -0
- comit/config.py +279 -0
- comit/git.py +281 -0
- comit/prompts.py +148 -0
- comit/review/__init__.py +29 -0
- comit/review/base.py +13 -0
- comit/review/databases.py +40 -0
- comit/review/engine.py +92 -0
- comit/review/generated_files.py +75 -0
- comit/review/git_files.py +54 -0
- comit/review/large_files.py +38 -0
- comit/review/models.py +59 -0
- comit/review/private_keys.py +69 -0
- comit/review/secrets.py +230 -0
- comit/review/sensitive_files.py +79 -0
- comit/ui.py +412 -0
- comit_cli-1.0.0.dist-info/METADATA +524 -0
- comit_cli-1.0.0.dist-info/RECORD +32 -0
- comit_cli-1.0.0.dist-info/WHEEL +5 -0
- comit_cli-1.0.0.dist-info/entry_points.txt +3 -0
- comit_cli-1.0.0.dist-info/top_level.txt +1 -0
comit/ui.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.prompt import Prompt, Confirm
|
|
8
|
+
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
try:
|
|
11
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
error_console = Console(stderr=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def show_step_success(message: str) -> None:
|
|
23
|
+
console.print(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def show_step_error(message: str) -> None:
|
|
27
|
+
error_console.print(f"Error: {message}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def show_not_git_repository() -> None:
|
|
31
|
+
error_console.print("Not a Git repository.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def show_no_staged_changes() -> None:
|
|
35
|
+
error_console.print("No staged changes found.\n")
|
|
36
|
+
error_console.print("Stage your changes first:")
|
|
37
|
+
error_console.print(" git add <files>\n")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def display_suggested_commit(message: str, title: str = "Generated") -> None:
|
|
41
|
+
console.print(f"{title}:\n\n {message}\n")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_prompt_toolkit_menu(
|
|
45
|
+
options: List[Tuple[str, str]],
|
|
46
|
+
prompt_text: str = "",
|
|
47
|
+
default_index: int = 0,
|
|
48
|
+
allow_quit_key: bool = True,
|
|
49
|
+
) -> Optional[str]:
|
|
50
|
+
from prompt_toolkit.application import Application
|
|
51
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
52
|
+
from prompt_toolkit.layout import Layout, HSplit, Window
|
|
53
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
54
|
+
from prompt_toolkit.styles import Style
|
|
55
|
+
|
|
56
|
+
kb = KeyBindings()
|
|
57
|
+
selected = [default_index]
|
|
58
|
+
num_options = len(options)
|
|
59
|
+
|
|
60
|
+
@kb.add("up")
|
|
61
|
+
@kb.add("k")
|
|
62
|
+
def _(event):
|
|
63
|
+
selected[0] = (selected[0] - 1) % num_options
|
|
64
|
+
|
|
65
|
+
@kb.add("down")
|
|
66
|
+
@kb.add("j")
|
|
67
|
+
def _(event):
|
|
68
|
+
selected[0] = (selected[0] + 1) % num_options
|
|
69
|
+
|
|
70
|
+
@kb.add("enter")
|
|
71
|
+
def _(event):
|
|
72
|
+
event.app.exit(result=options[selected[0]][0])
|
|
73
|
+
|
|
74
|
+
if allow_quit_key:
|
|
75
|
+
@kb.add("q")
|
|
76
|
+
@kb.add("Q")
|
|
77
|
+
def _(event):
|
|
78
|
+
res = "cancel" if "cancel" in [o[0] for o in options] else "exit"
|
|
79
|
+
event.app.exit(result=res)
|
|
80
|
+
|
|
81
|
+
for opt_id, _ in options:
|
|
82
|
+
first_char = opt_id[0].lower()
|
|
83
|
+
@kb.add(first_char)
|
|
84
|
+
def _(event, val=opt_id):
|
|
85
|
+
event.app.exit(result=val)
|
|
86
|
+
|
|
87
|
+
for idx, (opt_id, _) in enumerate(options, start=1):
|
|
88
|
+
@kb.add(str(idx))
|
|
89
|
+
def _(event, val=opt_id):
|
|
90
|
+
event.app.exit(result=val)
|
|
91
|
+
|
|
92
|
+
@kb.add("c-c")
|
|
93
|
+
def _(event):
|
|
94
|
+
res = "cancel" if "cancel" in [o[0] for o in options] else "exit"
|
|
95
|
+
event.app.exit(result=res)
|
|
96
|
+
|
|
97
|
+
def get_formatted_text():
|
|
98
|
+
tokens = []
|
|
99
|
+
if prompt_text:
|
|
100
|
+
tokens.append(("bold", f"{prompt_text}\n\n"))
|
|
101
|
+
for i, (_, label) in enumerate(options):
|
|
102
|
+
if i == selected[0]:
|
|
103
|
+
tokens.append(("ansicyan bold", f"> {label}\n"))
|
|
104
|
+
else:
|
|
105
|
+
tokens.append(("", f" {label}\n"))
|
|
106
|
+
return tokens
|
|
107
|
+
|
|
108
|
+
style = Style.from_dict({
|
|
109
|
+
"prompt": "bold",
|
|
110
|
+
"cursor": "ansicyan bold",
|
|
111
|
+
"selected": "ansicyan bold",
|
|
112
|
+
"unselected": "",
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
app = Application(
|
|
116
|
+
layout=Layout(HSplit([Window(content=FormattedTextControl(get_formatted_text))])),
|
|
117
|
+
key_bindings=kb,
|
|
118
|
+
style=style,
|
|
119
|
+
full_screen=False,
|
|
120
|
+
erase_when_done=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return app.run()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def select_arrow_menu(
|
|
127
|
+
options: List[Tuple[str, str]],
|
|
128
|
+
prompt_text: str = "",
|
|
129
|
+
default_index: int = 0,
|
|
130
|
+
allow_quit_key: bool = True,
|
|
131
|
+
) -> str:
|
|
132
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
133
|
+
try:
|
|
134
|
+
result = _run_prompt_toolkit_menu(
|
|
135
|
+
options=options,
|
|
136
|
+
prompt_text=prompt_text,
|
|
137
|
+
default_index=default_index,
|
|
138
|
+
allow_quit_key=allow_quit_key,
|
|
139
|
+
)
|
|
140
|
+
if result:
|
|
141
|
+
return result
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
if prompt_text:
|
|
146
|
+
console.print(f"{prompt_text}\n")
|
|
147
|
+
for idx, (opt_id, opt_label) in enumerate(options, start=1):
|
|
148
|
+
console.print(f" {opt_id} - {opt_label}")
|
|
149
|
+
choices = [opt_id for opt_id, _ in options]
|
|
150
|
+
if allow_quit_key:
|
|
151
|
+
choices.extend(["q", "quit"])
|
|
152
|
+
try:
|
|
153
|
+
choice = Prompt.ask(
|
|
154
|
+
"\nSelect an option",
|
|
155
|
+
default=options[default_index][0],
|
|
156
|
+
console=console,
|
|
157
|
+
).strip().lower()
|
|
158
|
+
except Exception:
|
|
159
|
+
return options[default_index][0]
|
|
160
|
+
|
|
161
|
+
if allow_quit_key and choice in ("q", "quit"):
|
|
162
|
+
return "cancel" if "cancel" in [o[0] for o in options] else "exit"
|
|
163
|
+
for idx, (opt_id, opt_label) in enumerate(options, start=1):
|
|
164
|
+
if choice in (opt_id.lower(), opt_label.lower(), str(idx), opt_id[:1].lower()):
|
|
165
|
+
return opt_id
|
|
166
|
+
return options[default_index][0]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def prompt_action() -> str:
|
|
170
|
+
options = [
|
|
171
|
+
("accept", "Accept"),
|
|
172
|
+
("edit", "Edit"),
|
|
173
|
+
("regenerate", "Regenerate"),
|
|
174
|
+
("cancel", "Cancel"),
|
|
175
|
+
]
|
|
176
|
+
return select_arrow_menu(options, prompt_text="", default_index=0)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def prompt_edit(current_message: str) -> Optional[str]:
|
|
180
|
+
console.print("\nEdit commit message (press Enter to save, Esc or Ctrl+C to cancel):")
|
|
181
|
+
while True:
|
|
182
|
+
try:
|
|
183
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
184
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
185
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
186
|
+
|
|
187
|
+
kb = KeyBindings()
|
|
188
|
+
cancelled = [False]
|
|
189
|
+
|
|
190
|
+
@kb.add("escape")
|
|
191
|
+
def _(event):
|
|
192
|
+
cancelled[0] = True
|
|
193
|
+
event.app.exit(result=None)
|
|
194
|
+
|
|
195
|
+
edited = pt_prompt("> ", default=current_message, key_bindings=kb)
|
|
196
|
+
if cancelled[0] or edited is None:
|
|
197
|
+
console.print("Edit cancelled. Keeping previous message.")
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
cleaned = edited.strip()
|
|
201
|
+
if not cleaned:
|
|
202
|
+
error_console.print("Commit message cannot be empty. Please enter a valid message or press Esc to cancel.")
|
|
203
|
+
continue
|
|
204
|
+
return cleaned
|
|
205
|
+
else:
|
|
206
|
+
from rich.prompt import Prompt
|
|
207
|
+
edited = Prompt.ask("> ", default=current_message, console=console)
|
|
208
|
+
if edited is None:
|
|
209
|
+
return None
|
|
210
|
+
cleaned = edited.strip()
|
|
211
|
+
if not cleaned:
|
|
212
|
+
error_console.print("Commit message cannot be empty.")
|
|
213
|
+
return None
|
|
214
|
+
return cleaned
|
|
215
|
+
except (KeyboardInterrupt, EOFError):
|
|
216
|
+
console.print("Edit cancelled. Keeping previous message.")
|
|
217
|
+
return None
|
|
218
|
+
except Exception:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def prompt_confirm_continue_commit() -> bool:
|
|
223
|
+
try:
|
|
224
|
+
return Confirm.ask(
|
|
225
|
+
"Continue to commit?",
|
|
226
|
+
default=False,
|
|
227
|
+
console=console,
|
|
228
|
+
)
|
|
229
|
+
except Exception:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def show_commit_success(message: str) -> None:
|
|
234
|
+
console.print("\nCommit created successfully.\n")
|
|
235
|
+
console.print(f" {message}\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def show_auto_commit_success(message: str) -> None:
|
|
239
|
+
console.print(f"\nGenerated:\n\n {message}\n\nCommit created successfully.\n")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def show_cancelled() -> None:
|
|
243
|
+
console.print("\nCancelled. No commit was created.\n")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def show_error(message: str) -> None:
|
|
247
|
+
error_console.print(f"Error: {message}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def show_settings_summary(summary: Dict[str, Any]) -> None:
|
|
251
|
+
console.print("\nComit Configuration\n")
|
|
252
|
+
console.print(f" AI Provider: {summary['provider']} ({summary.get('provider_source', 'default')})")
|
|
253
|
+
console.print(f" Model: {summary['model']} ({summary['model_source']})")
|
|
254
|
+
if summary['provider'] == "ollama":
|
|
255
|
+
console.print(f" Ollama Host: {summary['ollama_host']}")
|
|
256
|
+
else:
|
|
257
|
+
provider_title = summary['provider'].capitalize()
|
|
258
|
+
console.print(f" {provider_title} API Key: {summary['api_key_masked']} ({summary['api_key_source']})")
|
|
259
|
+
console.print(f" Config File: {summary['config_file']}")
|
|
260
|
+
console.print(f" Config File Status: {'Present' if summary['config_exists'] else 'Not created (using defaults)'}\n")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def prompt_settings_menu(provider: str = "groq") -> str:
|
|
264
|
+
options = [
|
|
265
|
+
("view", "View configuration"),
|
|
266
|
+
("set_provider", "Select AI provider"),
|
|
267
|
+
("set_key", f"Configure {provider.capitalize()} API key" if provider != "ollama" else "Configure Ollama host"),
|
|
268
|
+
("set_model", "Configure model"),
|
|
269
|
+
("reset", "Reset configuration"),
|
|
270
|
+
("exit", "Exit"),
|
|
271
|
+
]
|
|
272
|
+
return select_arrow_menu(options, prompt_text="Settings Menu", default_index=0, allow_quit_key=True)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def prompt_provider_selection(current_provider: str = "groq") -> str:
|
|
276
|
+
options = [
|
|
277
|
+
("groq", "Groq (Fast inference, default)"),
|
|
278
|
+
("gemini", "Google Gemini"),
|
|
279
|
+
("openai", "OpenAI"),
|
|
280
|
+
("ollama", "Ollama (Local models)"),
|
|
281
|
+
]
|
|
282
|
+
idx = 0
|
|
283
|
+
for i, (p_id, _) in enumerate(options):
|
|
284
|
+
if p_id == current_provider:
|
|
285
|
+
idx = i
|
|
286
|
+
break
|
|
287
|
+
return select_arrow_menu(options, prompt_text="Select AI Provider", default_index=idx, allow_quit_key=True)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def prompt_api_key(provider: str = "groq") -> str:
|
|
291
|
+
provider_title = provider.capitalize()
|
|
292
|
+
key = Prompt.ask(
|
|
293
|
+
f"Enter {provider_title} API Key",
|
|
294
|
+
password=True,
|
|
295
|
+
console=console,
|
|
296
|
+
).strip()
|
|
297
|
+
return key
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def prompt_model(current_model: str, provider: str = "groq") -> str:
|
|
301
|
+
provider_title = provider.capitalize()
|
|
302
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
303
|
+
try:
|
|
304
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
305
|
+
model = pt_prompt(f"Enter {provider_title} Model: ", default=current_model).strip()
|
|
306
|
+
return model if model else current_model
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
model = Prompt.ask(
|
|
310
|
+
f"Enter {provider_title} Model",
|
|
311
|
+
default=current_model,
|
|
312
|
+
console=console,
|
|
313
|
+
).strip()
|
|
314
|
+
return model if model else current_model
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def prompt_ollama_host(current_host: str = "http://localhost:11434") -> str:
|
|
318
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
319
|
+
try:
|
|
320
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
321
|
+
host = pt_prompt("Enter Ollama Host: ", default=current_host).strip()
|
|
322
|
+
return host if host else current_host
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
host = Prompt.ask(
|
|
326
|
+
"Enter Ollama Host",
|
|
327
|
+
default=current_host,
|
|
328
|
+
console=console,
|
|
329
|
+
).strip()
|
|
330
|
+
return host if host else current_host
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def prompt_confirm_reset() -> bool:
|
|
334
|
+
return Confirm.ask(
|
|
335
|
+
"Are you sure you want to reset all user configuration?",
|
|
336
|
+
default=False,
|
|
337
|
+
console=console,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def prompt_confirm_push() -> bool:
|
|
342
|
+
try:
|
|
343
|
+
return Confirm.ask(
|
|
344
|
+
"\nPush this commit to the remote?",
|
|
345
|
+
default=False,
|
|
346
|
+
console=console,
|
|
347
|
+
)
|
|
348
|
+
except Exception:
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def show_push_success(remote: str, branch: str) -> None:
|
|
353
|
+
console.print(f"Pushed to {remote}/{branch}.\n")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def show_push_skipped() -> None:
|
|
357
|
+
console.print("Commit created locally. Nothing was pushed.\n")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def show_no_remote(explicit_push: bool = False) -> None:
|
|
361
|
+
if explicit_push:
|
|
362
|
+
console.print("No Git remote is configured. Commit created locally; nothing was pushed.\n")
|
|
363
|
+
else:
|
|
364
|
+
console.print("No Git remote is configured. Commit created locally.\n")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def show_no_default_remote() -> None:
|
|
368
|
+
console.print("No default push remote ('origin') found. Commit created locally; nothing was pushed.\n")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def show_push_error(error_message: str) -> None:
|
|
372
|
+
error_console.print("\nPush failed:\n")
|
|
373
|
+
error_console.print(f" {error_message}\n")
|
|
374
|
+
console.print("Your commit was created locally and was not lost.\n")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def display_review_result(result: Any) -> None:
|
|
378
|
+
from comit.review.models import ReviewSeverity
|
|
379
|
+
|
|
380
|
+
if not result.has_findings:
|
|
381
|
+
console.print("No issues detected.\n")
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
console.print("Review findings:\n")
|
|
385
|
+
|
|
386
|
+
for finding in result.findings:
|
|
387
|
+
if finding.severity == ReviewSeverity.HIGH:
|
|
388
|
+
sev_tag = "HIGH"
|
|
389
|
+
elif finding.severity == ReviewSeverity.WARNING:
|
|
390
|
+
sev_tag = "WARNING"
|
|
391
|
+
else:
|
|
392
|
+
sev_tag = "INFO"
|
|
393
|
+
|
|
394
|
+
console.print(f"{sev_tag}")
|
|
395
|
+
console.print(f"{finding.message}")
|
|
396
|
+
console.print(f"{finding.file_path}")
|
|
397
|
+
if finding.details:
|
|
398
|
+
console.print(f" {finding.details}")
|
|
399
|
+
console.print()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def display_validation_result(result: Any) -> None:
|
|
403
|
+
if not result.has_warnings and not result.has_errors:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
console.print("Validation warning:\n")
|
|
407
|
+
for finding in result.findings:
|
|
408
|
+
if finding.severity.value in ("WARNING", "ERROR"):
|
|
409
|
+
console.print(f" {finding.message}")
|
|
410
|
+
if finding.details:
|
|
411
|
+
console.print(f" {finding.details}")
|
|
412
|
+
console.print()
|