centicodehelp 0.2.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.
@@ -0,0 +1,389 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+ import click
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
9
+ from rich.syntax import Syntax
10
+
11
+ from centicodehelp.core.models import Finding, Severity, AnalysisResult
12
+ from centicodehelp.core.config import load_config
13
+ from centicodehelp.analysis.detector import detect_language, is_binary_file
14
+ from centicodehelp.analysis.static_analyzer import StaticAnalyzer
15
+ from centicodehelp.analysis.binary_analyzer import BinaryAnalyzer
16
+ from centicodehelp.ai.llm_client import LLMClient
17
+ from centicodehelp.ai.reviewer import AIReviewer, AIReviewResult
18
+ from centicodehelp.ai.prompts import FIX_PROMPT_TEMPLATE
19
+ from centicodehelp.fixer.patcher import PatchGenerator
20
+ from centicodehelp.fixer.applier import PatchApplier
21
+ from centicodehelp.cli.theme import (
22
+ console,
23
+ COLOR_PURPLE,
24
+ COLOR_BLUE,
25
+ COLOR_INDIGO,
26
+ COLOR_CRITICAL,
27
+ COLOR_HIGH,
28
+ COLOR_MEDIUM,
29
+ COLOR_LOW,
30
+ COLOR_INFO,
31
+ COLOR_SUCCESS,
32
+ COLOR_ERROR,
33
+ COLOR_TEXT,
34
+ )
35
+
36
+ # Dark Theme Severity Styles
37
+ SEVERITY_STYLES = {
38
+ Severity.CRITICAL: (COLOR_CRITICAL, "🔴 CRITICAL"),
39
+ Severity.HIGH: (f"bold {COLOR_HIGH}", "🟠 HIGH"),
40
+ Severity.MEDIUM: (f"bold {COLOR_MEDIUM}", "🟡 MEDIUM"),
41
+ Severity.LOW: (f"{COLOR_LOW}", "🔵 LOW"),
42
+ Severity.INFO: (f"{COLOR_INFO}", "⚪ INFO"),
43
+ }
44
+
45
+ @click.command(name="analyze", help="Quellcode oder Binärdateien mit statischen Regeln und KI analysieren.")
46
+ @click.argument("target", type=click.Path(exists=False), required=False)
47
+ @click.option("-f", "--file", "file_opt", type=click.Path(exists=False), help="Zu analysierende Datei.")
48
+ @click.option("-d", "--dir", "dir_opt", type=click.Path(exists=False), help="Zu analysierendes Verzeichnis.")
49
+ @click.option("--lang", type=str, help="Programmiersprache erzwingen (z. B. python, cpp, csharp, java).")
50
+ @click.option("--ai", is_flag=True, default=False, help="KI-gestützte Tiefenanalyse aktivieren.")
51
+ @click.option("--fix", is_flag=True, default=False, help="Gefundene Probleme automatisch mit KI beheben.")
52
+ @click.option("--provider", type=str, help="KI-Anbieter (openai, anthropic, ollama, deepseek, groq, etc.).")
53
+ @click.option("--model", type=str, help="Modellname (z. B. gpt-4o, claude-3-5-sonnet, llama3.2).")
54
+ @click.option("--output", type=click.Choice(["terminal", "json", "diff"], case_sensitive=False), default="terminal", help="Ausgabeformat.")
55
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Korrekturen ohne manuelle Bestätigung anwenden.")
56
+ @click.option("--debug", is_flag=True, default=False, help="Debug-Informationen (Roh-Antwort der KI, Tokens, Latenz) anzeigen.")
57
+ def analyze_cmd(
58
+ target: Optional[str],
59
+ file_opt: Optional[str],
60
+ dir_opt: Optional[str],
61
+ lang: Optional[str],
62
+ ai: bool,
63
+ fix: bool,
64
+ provider: Optional[str],
65
+ model: Optional[str],
66
+ output: str,
67
+ yes: bool,
68
+ debug: bool,
69
+ ):
70
+ """Hauptbefehl zur Code- und Binäranalyse mit modernem Centi09-Dark-Theme."""
71
+ target_str = file_opt or dir_opt or target or "."
72
+ target_path = Path(target_str)
73
+
74
+ if not target_path.exists():
75
+ console.print(f"\n[bold {COLOR_ERROR}]❌ Ziel nicht gefunden:[/bold {COLOR_ERROR}] [white]{target_path}[/white]\n")
76
+ return
77
+
78
+ # Sammle alle Dateien
79
+ files_to_analyze: List[Path] = []
80
+ if target_path.is_file():
81
+ files_to_analyze.append(target_path)
82
+ else:
83
+ for root, dirs, files in os.walk(target_path):
84
+ dirs[:] = [
85
+ d for d in dirs
86
+ if not d.startswith(".")
87
+ and d not in ("node_modules", "target", "build", "dist", "__pycache__", "venv", ".venv", "env")
88
+ ]
89
+ for f in files:
90
+ files_to_analyze.append(Path(root) / f)
91
+
92
+ if not files_to_analyze:
93
+ console.print(f"\n[bold {COLOR_MEDIUM}]⚠️ Keine analysierbaren Dateien im Verzeichnis gefunden.[/bold {COLOR_MEDIUM}]\n")
94
+ return
95
+
96
+ config = load_config()
97
+ selected_provider = provider or config.default_provider
98
+ selected_model = model or config.default_model
99
+
100
+ # Info Header
101
+ mode_text = "[bold #38BDF8]Statische Analyse[/bold #38BDF8]"
102
+ if ai and fix:
103
+ mode_text = f"[bold {COLOR_PURPLE}]Statisch + KI-Audit & Auto-Fix[/bold {COLOR_PURPLE}] ({selected_provider}/{selected_model})"
104
+ elif ai:
105
+ mode_text = f"[bold {COLOR_PURPLE}]Statisch + KI-Tiefenanalyse[/bold {COLOR_PURPLE}] ({selected_provider}/{selected_model})"
106
+ elif fix:
107
+ mode_text = f"[bold {COLOR_PURPLE}]Statisch + Auto-Fix[/bold {COLOR_PURPLE}] ({selected_provider}/{selected_model})"
108
+
109
+ header_panel = Panel(
110
+ f"[bold]Ziel:[/bold] [yellow]{target_path}[/yellow] ({len(files_to_analyze)} Datei(en))\n"
111
+ f"[bold]Modus:[/bold] {mode_text}",
112
+ title=f"[bold {COLOR_PURPLE}]âš¡ centicodehelp ANALYSE[/bold {COLOR_PURPLE}]",
113
+ border_style=COLOR_INDIGO,
114
+ expand=False,
115
+ )
116
+ console.print()
117
+ console.print(header_panel)
118
+ console.print()
119
+
120
+ static_analyzer = StaticAnalyzer()
121
+ binary_analyzer = BinaryAnalyzer()
122
+ llm_client = None
123
+ ai_reviewer = None
124
+
125
+ if ai or fix:
126
+ try:
127
+ llm_client = LLMClient(provider=selected_provider, model=selected_model)
128
+ ai_reviewer = AIReviewer(llm_client)
129
+ except Exception as e:
130
+ console.print(f"[bold {COLOR_ERROR}]❌ KI-Initialisierungsfehler:[/bold {COLOR_ERROR}] {e}")
131
+ if ai:
132
+ return
133
+
134
+ total_findings: List[Finding] = []
135
+
136
+ # Fortschrittsanzeige bei mehreren Dateien
137
+ if len(files_to_analyze) > 1:
138
+ with Progress(
139
+ SpinnerColumn(spinner_name="dots", style=COLOR_PURPLE),
140
+ TextColumn("[bold #818CF8]{task.description}"),
141
+ BarColumn(complete_style=COLOR_BLUE, finished_style=COLOR_SUCCESS),
142
+ TextColumn("[dim]{task.completed}/{task.total} Dateien[/dim]"),
143
+ console=console,
144
+ ) as progress:
145
+ scan_task = progress.add_task("Dateien scannen...", total=len(files_to_analyze))
146
+ for f_path in files_to_analyze:
147
+ progress.update(scan_task, description=f"Analysiere [cyan]{f_path.name}[/cyan]...")
148
+ file_lang = lang or detect_language(f_path)
149
+ if file_lang:
150
+ if file_lang == "binary":
151
+ findings = binary_analyzer.analyze_binary(f_path)
152
+ else:
153
+ findings = static_analyzer.analyze_file(f_path, file_lang)
154
+ total_findings.extend(findings)
155
+ progress.advance(scan_task)
156
+ else:
157
+ # Einzelne Datei
158
+ f_path = files_to_analyze[0]
159
+ file_lang = lang or detect_language(f_path)
160
+ if file_lang:
161
+ if file_lang == "binary":
162
+ findings = binary_analyzer.analyze_binary(f_path)
163
+ else:
164
+ findings = static_analyzer.analyze_file(f_path, file_lang)
165
+ total_findings.extend(findings)
166
+
167
+ # 1. Statische Findings anzeigen
168
+ _render_results(total_findings, output, title_prefix="STATISCHE CODE-ANALYSE")
169
+
170
+ # 2. KI-Tiefenanalyse (falls aktiviert)
171
+ ai_review_results: List[AIReviewResult] = []
172
+ if ai and ai_reviewer:
173
+ target_files = files_to_analyze[:3] # KI-Review für Zieldateien
174
+ for target_file in target_files:
175
+ f_lang = lang or detect_language(target_file)
176
+ if not f_lang or f_lang == "binary":
177
+ continue
178
+
179
+ with console.status(
180
+ f"[bold {COLOR_PURPLE}]🤖 Analysiere {target_file.name} mit {selected_provider}/{selected_model}...[/bold {COLOR_PURPLE}]",
181
+ spinner="dots12",
182
+ ):
183
+ review_res = ai_reviewer.review_with_details(target_file, f_lang, total_findings)
184
+ ai_review_results.append(review_res)
185
+
186
+ # Rendere KI-Tiefenanalyse-Abschnitt
187
+ _render_ai_section(ai_review_results, selected_provider, selected_model, debug=debug)
188
+
189
+ # Neue KI-Findings mit Gesamtergebnis verschmelzen
190
+ for r in ai_review_results:
191
+ if r.findings:
192
+ total_findings = ai_reviewer._merge_findings(total_findings, r.findings)
193
+
194
+ # Automatischer Fix falls angefordert
195
+ if fix and total_findings and llm_client:
196
+ for f_path in files_to_analyze:
197
+ f_lang = lang or detect_language(f_path)
198
+ if f_lang and f_lang != "binary":
199
+ file_findings = [f for f in total_findings if Path(f.file_path).name == f_path.name]
200
+ if file_findings:
201
+ _handle_fix(f_path, f_lang, file_findings, llm_client, yes, selected_provider, selected_model)
202
+
203
+
204
+ def _render_results(findings: List[Finding], output_format: str, title_prefix: str = "GEFUNDENE PROBLEME"):
205
+ if output_format == "json":
206
+ import json
207
+ out_list = [f.model_dump() for f in findings]
208
+ console.print(json.dumps(out_list, indent=2))
209
+ return
210
+
211
+ if not findings:
212
+ clean_panel = Panel(
213
+ f"[bold {COLOR_SUCCESS}]✅ Keine statischen Probleme gefunden![/bold {COLOR_SUCCESS}]\n"
214
+ f"[dim]Der Code entspricht allen statischen Sicherheits- und Qualitätsregeln.[/dim]",
215
+ border_style=COLOR_SUCCESS,
216
+ expand=False,
217
+ )
218
+ console.print(clean_panel)
219
+ console.print()
220
+ return
221
+
222
+ table = Table(
223
+ title=f"\n[bold {COLOR_PURPLE}]🔍 {title_prefix}[/bold {COLOR_PURPLE}] ({len(findings)} Finding(s))",
224
+ show_header=True,
225
+ header_style=f"bold {COLOR_BLUE}",
226
+ border_style=COLOR_INDIGO,
227
+ expand=True,
228
+ )
229
+ table.add_column("Schweregrad", width=14, justify="left")
230
+ table.add_column("Datei:Zeile", style="cyan", width=26)
231
+ table.add_column("Regel", style="bold #818CF8", width=14)
232
+ table.add_column("Beschreibung & Fix-Empfehlung", style=COLOR_TEXT)
233
+
234
+ for f in findings:
235
+ style_color, badge = SEVERITY_STYLES.get(f.severity, ("white", str(f.severity.value)))
236
+ loc = f"{Path(f.file_path).name}:{f.line if f.line is not None else '-'}"
237
+ desc_text = f"[{style_color}]{f.description}[/{style_color}]"
238
+ if f.suggested_fix:
239
+ desc_text += f"\n[dim italic]💡 Fix: {f.suggested_fix}[/dim italic]"
240
+
241
+ table.add_row(
242
+ f"[{style_color}]{badge}[/{style_color}]",
243
+ loc,
244
+ f.rule_id,
245
+ desc_text,
246
+ )
247
+
248
+ console.print(table)
249
+ console.print()
250
+
251
+
252
+ def _render_ai_section(
253
+ ai_results: List[AIReviewResult],
254
+ provider: str,
255
+ model: str,
256
+ debug: bool = False,
257
+ ):
258
+ """Rendert den sichtbaren Abschnitt für die KI-Tiefenanalyse."""
259
+ console.print()
260
+ section_title = f"🤖 KI-TIEFENANALYSE ({provider}/{model})"
261
+
262
+ if not ai_results:
263
+ console.print(
264
+ Panel(
265
+ f"[bold {COLOR_ERROR}]❌ Keine Antwort von der KI-Engine ({provider}/{model}) erhalten.[/bold {COLOR_ERROR}]",
266
+ title=f"[bold {COLOR_PURPLE}]{section_title}[/bold {COLOR_PURPLE}]",
267
+ border_style=COLOR_ERROR,
268
+ expand=False,
269
+ )
270
+ )
271
+ return
272
+
273
+ for res in ai_results:
274
+ telemetry = (
275
+ f"[dim]Dauer: [bold cyan]{res.duration_seconds}s[/bold cyan] • "
276
+ f"Tokens: [bold cyan]{res.total_tokens}[/bold cyan] (Prompt: {res.prompt_tokens}, Completion: {res.completion_tokens}) • "
277
+ f"Modell: [bold cyan]{res.model}[/bold cyan][/dim]"
278
+ )
279
+
280
+ content_lines = []
281
+ if res.error:
282
+ content_lines.append(f"[bold {COLOR_ERROR}]❌ KI-Fehler:[/bold {COLOR_ERROR}] {res.error}")
283
+ else:
284
+ if res.summary:
285
+ content_lines.append(f"[bold white]Zusammenfassung:[/bold white] {res.summary}")
286
+
287
+ if res.findings:
288
+ content_lines.append(f"\n[bold {COLOR_PURPLE}]Gefundene KI-Hinweise & Empfehlungen ({len(res.findings)}):[/bold {COLOR_PURPLE}]")
289
+ for idx, f in enumerate(res.findings, 1):
290
+ style_color, badge = SEVERITY_STYLES.get(f.severity, ("white", f.severity.value))
291
+ line_str = f"Zeile {f.line}" if f.line else "Allgemein"
292
+ content_lines.append(f" {idx}. [{style_color}]{badge}[/{style_color}] [cyan]({line_str})[/cyan]: {f.description}")
293
+ if f.suggested_fix:
294
+ content_lines.append(f" [dim italic]💡 Empfehlung: {f.suggested_fix}[/dim italic]")
295
+ else:
296
+ content_lines.append(f"\n[bold {COLOR_SUCCESS}]✅ Keine zusätzlichen Probleme durch KI gefunden.[/bold {COLOR_SUCCESS}]")
297
+
298
+ content_lines.append(f"\n{telemetry}")
299
+
300
+ console.print(
301
+ Panel(
302
+ "\n".join(content_lines),
303
+ title=f"[bold {COLOR_PURPLE}]{section_title}[/bold {COLOR_PURPLE}]",
304
+ border_style=COLOR_INDIGO,
305
+ expand=False,
306
+ )
307
+ )
308
+
309
+ # Debug Panel (falls --debug aktiviert oder Rohantwort gewünscht)
310
+ if debug and res.raw_response:
311
+ syntax_raw = Syntax(res.raw_response, "json", theme="monokai", line_numbers=True)
312
+ console.print(
313
+ Panel(
314
+ syntax_raw,
315
+ title=f"[bold #fb923c]🛠️ DEBUG: Rohtext-Antwort von {provider}/{model}[/bold #fb923c]",
316
+ border_style="#fb923c",
317
+ expand=False,
318
+ )
319
+ )
320
+
321
+ console.print()
322
+
323
+
324
+ def _handle_fix(
325
+ file_path: Path,
326
+ language: str,
327
+ findings: List[Finding],
328
+ llm_client: LLMClient,
329
+ auto_confirm: bool,
330
+ provider: str,
331
+ model: str,
332
+ ):
333
+ try:
334
+ source_code = file_path.read_text(encoding="utf-8", errors="replace")
335
+ except Exception as e:
336
+ console.print(f"[bold {COLOR_ERROR}]❌ Fehler beim Lesen von {file_path.name}:[/bold {COLOR_ERROR}] {e}")
337
+ return
338
+
339
+ findings_text = "\n".join(
340
+ [f"- Line {f.line or '?'}: {f.description} (Suggested fix: {f.suggested_fix or 'N/A'})" for f in findings]
341
+ )
342
+ fix_prompt = FIX_PROMPT_TEMPLATE.format(
343
+ file_path=str(file_path),
344
+ language=language,
345
+ findings_text=findings_text,
346
+ source_code=source_code,
347
+ )
348
+
349
+ clean_code = ""
350
+ with console.status(
351
+ f"[bold {COLOR_PURPLE}]Generiere KI-Korrektur mit {provider}/{model}...[/bold {COLOR_PURPLE}]",
352
+ spinner="dots12",
353
+ ):
354
+ try:
355
+ raw_fixed = llm_client.complete(
356
+ fix_prompt,
357
+ system="Du bist ein Sicherheitsexperte. Ersetze eval() IMMER durch ast.literal_eval(). Entferne alle print() Debug-Meldungen. Gib NUR den korrigierten Code zurück."
358
+ )
359
+ clean_code = PatchGenerator.extract_code_from_markdown(raw_fixed)
360
+ except Exception as e:
361
+ console.print(f"\n[bold {COLOR_ERROR}]❌ KI-Fix Fehler:[/bold {COLOR_ERROR}] {e}\n")
362
+ return
363
+
364
+ diff = PatchGenerator.generate_diff(source_code, clean_code, file_path=file_path.name)
365
+ if not diff:
366
+ console.print(f"[dim]Keine Diff-Änderung von der KI für {file_path.name} vorgeschlagen.[/dim]")
367
+ return
368
+
369
+ syntax_diff = Syntax(diff, "diff", theme="monokai", line_numbers=True)
370
+ console.print(
371
+ Panel(
372
+ syntax_diff,
373
+ title=f"[bold {COLOR_SUCCESS}]Vorgeschlagener Patch für {file_path.name}[/bold {COLOR_SUCCESS}]",
374
+ border_style=COLOR_INDIGO,
375
+ expand=False,
376
+ )
377
+ )
378
+
379
+ should_apply = auto_confirm
380
+ if not auto_confirm:
381
+ should_apply = click.confirm(f"Diesen Patch auf {file_path.name} anwenden?", default=True)
382
+
383
+ if should_apply:
384
+ applier = PatchApplier(backup=True)
385
+ if applier.apply_fix(file_path, clean_code):
386
+ console.print(f"\n[bold {COLOR_SUCCESS}]✅ Patch erfolgreich angewendet![/bold {COLOR_SUCCESS}] [dim](Backup: {file_path.name}.bak)[/dim]\n")
387
+ else:
388
+ console.print(f"\n[bold {COLOR_ERROR}]❌ Fehler beim Schreiben des Patches auf {file_path.name}[/bold {COLOR_ERROR}]\n")
389
+
@@ -0,0 +1,91 @@
1
+ from pathlib import Path
2
+ import click
3
+ from rich.table import Table
4
+ from rich.panel import Panel
5
+
6
+ from centicodehelp.analysis.detector import detect_language
7
+ from centicodehelp.analysis.static_analyzer import StaticAnalyzer
8
+ from centicodehelp.analysis.binary_analyzer import BinaryAnalyzer
9
+ from centicodehelp.core.models import Finding, Severity
10
+ from centicodehelp.cli.theme import (
11
+ console,
12
+ COLOR_PURPLE,
13
+ COLOR_BLUE,
14
+ COLOR_INDIGO,
15
+ COLOR_SUCCESS,
16
+ COLOR_ERROR,
17
+ COLOR_CRITICAL,
18
+ COLOR_HIGH,
19
+ COLOR_MEDIUM,
20
+ COLOR_LOW,
21
+ COLOR_INFO,
22
+ COLOR_TEXT,
23
+ )
24
+
25
+ SEVERITY_STYLES = {
26
+ Severity.CRITICAL: (COLOR_CRITICAL, "🔴 CRITICAL"),
27
+ Severity.HIGH: (f"bold {COLOR_HIGH}", "🟠 HIGH"),
28
+ Severity.MEDIUM: (f"bold {COLOR_MEDIUM}", "🟡 MEDIUM"),
29
+ Severity.LOW: (f"{COLOR_LOW}", "🔵 LOW"),
30
+ Severity.INFO: (f"{COLOR_INFO}", "⚪ INFO"),
31
+ }
32
+
33
+ @click.command(name="check", help="Schnellcheck einer einzelnen Datei (statisch).")
34
+ @click.argument("file_path", type=click.Path(exists=False), required=True)
35
+ def check_cmd(file_path: str):
36
+ """
37
+ ct check <datei> - Schnelle, kinderleichte Dateiüberprüfung.
38
+ """
39
+ path = Path(file_path)
40
+ if not path.exists() or not path.is_file():
41
+ console.print(f"\n[bold {COLOR_ERROR}]❌ Datei nicht gefunden:[/bold {COLOR_ERROR}] [white]{file_path}[/white]\n")
42
+ return
43
+
44
+ lang = detect_language(path)
45
+ if not lang:
46
+ console.print(f"\n[bold {COLOR_MEDIUM}]⚠️ Dateityp von {path.name} wird nicht unterstützt.[/bold {COLOR_MEDIUM}]\n")
47
+ return
48
+
49
+ static_analyzer = StaticAnalyzer()
50
+ binary_analyzer = BinaryAnalyzer()
51
+
52
+ with console.status(f"[bold {COLOR_PURPLE}]Prüfe {path.name}...[/bold {COLOR_PURPLE}]", spinner="dots"):
53
+ if lang == "binary":
54
+ findings = binary_analyzer.analyze_binary(path)
55
+ else:
56
+ findings = static_analyzer.analyze_file(path, lang)
57
+
58
+ console.print()
59
+ if not findings:
60
+ console.print(
61
+ Panel(
62
+ f"[bold {COLOR_SUCCESS}]✅ Keine Probleme gefunden![/bold {COLOR_SUCCESS}]\n"
63
+ f"[white]Die Datei [bold cyan]{path.name}[/bold cyan] ({lang}) ist sauber.[/white]",
64
+ title=f"[bold {COLOR_PURPLE}]âš¡ centicodehelp CHECK[/bold {COLOR_PURPLE}]",
65
+ border_style=COLOR_SUCCESS,
66
+ expand=False,
67
+ )
68
+ )
69
+ console.print()
70
+ return
71
+
72
+ table = Table(
73
+ title=f"[bold {COLOR_PURPLE}]âš¡ DATEICHECK:[/bold {COLOR_PURPLE}] [yellow]{path.name}[/yellow] ({len(findings)} Problem(e))",
74
+ show_header=True,
75
+ header_style=f"bold {COLOR_BLUE}",
76
+ border_style=COLOR_INDIGO,
77
+ )
78
+ table.add_column("Schweregrad", width=14)
79
+ table.add_column("Zeile", width=8, justify="right")
80
+ table.add_column("Was ist falsch?", style=COLOR_TEXT)
81
+ table.add_column("Tipp / Lösung", style="italic dim cyan")
82
+
83
+ for f in findings:
84
+ color, badge = SEVERITY_STYLES.get(f.severity, ("white", f.severity.value))
85
+ line_str = str(f.line) if f.line is not None else "-"
86
+ fix_str = f.suggested_fix or "Prüfen und korrigieren"
87
+ table.add_row(f"[{color}]{badge}[/{color}]", line_str, f.description, fix_str)
88
+
89
+ console.print(table)
90
+ console.print(f"[dim]Tipp: Repariere die Datei automatisch mit [bold {COLOR_BLUE}]ct fix {path}[/bold {COLOR_BLUE}][/dim]\n")
91
+
@@ -0,0 +1,67 @@
1
+ import click
2
+ from rich.table import Table
3
+ from rich.panel import Panel
4
+
5
+ from centicodehelp.core.config import load_config, save_config, get_config_path
6
+ from centicodehelp.cli.theme import (
7
+ console,
8
+ COLOR_PURPLE,
9
+ COLOR_BLUE,
10
+ COLOR_INDIGO,
11
+ COLOR_SUCCESS,
12
+ COLOR_ERROR,
13
+ COLOR_TEXT,
14
+ )
15
+
16
+ @click.group(name="config", help="centicodehelp Einstellungen und API-Schlüssel verwalten.")
17
+ def config_cmd():
18
+ """Konfiguration für centicodehelp."""
19
+ pass
20
+
21
+ @config_cmd.command(name="show", help="Aktuelle Konfiguration übersichtlich anzeigen.")
22
+ def show_config():
23
+ config = load_config()
24
+ cfg_path = get_config_path()
25
+
26
+ table = Table(
27
+ title=f"\n[bold {COLOR_PURPLE}]⚙️ centicodehelp KONFIGURATION[/bold {COLOR_PURPLE}]",
28
+ caption=f"[dim]Speicherort: {cfg_path}[/dim]",
29
+ show_header=True,
30
+ header_style=f"bold {COLOR_BLUE}",
31
+ border_style=COLOR_INDIGO,
32
+ expand=True,
33
+ )
34
+ table.add_column("Einstellung", style=f"bold {COLOR_PURPLE}", width=24)
35
+ table.add_column("Wert", style=COLOR_TEXT)
36
+
37
+ table.add_row("Standard-Anbieter", f"[bold {COLOR_BLUE}]{config.default_provider}[/bold {COLOR_BLUE}]")
38
+ table.add_row("Standard-Modell", f"[cyan]{config.default_model}[/cyan]")
39
+ table.add_row("Ollama API URL", config.ollama_base_url)
40
+ table.add_row("Automatisches Backup (.bak)", f"[{COLOR_SUCCESS}]Aktiviert[/{COLOR_SUCCESS}]" if config.auto_backup else "[dim]Deaktiviert[/dim]")
41
+
42
+ if config.api_keys:
43
+ keys_summary = ", ".join([f"[bold {COLOR_BLUE}]{k}[/bold {COLOR_BLUE}]: [dim]••••{v[-4:] if len(v)>4 else '••••'}[/dim]" for k, v in config.api_keys.items()])
44
+ table.add_row("Konfigurierte API-Keys", keys_summary)
45
+ else:
46
+ table.add_row("Konfigurierte API-Keys", "[dim italic]Keine (Verwende Umgebungsvariablen wie OPENAI_API_KEY)[/dim italic]")
47
+
48
+ console.print(table)
49
+ console.print()
50
+
51
+ @config_cmd.command(name="set-key", help="API-Schlüssel für einen Anbieter hinterlegen.")
52
+ @click.argument("provider")
53
+ @click.argument("api_key")
54
+ def set_key(provider: str, api_key: str):
55
+ config = load_config()
56
+ config.api_keys[provider.lower()] = api_key
57
+ save_config(config)
58
+ console.print(f"\n[bold {COLOR_SUCCESS}]✅ API-Schlüssel für[/bold {COLOR_SUCCESS}] [bold {COLOR_PURPLE}]{provider}[/bold {COLOR_PURPLE}] [bold {COLOR_SUCCESS}]gespeichert![/bold {COLOR_SUCCESS}]\n")
59
+
60
+ @config_cmd.command(name="set-default-model", help="Standardmodell festlegen.")
61
+ @click.argument("model")
62
+ def set_default_model(model: str):
63
+ config = load_config()
64
+ config.default_model = model
65
+ save_config(config)
66
+ console.print(f"\n[bold {COLOR_SUCCESS}]✅ Standardmodell geändert auf:[/bold {COLOR_SUCCESS}] [bold {COLOR_BLUE}]{model}[/bold {COLOR_BLUE}]\n")
67
+