codedna 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.
codedna/cli.py ADDED
@@ -0,0 +1,1965 @@
1
+ """CodeDNA CLI — typer tabanlı komut satırı arayüzü."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from codedna import __version__
16
+ from codedna.db import (
17
+ get_db_path,
18
+ get_all_file_understanding_scores,
19
+ get_commit_history,
20
+ get_file_scores_for_commit,
21
+ get_latest_commit,
22
+ init_db,
23
+ save_commit,
24
+ save_file_score,
25
+ update_understanding_score,
26
+ )
27
+ from codedna.git_hook import find_git_root, install_hook, install_ci_workflow, is_hook_installed, uninstall_hook
28
+ from codedna.scorer import get_repo, scan_repository, score_latest_commit
29
+ from codedna.survey import run_survey
30
+
31
+ app = typer.Typer(
32
+ name="codedna",
33
+ help="🧬 CodeDNA — AI kod şeffaflık aracı",
34
+ add_completion=False,
35
+ no_args_is_help=True,
36
+ )
37
+ console = Console()
38
+
39
+
40
+ def _get_db(repo_path: Optional[Path] = None) -> Path:
41
+ """Repo'ya özel veritabanı yolunu döndür."""
42
+ kok = repo_path or find_git_root() or Path.cwd()
43
+ return get_db_path(kok)
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # codedna init
48
+ # ---------------------------------------------------------------------------
49
+ @app.command()
50
+ def init(
51
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
52
+ with_ci: bool = typer.Option(False, "--with-ci", help="GitHub Actions CI şablonunu da oluştur"),
53
+ ) -> None:
54
+ """Git hook'u kur, veritabanını oluştur ve isteğe bağlı CI şablonu yaz."""
55
+ console.print()
56
+ console.print(
57
+ Panel.fit(
58
+ "[bold cyan]🧬 CodeDNA[/bold cyan] kurulumu başlatılıyor...",
59
+ border_style="cyan",
60
+ )
61
+ )
62
+
63
+ # Repo kökünü bul
64
+ kok = repo or find_git_root()
65
+ if not kok:
66
+ console.print("[bold red]Hata:[/bold red] Git repo bulunamadı. Önce 'git init' komutunu çalıştırın.")
67
+ raise typer.Exit(1)
68
+
69
+ console.print(f"[dim]Repo kökü:[/dim] {kok}")
70
+
71
+ # Veritabanını başlat
72
+ db_yolu = _get_db(kok)
73
+ init_db(db_yolu)
74
+ console.print(f"[green]✓[/green] Veritabanı oluşturuldu: [dim]{db_yolu}[/dim]")
75
+
76
+ # Hook'u kur
77
+ if is_hook_installed(kok):
78
+ console.print("[yellow]⚠[/yellow] Post-commit hook zaten kurulu.")
79
+ else:
80
+ if install_hook(kok):
81
+ console.print("[green]✓[/green] Post-commit hook kuruldu.")
82
+ else:
83
+ console.print("[red]✗[/red] Hook kurulumu başarısız.")
84
+ raise typer.Exit(1)
85
+
86
+ # CI şablonu
87
+ if with_ci:
88
+ install_ci_workflow(kok)
89
+
90
+ bilgi = (
91
+ "[bold green]CodeDNA başarıyla kuruldu![/bold green]\n"
92
+ "Artık her [bold]git commit[/bold] sonrası otomatik analiz çalışacak.\n\n"
93
+ "[dim]• Tüm repoyu taramak için:[/dim] [cyan]codedna scan[/cyan]\n"
94
+ "[dim]• Son commit skorunu görmek için:[/dim] [cyan]codedna status[/cyan]\n"
95
+ "[dim]• Geçmiş skorları görmek için:[/dim] [cyan]codedna history[/cyan]\n"
96
+ "[dim]• API sunucuyu başlatmak için:[/dim] [cyan]codedna serve[/cyan]\n"
97
+ "[dim]• HTML rapor üretmek için:[/dim] [cyan]codedna report[/cyan]"
98
+ )
99
+ if with_ci:
100
+ bilgi += "\n[dim]• CI şablonu:[/dim] [cyan].github/workflows/codedna.yml[/cyan]"
101
+
102
+ console.print()
103
+ console.print(Panel(bilgi, border_style="green", padding=(1, 2)))
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # codedna scan
108
+ # ---------------------------------------------------------------------------
109
+ @app.command()
110
+ def scan(
111
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
112
+ max_files: int = typer.Option(200, "--max", "-m", help="Maksimum taranacak dosya sayısı"),
113
+ min_risk: float = typer.Option(0.0, "--min-risk", help="Minimum AI olasılığı filtresi (0.0-1.0)"),
114
+ ) -> None:
115
+ """Mevcut repo'yu tara ve AI risk raporu göster."""
116
+ console.print()
117
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Repo taranıyor...\n")
118
+
119
+ kok = repo or find_git_root() or Path.cwd()
120
+
121
+ with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
122
+ sonuclar = scan_repository(kok, max_files=max_files)
123
+
124
+ if not sonuclar:
125
+ console.print("[yellow]Taranacak desteklenen dosya bulunamadı.[/yellow]")
126
+ console.print("[dim]Desteklenen: .py .js .jsx .ts .tsx[/dim]")
127
+ return
128
+
129
+ # Min risk filtresi uygula
130
+ if min_risk > 0:
131
+ sonuclar = [s for s in sonuclar if s.ai_probability >= min_risk]
132
+
133
+ # AI olasılığına göre sırala (yüksekten düşüğe)
134
+ sonuclar.sort(key=lambda s: s.ai_probability, reverse=True)
135
+
136
+ # DB'den tüm dosya anlama skorlarını tek sorguda çek
137
+ db_yolu = _get_db(kok)
138
+ anlama_skorlari_map: dict[str, float] = {}
139
+ try:
140
+ anlama_skorlari_map = get_all_file_understanding_scores(db_path=db_yolu)
141
+ except Exception:
142
+ pass
143
+
144
+ # Tablo oluştur
145
+ tablo = Table(
146
+ title="",
147
+ border_style="dim",
148
+ show_lines=True,
149
+ header_style="bold",
150
+ )
151
+ tablo.add_column("Dosya", style="white", min_width=25)
152
+ tablo.add_column("AI Olasılığı", justify="center", min_width=14)
153
+ tablo.add_column("Karmaşıklık", justify="center", min_width=12)
154
+ tablo.add_column("Satır", justify="right", min_width=6)
155
+ tablo.add_column("Anlama Skoru", justify="center", min_width=14)
156
+
157
+ toplam_ai = 0.0
158
+ for s in sonuclar:
159
+ yuzde = int(s.ai_probability * 100)
160
+ ai_metin = f"{s.ai_color} %{yuzde}"
161
+
162
+ if s.complexity_label == "Yüksek":
163
+ karmasiklik = "[red]Yüksek[/red]"
164
+ elif s.complexity_label == "Orta":
165
+ karmasiklik = "[yellow]Orta[/yellow]"
166
+ else:
167
+ karmasiklik = "[green]Düşük[/green]"
168
+
169
+ # DB'den tek sorguda gelen map'ten anlama skorunu oku
170
+ anlama_skor = anlama_skorlari_map.get(s.file_path)
171
+ if anlama_skor is not None:
172
+ renk = "green" if anlama_skor >= 4.0 else "yellow" if anlama_skor >= 2.5 else "red"
173
+ anlama = f"[{renk}]✅ {anlama_skor:.1f}/5[/{renk}]"
174
+ else:
175
+ anlama = "[dim]⚠️ Bilinmiyor[/dim]"
176
+
177
+ goreceli = _kisalt_yol(s.file_path, str(kok))
178
+
179
+ tablo.add_row(goreceli, ai_metin, karmasiklik, str(s.total_lines), anlama)
180
+ toplam_ai += s.ai_probability
181
+
182
+ console.print(tablo)
183
+
184
+ # Özet satırı
185
+ ortalama_ai = (toplam_ai / len(sonuclar)) * 100 if sonuclar else 0
186
+ risk_etiketi, risk_renk = _risk_etiketi(ortalama_ai)
187
+
188
+ console.print(
189
+ f"\n[bold]Repo Özeti:[/bold] {len(sonuclar)} dosya tarandı · "
190
+ f"Ortalama AI olasılığı: [bold]%{ortalama_ai:.0f}[/bold] · "
191
+ f"Risk: [bold {risk_renk}]{risk_etiketi}[/bold {risk_renk}]\n"
192
+ )
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # codedna status
197
+ # ---------------------------------------------------------------------------
198
+ @app.command()
199
+ def status(
200
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
201
+ hook: bool = typer.Option(False, "--hook", hidden=True, help="Hook modunda çalış (anket sor)"),
202
+ ) -> None:
203
+ """Son commit'in skorunu göster (ve hook modunda anket sor)."""
204
+ console.print()
205
+ kok = repo or find_git_root() or Path.cwd()
206
+ db_yolu = _get_db(kok)
207
+
208
+ # DB yoksa init et
209
+ init_db(db_yolu)
210
+
211
+ git_repo = get_repo(kok)
212
+ if not git_repo:
213
+ console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
214
+ raise typer.Exit(1)
215
+
216
+ with console.status("[dim]Son commit analiz ediliyor...[/dim]"):
217
+ commit_hash, sonuclar = score_latest_commit(kok)
218
+
219
+ if not commit_hash:
220
+ console.print("[yellow]Henüz commit bulunamadı.[/yellow]")
221
+ return
222
+
223
+ # Commit bilgilerini al
224
+ try:
225
+ commit = git_repo.head.commit
226
+ yazar = f"{commit.author.name}"
227
+ zaman_dam = int(commit.committed_date)
228
+ mesaj = commit.message.strip().splitlines()[0][:60]
229
+ except Exception:
230
+ yazar = "Bilinmiyor"
231
+ zaman_dam = 0
232
+ mesaj = ""
233
+
234
+ # Anket (sadece hook modunda)
235
+ anlama_skoru: Optional[float] = None
236
+ if hook:
237
+ anlama_skoru = run_survey(commit_hash)
238
+
239
+ # DB'ye kaydet
240
+ save_commit(
241
+ commit_hash=commit_hash,
242
+ author=yazar,
243
+ timestamp=zaman_dam,
244
+ files_changed=len(sonuclar),
245
+ understanding_score=anlama_skoru,
246
+ db_path=db_yolu,
247
+ )
248
+ for s in sonuclar:
249
+ save_file_score(
250
+ commit_hash=commit_hash,
251
+ file_path=s.file_path,
252
+ ai_probability=s.ai_probability,
253
+ complexity_score=s.complexity_score,
254
+ comment_ratio=s.comment_ratio,
255
+ understanding_score=anlama_skoru,
256
+ db_path=db_yolu,
257
+ )
258
+
259
+ # Özet göster
260
+ if sonuclar:
261
+ ort_ai = sum(s.ai_probability for s in sonuclar) / len(sonuclar)
262
+ risk_etiketi, risk_renk = _risk_etiketi(ort_ai * 100)
263
+
264
+ anlama_goster = (
265
+ f"[bold green]{anlama_skoru:.1f}/5[/bold green]"
266
+ if anlama_skoru is not None
267
+ else "[dim]Anket yapılmadı[/dim]"
268
+ )
269
+
270
+ console.print(
271
+ Panel(
272
+ f"[bold]Commit:[/bold] [dim]{commit_hash[:8]}[/dim] [dim]{mesaj}[/dim]\n"
273
+ f"[bold]Yazar:[/bold] {yazar}\n"
274
+ f"[bold]Değişen kod dosyaları:[/bold] {len(sonuclar)}\n"
275
+ f"[bold]Ort. AI olasılığı:[/bold] [bold {risk_renk}]%{ort_ai*100:.0f} ({risk_etiketi})[/bold {risk_renk}]\n"
276
+ f"[bold]Anlama skoru:[/bold] {anlama_goster}",
277
+ title="[bold cyan]🧬 CodeDNA — Commit Skoru[/bold cyan]",
278
+ border_style="cyan",
279
+ padding=(1, 2),
280
+ )
281
+ )
282
+ console.print("[dim]Commit skoru kaydedildi.[/dim]\n")
283
+ else:
284
+ console.print(
285
+ f"[bold]Commit:[/bold] [dim]{commit_hash[:8]}[/dim]\n"
286
+ "[dim]Bu commit'te desteklenen kod dosyası bulunamadı.[/dim]"
287
+ )
288
+
289
+ # Post-commit hook: korumalı modül ihlallerini kontrol et ve uyar (bloklama YOK)
290
+ if hook:
291
+ try:
292
+ from codedna.protection import ihlal_uyarisi_goster
293
+ uyarilar = ihlal_uyarisi_goster(db_yolu)
294
+ for uyari in uyarilar:
295
+ console.print(f"[bold red]{uyari}[/bold red]")
296
+ except Exception:
297
+ pass # Hook'u asla bozmaz
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # codedna history
302
+ # ---------------------------------------------------------------------------
303
+ @app.command()
304
+ def history(
305
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
306
+ limit: int = typer.Option(20, "--limit", "-n", help="Gösterilecek commit sayısı"),
307
+ ) -> None:
308
+ """Geçmiş commit skorlarını tablo olarak göster."""
309
+ console.print()
310
+ kok = repo or find_git_root() or Path.cwd()
311
+ db_yolu = _get_db(kok)
312
+
313
+ init_db(db_yolu)
314
+ satirlar = get_commit_history(limit=limit, db_path=db_yolu)
315
+
316
+ if not satirlar:
317
+ console.print(
318
+ "[yellow]Henüz kayıtlı commit yok.[/yellow]\n"
319
+ "[dim]İpucu: 'codedna init' ile kurulum yapın, ardından commit atın.[/dim]"
320
+ )
321
+ return
322
+
323
+ tablo = Table(
324
+ title=f"[bold cyan]🧬 CodeDNA — Son {len(satirlar)} Commit[/bold cyan]",
325
+ border_style="dim",
326
+ show_lines=True,
327
+ header_style="bold",
328
+ )
329
+ tablo.add_column("Commit", style="dim", min_width=10)
330
+ tablo.add_column("Yazar", min_width=15)
331
+ tablo.add_column("Tarih", min_width=17)
332
+ tablo.add_column("Kod Dosyası", justify="right", min_width=11)
333
+ tablo.add_column("Anlama", justify="center", min_width=12)
334
+
335
+ for satir in satirlar:
336
+ tarih_str = (
337
+ datetime.fromtimestamp(satir["timestamp"]).strftime("%Y-%m-%d %H:%M")
338
+ if satir["timestamp"]
339
+ else "?"
340
+ )
341
+
342
+ if satir["understanding_score"] is not None:
343
+ skor = satir["understanding_score"]
344
+ if skor >= 4.0:
345
+ anlama = f"[green]✅ {skor:.1f}/5[/green]"
346
+ elif skor >= 2.5:
347
+ anlama = f"[yellow]🔶 {skor:.1f}/5[/yellow]"
348
+ else:
349
+ anlama = f"[red]🔴 {skor:.1f}/5[/red]"
350
+ else:
351
+ anlama = "[dim]⚠️ Yok[/dim]"
352
+
353
+ tablo.add_row(
354
+ satir["commit_hash"][:8],
355
+ satir["author"] or "?",
356
+ tarih_str,
357
+ str(satir["files_changed"] or 0),
358
+ anlama,
359
+ )
360
+
361
+ console.print(tablo)
362
+ console.print()
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # codedna serve
367
+ # ---------------------------------------------------------------------------
368
+ @app.command()
369
+ def serve(
370
+ host: str = typer.Option("127.0.0.1", "--host", help="Dinlenecek IP adresi"),
371
+ port: int = typer.Option(8000, "--port", "-p", help="Port numarası"),
372
+ reload: bool = typer.Option(False, "--reload", help="Geliştirme modunda otomatik yeniden başlat"),
373
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
374
+ ) -> None:
375
+ """FastAPI REST sunucusunu başlat."""
376
+ import os
377
+ import uvicorn
378
+
379
+ kok = repo or find_git_root() or Path.cwd()
380
+ db_yolu = _get_db(kok)
381
+
382
+ # Ortam değişkenlerini ayarla (api.py tarafından okunur)
383
+ os.environ.setdefault("CODEDNA_REPO_PATH", str(kok))
384
+ os.environ.setdefault("CODEDNA_DB_PATH", str(db_yolu))
385
+
386
+ init_db(db_yolu)
387
+
388
+ console.print()
389
+ console.print(
390
+ Panel(
391
+ f"[bold cyan]🧬 CodeDNA API[/bold cyan] başlatılıyor...\n\n"
392
+ f"[bold]Adres:[/bold] [link]http://{host}:{port}[/link]\n"
393
+ f"[bold]Docs:[/bold] [link]http://{host}:{port}/docs[/link]\n"
394
+ f"[bold]Repo:[/bold] [dim]{kok}[/dim]\n"
395
+ f"[bold]Veritabanı:[/bold] [dim]{db_yolu}[/dim]\n\n"
396
+ "[dim]Durdurmak için Ctrl+C[/dim]",
397
+ border_style="cyan",
398
+ padding=(1, 2),
399
+ )
400
+ )
401
+
402
+ uvicorn.run(
403
+ "codedna.api:app",
404
+ host=host,
405
+ port=port,
406
+ reload=reload,
407
+ log_level="info",
408
+ )
409
+
410
+
411
+ # ---------------------------------------------------------------------------
412
+ # codedna report
413
+ # ---------------------------------------------------------------------------
414
+ @app.command()
415
+ def report(
416
+ output: Path = typer.Option(Path("codedna-report.html"), "--output", "-o", help="Çıktı dosyası"),
417
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
418
+ open_browser: bool = typer.Option(False, "--open", help="Raporu tarayıcıda aç"),
419
+ ) -> None:
420
+ """HTML rapor oluştur."""
421
+ import webbrowser
422
+ from datetime import datetime
423
+ from codedna.api import _rapor_html_olustur
424
+
425
+ kok = repo or find_git_root() or Path.cwd()
426
+ db_yolu = _get_db(kok)
427
+ init_db(db_yolu)
428
+
429
+ console.print()
430
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — HTML rapor oluşturuluyor...\n")
431
+
432
+ with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
433
+ sonuclar = scan_repository(kok, max_files=200)
434
+
435
+ sonuclar.sort(key=lambda s: s.ai_probability, reverse=True)
436
+ commitler = get_commit_history(limit=50, db_path=db_yolu)
437
+
438
+ tum_ai = [s.ai_probability for s in sonuclar]
439
+ ort_ai = sum(tum_ai) / len(tum_ai) if tum_ai else 0.0
440
+ risk_etkt, _ = _risk_etiketi(ort_ai * 100)
441
+
442
+ anlama_skorlari = [
443
+ float(c["understanding_score"])
444
+ for c in commitler
445
+ if c["understanding_score"] is not None
446
+ ]
447
+ ort_anlama = sum(anlama_skorlari) / len(anlama_skorlari) if anlama_skorlari else None
448
+
449
+ html = _rapor_html_olustur(
450
+ repo_adi=kok.name,
451
+ toplam_dosya=len(sonuclar),
452
+ ort_ai=ort_ai,
453
+ risk=risk_etkt,
454
+ ort_anlama=ort_anlama,
455
+ toplam_commit=len(commitler),
456
+ dosyalar=sonuclar,
457
+ commitler=commitler,
458
+ kok=kok,
459
+ )
460
+
461
+ output.write_text(html, encoding="utf-8")
462
+ console.print(f"[green]✓[/green] Rapor oluşturuldu: [bold]{output.resolve()}[/bold]")
463
+ console.print(
464
+ f" [dim]{len(sonuclar)} dosya · "
465
+ f"Ort. AI: %{ort_ai*100:.0f} · "
466
+ f"Risk: {risk_etkt}[/dim]"
467
+ )
468
+
469
+ if open_browser:
470
+ webbrowser.open(output.resolve().as_uri())
471
+ console.print("[dim]Tarayıcıda açılıyor...[/dim]")
472
+
473
+ console.print()
474
+
475
+
476
+ # ---------------------------------------------------------------------------
477
+ # codedna ai-compare
478
+ # ---------------------------------------------------------------------------
479
+ @app.command(name="ai-compare")
480
+ def ai_compare(
481
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
482
+ ) -> None:
483
+ """Repo genelinde AI araç bazlı karşılaştırma tablosu göster."""
484
+ from codedna.ai_fingerprint import compare_tools_in_repo
485
+ from codedna.plan import is_feature_available
486
+
487
+ if not is_feature_available("ai_comparison"):
488
+ console.print(
489
+ Panel(
490
+ "[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]\n"
491
+ "[dim]This feature is available on Enterprise plan.[/dim]\n\n"
492
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
493
+ border_style="yellow",
494
+ padding=(1, 2),
495
+ )
496
+ )
497
+ raise typer.Exit(1)
498
+
499
+ kok = repo or find_git_root() or Path.cwd()
500
+ db_yolu = _get_db(kok)
501
+ init_db(db_yolu)
502
+
503
+ console.print()
504
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — AI araç parmak izi analizi çalışıyor...\n")
505
+ console.print(
506
+ "[dim]⚠️ Bu tespit örüntü tabanlı bir tahmindir — kesin değildir.[/dim]\n"
507
+ )
508
+
509
+ with console.status("[dim]Dosyalar analiz ediliyor...[/dim]"):
510
+ sonuclar = compare_tools_in_repo(kok, db_yolu)
511
+
512
+ if not sonuclar:
513
+ console.print("[yellow]Analiz edilecek dosya bulunamadı.[/yellow]")
514
+ return
515
+
516
+ tablo = Table(border_style="dim", show_lines=True, header_style="bold")
517
+ tablo.add_column("AI Aracı", min_width=12)
518
+ tablo.add_column("Dosya Sayısı", justify="right", min_width=13)
519
+ tablo.add_column("Ort. AI Skoru", justify="right", min_width=13)
520
+ tablo.add_column("Ort. Anlama", justify="right", min_width=12)
521
+
522
+ arac_emojileri = {
523
+ "copilot": "🐙", "cursor": "🖱️",
524
+ "claude": "🧠", "unknown": "❓",
525
+ }
526
+
527
+ for arac, veri in sorted(sonuclar.items(), key=lambda x: -x[1].get("dosya_sayisi", 0)):
528
+ emoji = arac_emojileri.get(arac, "🤖")
529
+ anlama = (
530
+ f"{veri['avg_understanding']:.1f}/5"
531
+ if veri.get("avg_understanding") else "—"
532
+ )
533
+ tablo.add_row(
534
+ f"{emoji} {arac}",
535
+ str(veri.get("dosya_sayisi", 0)),
536
+ f"%{veri.get('avg_ai_probability', 0) * 100:.0f}",
537
+ anlama,
538
+ )
539
+
540
+ console.print(tablo)
541
+ console.print()
542
+
543
+
544
+ # ---------------------------------------------------------------------------
545
+ # codedna onboarding
546
+ # ---------------------------------------------------------------------------
547
+ @app.command()
548
+ def onboarding(
549
+ author: Optional[str] = typer.Option(None, "--author", "-a", help="Tek yazar analizi"),
550
+ team: bool = typer.Option(False, "--team", "-t", help="Takım geneli özet"),
551
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
552
+ ) -> None:
553
+ """Geliştirici onboarding hızını ölç ve ramp-up eğrisini göster."""
554
+ from codedna.onboarding import (
555
+ get_author_curve, team_onboarding_summary, get_all_authors,
556
+ )
557
+ from codedna.plan import is_feature_available
558
+
559
+ if not is_feature_available("sprint_health"):
560
+ console.print(
561
+ Panel(
562
+ "[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
563
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
564
+ border_style="yellow",
565
+ )
566
+ )
567
+ raise typer.Exit(1)
568
+
569
+ kok = repo or find_git_root() or Path.cwd()
570
+ db_yolu = _get_db(kok)
571
+ init_db(db_yolu)
572
+
573
+ console.print()
574
+
575
+ if team or not author:
576
+ # Takım özeti
577
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Onboarding takım özeti\n")
578
+
579
+ with console.status("[dim]Analiz ediliyor...[/dim]"):
580
+ ozet = team_onboarding_summary(db_yolu)
581
+
582
+ if not ozet:
583
+ console.print("[yellow]Kayıtlı yazar bulunamadı.[/yellow]")
584
+ return
585
+
586
+ tablo = Table(
587
+ title="[bold cyan]🚀 Onboarding Özeti[/bold cyan]",
588
+ border_style="dim",
589
+ show_lines=True,
590
+ header_style="bold",
591
+ )
592
+ tablo.add_column("Yazar", min_width=18)
593
+ tablo.add_column("Commit", justify="right", min_width=8)
594
+ tablo.add_column("Anketli", justify="right", min_width=9)
595
+ tablo.add_column("Ramp-up", justify="center", min_width=12)
596
+ tablo.add_column("Son Anlama", justify="center", min_width=12)
597
+
598
+ for y in ozet:
599
+ ramp_str = (
600
+ f"{y['ramp_up_hafta']:.1f} hafta"
601
+ if y["ramp_up_hafta"] is not None
602
+ else ("[dim]Yeterli veri yok[/dim]" if not y["yeterli_veri"] else "[yellow]Eşik aşılmadı[/yellow]")
603
+ )
604
+ anlama_str = (
605
+ f"{y['son_ort_anlama']:.1f}/5" if y["son_ort_anlama"] else "—"
606
+ )
607
+ tablo.add_row(
608
+ y["yazar"],
609
+ str(y["toplam_commit"]),
610
+ str(y["anlama_skoru_olan"]),
611
+ ramp_str,
612
+ anlama_str,
613
+ )
614
+
615
+ console.print(tablo)
616
+
617
+ else:
618
+ # Tek yazar eğrisi
619
+ console.print(f"[bold cyan]🧬 CodeDNA[/bold cyan] — [bold]{author}[/bold] onboarding eğrisi\n")
620
+
621
+ with console.status("[dim]Analiz ediliyor...[/dim]"):
622
+ egri = get_author_curve(author, db_yolu)
623
+
624
+ if egri.toplam_commit == 0:
625
+ console.print(f"[yellow]'{author}' yazarına ait commit bulunamadı.[/yellow]")
626
+ return
627
+
628
+ # Ramp-up panel
629
+ ramp_str = (
630
+ f"[green]{egri.ramp_up_hafta:.1f} hafta[/green]"
631
+ if egri.ramp_up_hafta is not None
632
+ else "[yellow]Eşik henüz aşılmadı[/yellow]"
633
+ if egri.anlama_skoru_olan >= 5
634
+ else "[dim]Yeterli veri yok (en az 5 commit)[/dim]"
635
+ )
636
+
637
+ anlama_str = (
638
+ f"{egri.son_ort_anlama:.1f}/5" if egri.son_ort_anlama else "—"
639
+ )
640
+
641
+ console.print(
642
+ Panel(
643
+ f"[bold]Yazar:[/bold] {egri.yazar}\n"
644
+ f"[bold]Toplam commit:[/bold] {egri.toplam_commit}\n"
645
+ f"[bold]Anketli commit:[/bold] {egri.anlama_skoru_olan}\n"
646
+ f"[bold]Tahmini ramp-up:[/bold] {ramp_str}\n"
647
+ f"[bold]Son ort. anlama:[/bold] {anlama_str}",
648
+ title="[bold cyan]🚀 Onboarding Analizi[/bold cyan]",
649
+ border_style="cyan",
650
+ padding=(1, 2),
651
+ )
652
+ )
653
+
654
+ # Commit bazlı tablo (anket verisi olanlar)
655
+ anketli = [n for n in egri.noktalar if n.understanding_score is not None]
656
+ if anketli:
657
+ tablo = Table(border_style="dim", show_lines=True, header_style="bold")
658
+ tablo.add_column("#", justify="right", min_width=4)
659
+ tablo.add_column("Hash", style="dim", min_width=10)
660
+ tablo.add_column("Tarih", min_width=12)
661
+ tablo.add_column("Hafta", justify="right", min_width=7)
662
+ tablo.add_column("Anlama", justify="center", min_width=10)
663
+
664
+ for n in anketli:
665
+ skor = n.understanding_score or 0.0
666
+ renk = "green" if skor >= 4 else "yellow" if skor >= 2.5 else "red"
667
+ tablo.add_row(
668
+ str(n.commit_no),
669
+ n.commit_hash[:8],
670
+ n.tarih.strftime("%Y-%m-%d"),
671
+ str(n.hafta_no),
672
+ f"[{renk}]{skor:.1f}/5[/{renk}]",
673
+ )
674
+ console.print(tablo)
675
+
676
+ console.print()
677
+
678
+
679
+ # ---------------------------------------------------------------------------
680
+ # codedna pr-comment
681
+ # ---------------------------------------------------------------------------
682
+ @app.command(name="pr-comment")
683
+ def pr_comment(
684
+ repo: Optional[str] = typer.Option(None, "--repo", help="owner/repo formatında repo (boşsa GITHUB_REPOSITORY env'den alınır)"),
685
+ pr: Optional[int] = typer.Option(None, "--pr", help="PR numarası (boşsa event payload'dan algılanır)"),
686
+ rate: float = typer.Option(75.0, "--rate", help="Teknik borç saatlik ücreti"),
687
+ ) -> None:
688
+ """GitHub PR'ına CodeDNA analiz yorumu bırak (mevcut yorumu günceller, spam yapmaz)."""
689
+ import os
690
+ from codedna.integrations.github_bot import (
691
+ format_pr_comment, post_or_update_comment, github_actions_pr_bilgisi,
692
+ )
693
+ from codedna.tech_debt import calculate_repo_debt
694
+
695
+ # Token — asla loglanmaz
696
+ token = os.environ.get("GITHUB_TOKEN")
697
+ if not token:
698
+ console.print(
699
+ "[bold red]Hata:[/bold red] GITHUB_TOKEN ortam değişkeni tanımlanmamış."
700
+ )
701
+ raise typer.Exit(1)
702
+
703
+ # Repo ve PR numarası — önce parametre, sonra GitHub Actions env
704
+ hedef_repo = repo
705
+ hedef_pr = pr
706
+
707
+ if not hedef_repo or not hedef_pr:
708
+ bilgi = github_actions_pr_bilgisi()
709
+ if bilgi:
710
+ otomatik_repo, otomatik_pr = bilgi
711
+ hedef_repo = hedef_repo or otomatik_repo
712
+ hedef_pr = hedef_pr or otomatik_pr
713
+
714
+ if not hedef_repo or not hedef_pr:
715
+ console.print(
716
+ "[bold red]Hata:[/bold red] Repo ve PR numarası bulunamadı.\n"
717
+ "[dim]--repo owner/repo --pr 42 ile belirtin veya GitHub Actions içinde çalıştırın.[/dim]"
718
+ )
719
+ raise typer.Exit(1)
720
+
721
+ kok = find_git_root() or Path.cwd()
722
+ db_yolu = _get_db(kok)
723
+ init_db(db_yolu)
724
+
725
+ console.print(f"\n[bold cyan]🧬 CodeDNA[/bold cyan] — PR #{hedef_pr} analiz ediliyor...\n")
726
+
727
+ with console.status("[dim]Dosyalar taranıyor...[/dim]"):
728
+ from codedna.scorer import scan_repository
729
+ sonuclar = scan_repository(kok, max_files=200)
730
+
731
+ debt_ozeti_dict: Optional[dict] = None
732
+ try:
733
+ debt = calculate_repo_debt(kok, db_yolu, hourly_rate=rate)
734
+ debt_ozeti_dict = {
735
+ "toplam_debt_saatleri": debt.toplam_debt_saatleri,
736
+ "toplam_aylik_maliyet_usd": debt.toplam_aylik_maliyet_usd,
737
+ }
738
+ except Exception:
739
+ pass
740
+
741
+ yorum = format_pr_comment(sonuclar, debt_ozeti_dict)
742
+
743
+ try:
744
+ sonuc = post_or_update_comment(hedef_repo, hedef_pr, yorum, token)
745
+ yorum_url = sonuc.get("html_url", "")
746
+ console.print(
747
+ f"[green]✓[/green] PR yorumu gönderildi: [dim]{yorum_url}[/dim]\n"
748
+ )
749
+ except RuntimeError as e:
750
+ console.print(f"[bold red]Hata:[/bold red] GitHub API isteği başarısız: {e}")
751
+ raise typer.Exit(1)
752
+
753
+
754
+ # ---------------------------------------------------------------------------
755
+ # codedna protect
756
+ # ---------------------------------------------------------------------------
757
+ protect_app = typer.Typer(help="Korumalı modül yönetimi.")
758
+ app.add_typer(protect_app, name="protect")
759
+
760
+
761
+ @protect_app.command("add")
762
+ def protect_add(
763
+ file_path: str = typer.Argument(..., help="Korunacak dosya yolu"),
764
+ threshold: float = typer.Option(3.5, "--threshold", "-t", help="Minimum anlama skoru eşiği"),
765
+ label: str = typer.Option("", "--label", "-l", help="İnsan okunabilir etiket"),
766
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
767
+ ) -> None:
768
+ """Bir dosyayı korumalı modül olarak işaretle."""
769
+ from codedna.protection import protect_module
770
+ from codedna.plan import is_feature_available
771
+
772
+ if not is_feature_available("bus_factor"):
773
+ console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
774
+ raise typer.Exit(1)
775
+
776
+ kok = repo or find_git_root() or Path.cwd()
777
+ db_yolu = _get_db(kok)
778
+ init_db(db_yolu)
779
+
780
+ # Göreli yolu tam yola çevir
781
+ tam_yol = str((kok / file_path).resolve())
782
+ etiket = label or file_path
783
+ yazar = "cli"
784
+
785
+ kayit_id = protect_module(tam_yol, threshold, etiket, yazar, db_yolu)
786
+ console.print(
787
+ f"[green]✓[/green] Korumalı modül eklendi: [cyan]{file_path}[/cyan]\n"
788
+ f" [dim]Etiket:[/dim] {etiket} · [dim]Eşik:[/dim] {threshold}/5 · [dim]ID:[/dim] #{kayit_id}"
789
+ )
790
+
791
+
792
+ @protect_app.command("remove")
793
+ def protect_remove(
794
+ file_path: str = typer.Argument(..., help="Koruma kaldırılacak dosya yolu"),
795
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
796
+ ) -> None:
797
+ """Bir dosyadan korumayı kaldır."""
798
+ from codedna.protection import unprotect_module
799
+ from codedna.plan import is_feature_available
800
+
801
+ if not is_feature_available("bus_factor"):
802
+ console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
803
+ raise typer.Exit(1)
804
+
805
+ kok = repo or find_git_root() or Path.cwd()
806
+ db_yolu = _get_db(kok)
807
+ tam_yol = str((kok / file_path).resolve())
808
+
809
+ if unprotect_module(tam_yol, db_yolu):
810
+ console.print(f"[green]✓[/green] Koruma kaldırıldı: [cyan]{file_path}[/cyan]")
811
+ else:
812
+ console.print(f"[yellow]Bulunamadı:[/yellow] '{file_path}' korumalı modüller arasında yok.")
813
+
814
+
815
+ @protect_app.command("list")
816
+ def protect_list(
817
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
818
+ ) -> None:
819
+ """Tüm korumalı modülleri ve durumlarını göster."""
820
+ from codedna.protection import check_protected_modules
821
+ from codedna.plan import is_feature_available
822
+
823
+ if not is_feature_available("bus_factor"):
824
+ console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
825
+ raise typer.Exit(1)
826
+
827
+ kok = repo or find_git_root() or Path.cwd()
828
+ db_yolu = _get_db(kok)
829
+ init_db(db_yolu)
830
+
831
+ moduller = check_protected_modules(db_yolu)
832
+ if not moduller:
833
+ console.print("[yellow]Henüz korumalı modül yok.[/yellow]")
834
+ console.print("[dim]Eklemek için:[/dim] [cyan]codedna protect add <dosya> --label 'Etiket'[/cyan]")
835
+ return
836
+
837
+ tablo = Table(
838
+ title="[bold cyan]🛡️ Korumalı Modüller[/bold cyan]",
839
+ border_style="dim", show_lines=True, header_style="bold",
840
+ )
841
+ tablo.add_column("Dosya", min_width=30)
842
+ tablo.add_column("Etiket", min_width=16)
843
+ tablo.add_column("Eşik", justify="center", min_width=7)
844
+ tablo.add_column("Mevcut", justify="center", min_width=9)
845
+ tablo.add_column("Durum", justify="center", min_width=12)
846
+
847
+ for m in moduller:
848
+ mevcut_str = f"{m.mevcut_skor:.1f}" if m.mevcut_skor is not None else "—"
849
+ if m.durum == "İHLAL":
850
+ durum_str = "[bold red]🔴 İHLAL[/bold red]"
851
+ elif m.durum == "GÜVENLİ":
852
+ durum_str = "[green]✅ GÜVENLİ[/green]"
853
+ else:
854
+ durum_str = "[dim]⚪ BİLİNMİYOR[/dim]"
855
+
856
+ try:
857
+ goreceli = str(Path(m.dosya_yolu).relative_to(kok))
858
+ except ValueError:
859
+ goreceli = m.dosya_yolu[-40:]
860
+
861
+ tablo.add_row(goreceli, m.etiket, f"{m.esik:.1f}", mevcut_str, durum_str)
862
+
863
+ console.print()
864
+ console.print(tablo)
865
+ console.print()
866
+
867
+
868
+ @protect_app.command("check")
869
+ def protect_check(
870
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
871
+ ) -> None:
872
+ """Sadece eşik altına düşmüş (ihlaldeki) modülleri göster."""
873
+ from codedna.protection import get_violations
874
+ from codedna.plan import is_feature_available
875
+
876
+ if not is_feature_available("bus_factor"):
877
+ console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
878
+ raise typer.Exit(1)
879
+
880
+ kok = repo or find_git_root() or Path.cwd()
881
+ db_yolu = _get_db(kok)
882
+ init_db(db_yolu)
883
+
884
+ ihlaller = get_violations(db_yolu)
885
+ if not ihlaller:
886
+ console.print("[green]✅ Tüm korumalı modüller güvende.[/green]")
887
+ return
888
+
889
+ console.print()
890
+ for ihlal in ihlaller:
891
+ skor_str = f"{ihlal.mevcut_skor:.1f}" if ihlal.mevcut_skor else "?"
892
+ try:
893
+ goreceli = str(Path(ihlal.dosya_yolu).relative_to(kok))
894
+ except ValueError:
895
+ goreceli = ihlal.dosya_yolu
896
+ console.print(
897
+ f"[bold red]⚠️ İHLAL:[/bold red] [cyan]{goreceli}[/cyan] — "
898
+ f"anlama: [red]{skor_str}[/red] < eşik: [yellow]{ihlal.esik:.1f}[/yellow] "
899
+ f"([dim]{ihlal.etiket}[/dim])"
900
+ )
901
+ console.print()
902
+
903
+
904
+ # ---------------------------------------------------------------------------
905
+ # codedna interview
906
+ # ---------------------------------------------------------------------------
907
+ interview_app = typer.Typer(help="Aday mülakat aracı.")
908
+ app.add_typer(interview_app, name="interview")
909
+
910
+
911
+ @interview_app.command("start")
912
+ def interview_start(
913
+ candidate: str = typer.Option(..., "--candidate", "-c", help="Aday adı"),
914
+ difficulty: str = typer.Option("medium", "--difficulty", "-d", help="Zorluk: easy|medium|hard"),
915
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
916
+ ) -> None:
917
+ """Yeni mülakat oturumu başlat ve anonimleştirilmiş kod göster."""
918
+ from codedna.interview import select_candidate_file, generate_questions, start_session
919
+ from codedna.plan import is_feature_available
920
+
921
+ if not is_feature_available("interview_tool"):
922
+ console.print(
923
+ Panel(
924
+ "[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]\n"
925
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
926
+ border_style="yellow",
927
+ )
928
+ )
929
+ raise typer.Exit(1)
930
+
931
+ kok = repo or find_git_root() or Path.cwd()
932
+ db_yolu = _get_db(kok)
933
+ init_db(db_yolu)
934
+
935
+ console.print()
936
+ console.print(
937
+ "[dim]⚠️ Bu araç insan değerlendirmesinin YERİNE GEÇMEZ — tamamlayıcı bir sinyaldir.[/dim]\n"
938
+ )
939
+
940
+ with console.status("[dim]Uygun dosya seçiliyor...[/dim]"):
941
+ dosya = select_candidate_file(kok, db_yolu, difficulty)
942
+
943
+ if not dosya:
944
+ console.print(f"[yellow]'{difficulty}' zorluğunda uygun dosya bulunamadı.[/yellow]")
945
+ raise typer.Exit(1)
946
+
947
+ sorular = generate_questions(dosya.anonimlestirilmis_kod)
948
+ session_id = start_session(candidate, dosya.dosya_yolu, sorular, db_yolu)
949
+
950
+ console.print(
951
+ Panel(
952
+ f"[bold]Aday:[/bold] {candidate}\n"
953
+ f"[bold]Zorluk:[/bold] {difficulty} · [dim]Karmaşıklık:[/dim] {dosya.karmasiklik_skoru:.0f} · [dim]Satır:[/dim] {dosya.satir_sayisi}\n"
954
+ f"[bold]Oturum ID:[/bold] [cyan]#{session_id}[/cyan]",
955
+ title="[bold cyan]🎯 Mülakat Başladı[/bold cyan]",
956
+ border_style="cyan",
957
+ padding=(1, 2),
958
+ )
959
+ )
960
+
961
+ console.print("\n[bold]─── Anonimleştirilmiş Kod ───[/bold]")
962
+ console.print(f"[dim]{dosya.anonimlestirilmis_kod[:800]}[/dim]")
963
+ if len(dosya.anonimlestirilmis_kod) > 800:
964
+ console.print("[dim]... (kısaltıldı)[/dim]")
965
+
966
+ console.print("\n[bold]─── Sorular ───[/bold]")
967
+ for i, soru in enumerate(sorular, 1):
968
+ console.print(f" [bold cyan]{i}.[/bold cyan] {soru}")
969
+
970
+ console.print(
971
+ f"\n[dim]Değerlendirme için:[/dim] [cyan]codedna interview score {session_id} --score 4.0 --notes 'Not'[/cyan]\n"
972
+ )
973
+
974
+
975
+ @interview_app.command("list")
976
+ def interview_list(
977
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
978
+ limit: int = typer.Option(10, "--limit", "-n", help="Gösterilecek oturum sayısı"),
979
+ ) -> None:
980
+ """Geçmiş mülakat oturumlarını göster."""
981
+ from codedna.interview import get_sessions
982
+ from codedna.plan import is_feature_available
983
+
984
+ if not is_feature_available("interview_tool"):
985
+ console.print("[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]")
986
+ raise typer.Exit(1)
987
+
988
+ kok = repo or find_git_root() or Path.cwd()
989
+ db_yolu = _get_db(kok)
990
+ init_db(db_yolu)
991
+
992
+ oturumlar = get_sessions(db_yolu, limit=limit)
993
+ if not oturumlar:
994
+ console.print("[yellow]Henüz mülakat oturumu yok.[/yellow]")
995
+ return
996
+
997
+ tablo = Table(
998
+ title="[bold cyan]🎯 Mülakat Geçmişi[/bold cyan]",
999
+ border_style="dim", show_lines=True, header_style="bold",
1000
+ )
1001
+ tablo.add_column("#", justify="right", min_width=4)
1002
+ tablo.add_column("Aday", min_width=16)
1003
+ tablo.add_column("Başlangıç", min_width=17)
1004
+ tablo.add_column("Skor", justify="center", min_width=8)
1005
+ tablo.add_column("Notlar", min_width=20)
1006
+
1007
+ for o in oturumlar:
1008
+ skor_str = f"{o['skor']:.1f}/5" if o["skor"] is not None else "[dim]—[/dim]"
1009
+ tablo.add_row(
1010
+ str(o["id"]),
1011
+ o["aday"] or "?",
1012
+ o["baslangic"] or "?",
1013
+ skor_str,
1014
+ (o["notlar"] or "")[:30],
1015
+ )
1016
+
1017
+ console.print()
1018
+ console.print(tablo)
1019
+ console.print()
1020
+
1021
+
1022
+ @interview_app.command("score")
1023
+ def interview_score(
1024
+ session_id: int = typer.Argument(..., help="Oturum ID'si"),
1025
+ score: float = typer.Option(..., "--score", "-s", help="0.0–5.0 arası puan"),
1026
+ notes: str = typer.Option("", "--notes", "-n", help="Değerlendirici notları"),
1027
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1028
+ ) -> None:
1029
+ """Mülakat oturumuna insan değerlendirmesi puanı ekle."""
1030
+ from codedna.interview import submit_score
1031
+ from codedna.plan import is_feature_available
1032
+
1033
+ if not is_feature_available("interview_tool"):
1034
+ console.print("[bold yellow]🔒 Bu özellik Enterprise planında mevcut.[/bold yellow]")
1035
+ raise typer.Exit(1)
1036
+
1037
+ kok = repo or find_git_root() or Path.cwd()
1038
+ db_yolu = _get_db(kok)
1039
+
1040
+ try:
1041
+ sonuc = submit_score(session_id, score, notes, db_yolu)
1042
+ console.print(
1043
+ f"[green]✓[/green] Değerlendirme kaydedildi: "
1044
+ f"Oturum [cyan]#{session_id}[/cyan] → [bold]{score:.1f}/5[/bold]"
1045
+ )
1046
+ if notes:
1047
+ console.print(f" [dim]Not:[/dim] {notes}")
1048
+ except ValueError as e:
1049
+ console.print(f"[bold red]Hata:[/bold red] {e}")
1050
+ raise typer.Exit(1)
1051
+
1052
+
1053
+ # ---------------------------------------------------------------------------
1054
+ # codedna bus-factor
1055
+ # ---------------------------------------------------------------------------
1056
+ @app.command(name="bus-factor")
1057
+ def bus_factor(
1058
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1059
+ critical: bool = typer.Option(False, "--critical", "-c", help="Sadece kritik (bus_factor=1) dosyaları göster"),
1060
+ max_files: int = typer.Option(500, "--max", "-m", help="İşlenecek maksimum dosya sayısı"),
1061
+ ) -> None:
1062
+ """Repo genelinde bus factor analizi yap ve kritik sahiplik risklerini göster."""
1063
+ from codedna.bus_factor import calculate_bus_factor, get_at_risk_files, _BUYUK_REPO_ESIGI
1064
+ from codedna.plan import is_feature_available
1065
+
1066
+ # Plan kontrolü
1067
+ if not is_feature_available("bus_factor"):
1068
+ console.print(
1069
+ Panel(
1070
+ "[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
1071
+ "[dim]This feature is available on Team plan.[/dim]\n\n"
1072
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
1073
+ border_style="yellow",
1074
+ padding=(1, 2),
1075
+ )
1076
+ )
1077
+ raise typer.Exit(1)
1078
+
1079
+ kok = repo or find_git_root() or Path.cwd()
1080
+ db_yolu = _get_db(kok)
1081
+ init_db(db_yolu)
1082
+
1083
+ if max_files > _BUYUK_REPO_ESIGI:
1084
+ console.print(
1085
+ f"[yellow]⚠[/yellow] {max_files} dosya taranacak — büyük repolar için yavaş olabilir."
1086
+ )
1087
+
1088
+ console.print()
1089
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Bus Factor analizi çalışıyor...\n")
1090
+
1091
+ with console.status("[dim]git blame çalıştırılıyor...[/dim]"):
1092
+ if critical:
1093
+ sonuclar = get_at_risk_files(kok, db_yolu)
1094
+ else:
1095
+ sonuclar = calculate_bus_factor(kok, db_yolu, max_dosya=max_files)
1096
+
1097
+ if not sonuclar:
1098
+ console.print("[yellow]Analiz edilecek dosya bulunamadı.[/yellow]")
1099
+ return
1100
+
1101
+ # Tablo
1102
+ tablo = Table(
1103
+ title="",
1104
+ border_style="dim",
1105
+ show_lines=True,
1106
+ header_style="bold",
1107
+ )
1108
+ tablo.add_column("Dosya", style="white", min_width=30)
1109
+ tablo.add_column("Bus Factor", justify="center", min_width=11)
1110
+ tablo.add_column("Ana Sahip", min_width=16)
1111
+ tablo.add_column("Sahiplik %", justify="right", min_width=11)
1112
+ tablo.add_column("Risk", justify="center", min_width=10)
1113
+
1114
+ kritik_sayisi = 0
1115
+ for s in sonuclar:
1116
+ bf_str = str(s.bus_factor)
1117
+ if s.risk == "KRİTİK":
1118
+ bf_goster = f"[red]🚌 {bf_str}[/red]"
1119
+ risk_goster = "[bold red]KRİTİK[/bold red]"
1120
+ kritik_sayisi += 1
1121
+ elif s.risk == "RİSKLİ":
1122
+ bf_goster = f"[yellow]🚌 {bf_str}[/yellow]"
1123
+ risk_goster = "[yellow]RİSKLİ[/yellow]"
1124
+ else:
1125
+ bf_goster = f"[green]🚌 {bf_str}[/green]"
1126
+ risk_goster = "[green]GÜVENLİ[/green]"
1127
+
1128
+ tablo.add_row(
1129
+ s.dosya_yolu,
1130
+ bf_goster,
1131
+ s.birincil_sahip or "?",
1132
+ f"%{s.sahiplik_yuzdesi:.1f}",
1133
+ risk_goster,
1134
+ )
1135
+
1136
+ console.print(tablo)
1137
+ console.print(
1138
+ f"\n[bold]Özet:[/bold] {len(sonuclar)} dosya · "
1139
+ f"[red]{kritik_sayisi} kritik[/red] · "
1140
+ f"[dim]Eşik: anlama skoru ≥ 3.5[/dim]\n"
1141
+ )
1142
+
1143
+
1144
+ # ---------------------------------------------------------------------------
1145
+ # codedna debt
1146
+ # ---------------------------------------------------------------------------
1147
+ @app.command()
1148
+ def debt(
1149
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1150
+ rate: float = typer.Option(75.0, "--rate", help="Saatlik maliyet ($/saat)"),
1151
+ file: Optional[Path] = typer.Option(None, "--file", "-f", help="Tek dosya analizi"),
1152
+ ) -> None:
1153
+ """Repo genelinde teknik borç maliyeti hesapla."""
1154
+ from codedna.tech_debt import calculate_repo_debt, calculate_file_debt
1155
+ from codedna.plan import get_current_plan, Plan
1156
+
1157
+ kok = repo or find_git_root() or Path.cwd()
1158
+ db_yolu = _get_db(kok)
1159
+ init_db(db_yolu)
1160
+
1161
+ mevcut_plan = get_current_plan()
1162
+ dolar_gizli = mevcut_plan == Plan.FREE # Free planda dolar tutarı gizli
1163
+
1164
+ console.print()
1165
+ console.print("[bold cyan]🧬 CodeDNA[/bold cyan] — Teknik borç hesaplanıyor...\n")
1166
+
1167
+ # Tek dosya modu
1168
+ if file:
1169
+ # Göreli yolu tam yola çevir
1170
+ tam_dosya = (kok / file).resolve() if not file.is_absolute() else file
1171
+ with console.status("[dim]Analiz ediliyor...[/dim]"):
1172
+ borc = calculate_file_debt(str(tam_dosya), db_yolu, hourly_rate=rate)
1173
+
1174
+ if not borc:
1175
+ console.print(f"[yellow]Uyarı:[/yellow] '{file}' için veri bulunamadı.")
1176
+ return
1177
+
1178
+ risk_renk = {"KRİTİK": "red", "YÜKSEK": "yellow", "ORTA": "yellow", "DÜŞÜK": "green"}.get(
1179
+ borc.risk_seviyesi, "white"
1180
+ )
1181
+ maliyet_str = (
1182
+ f"[dim]Pro+ plan gerekli[/dim]"
1183
+ if dolar_gizli
1184
+ else f"[bold green]${borc.aylik_maliyet_usd:.2f}/ay[/bold green]"
1185
+ )
1186
+
1187
+ console.print(
1188
+ Panel(
1189
+ f"[bold]Dosya:[/bold] [dim]{borc.dosya_yolu}[/dim]\n"
1190
+ f"[bold]Borç saati:[/bold] {borc.debt_saatleri:.1f} saat\n"
1191
+ f"[bold]Aylık maliyet:[/bold] {maliyet_str}\n"
1192
+ f"[bold]Risk:[/bold] [{risk_renk}]{borc.risk_seviyesi}[/{risk_renk}]\n"
1193
+ f"[bold]AI olasılığı:[/bold] %{borc.ai_olasiligi*100:.0f} · "
1194
+ f"[bold]Karmaşıklık:[/bold] {borc.karmasiklik:.0f} · "
1195
+ f"[bold]Satır:[/bold] {borc.toplam_satir}",
1196
+ title="[bold cyan]💰 Teknik Borç — Dosya Detayı[/bold cyan]",
1197
+ border_style="cyan",
1198
+ padding=(1, 2),
1199
+ )
1200
+ )
1201
+ return
1202
+
1203
+ # Repo genel modu
1204
+ with console.status("[dim]Tüm dosyalar analiz ediliyor...[/dim]"):
1205
+ ozet = calculate_repo_debt(kok, db_yolu, hourly_rate=rate)
1206
+
1207
+ maliyet_str = (
1208
+ "[dim]🔒 Pro+ plan gerekli[/dim]"
1209
+ if dolar_gizli
1210
+ else f"[bold green]${ozet.toplam_aylik_maliyet_usd:.2f}/ay[/bold green]"
1211
+ )
1212
+
1213
+ console.print(
1214
+ Panel(
1215
+ f"[bold]💰 Teknik Borç Özeti[/bold]\n\n"
1216
+ f"[bold]Toplam tahmini borç:[/bold] [cyan]{ozet.toplam_debt_saatleri:.1f} saat[/cyan]\n"
1217
+ f"[bold]Aylık maliyet:[/bold] {maliyet_str}\n"
1218
+ f"[bold]Saatlik ücret:[/bold] [dim]${rate:.0f}/saat[/dim]\n"
1219
+ f"[bold]Analiz edilen dosya:[/bold] {ozet.toplam_dosya}",
1220
+ border_style="cyan",
1221
+ padding=(1, 2),
1222
+ )
1223
+ )
1224
+
1225
+ if ozet.en_pahali_5:
1226
+ console.print("[bold]En maliyetli 5 dosya:[/bold]")
1227
+ tablo = Table(border_style="dim", show_lines=True, header_style="bold")
1228
+ tablo.add_column("Dosya", style="white", min_width=30)
1229
+ tablo.add_column("Borç Saati", justify="right", min_width=11)
1230
+ tablo.add_column("Aylık", justify="right", min_width=10)
1231
+ tablo.add_column("Risk", justify="center", min_width=10)
1232
+
1233
+ for d in ozet.en_pahali_5:
1234
+ risk_renk = {
1235
+ "KRİTİK": "red", "YÜKSEK": "yellow",
1236
+ "ORTA": "yellow", "DÜŞÜK": "green",
1237
+ }.get(d.risk_seviyesi, "white")
1238
+
1239
+ aylik = (
1240
+ "[dim]gizli[/dim]"
1241
+ if dolar_gizli
1242
+ else f"[{risk_renk}]${d.aylik_maliyet_usd:.2f}[/{risk_renk}]"
1243
+ )
1244
+
1245
+ # Dosya yolunu kısalt
1246
+ try:
1247
+ goreceli = str(Path(d.dosya_yolu).relative_to(kok))
1248
+ except ValueError:
1249
+ goreceli = d.dosya_yolu[-40:]
1250
+
1251
+ tablo.add_row(
1252
+ goreceli,
1253
+ f"{d.debt_saatleri:.1f} saat",
1254
+ aylik,
1255
+ f"[{risk_renk}]{d.risk_seviyesi}[/{risk_renk}]",
1256
+ )
1257
+ console.print(tablo)
1258
+
1259
+ if dolar_gizli:
1260
+ console.print(
1261
+ "\n[dim]💡 Dolar tutarları Pro+ planda görünür: [cyan]codedna plan activate <KEY>[/cyan][/dim]\n"
1262
+ )
1263
+ else:
1264
+ console.print()
1265
+
1266
+
1267
+ # ---------------------------------------------------------------------------
1268
+ # codedna sprint
1269
+ # ---------------------------------------------------------------------------
1270
+ sprint_app = typer.Typer(help="Sprint yönetimi ve sağlık skoru.")
1271
+ app.add_typer(sprint_app, name="sprint")
1272
+
1273
+
1274
+ @sprint_app.command("create")
1275
+ def sprint_olustur(
1276
+ name: str = typer.Option(..., "--name", "-n", help="Sprint ismi"),
1277
+ start: str = typer.Option(..., "--start", "-s", help="Başlangıç tarihi (YYYY-MM-DD)"),
1278
+ end: str = typer.Option(..., "--end", "-e", help="Bitiş tarihi (YYYY-MM-DD)"),
1279
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1280
+ ) -> None:
1281
+ """Yeni sprint oluştur ve sağlık skoru hesapla."""
1282
+ from datetime import datetime as dt
1283
+ from codedna.sprint_health import calculate_sprint_health, save_sprint_result
1284
+ from codedna.plan import is_feature_available
1285
+
1286
+ if not is_feature_available("sprint_health"):
1287
+ console.print(
1288
+ Panel(
1289
+ "[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
1290
+ "[dim]This feature is available on Team plan.[/dim]\n\n"
1291
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
1292
+ border_style="yellow",
1293
+ padding=(1, 2),
1294
+ )
1295
+ )
1296
+ raise typer.Exit(1)
1297
+
1298
+ # Tarihleri parse et
1299
+ try:
1300
+ baslangic = dt.fromisoformat(start)
1301
+ bitis = dt.fromisoformat(end)
1302
+ except ValueError:
1303
+ console.print(f"[bold red]Hata:[/bold red] Geçersiz tarih formatı. Kullanım: YYYY-MM-DD")
1304
+ raise typer.Exit(1)
1305
+
1306
+ if bitis <= baslangic:
1307
+ console.print("[bold red]Hata:[/bold red] Bitiş tarihi başlangıçtan sonra olmalı.")
1308
+ raise typer.Exit(1)
1309
+
1310
+ kok = repo or find_git_root() or Path.cwd()
1311
+ db_yolu = _get_db(kok)
1312
+ init_db(db_yolu)
1313
+
1314
+ console.print()
1315
+ console.print(f"[bold cyan]🧬 CodeDNA[/bold cyan] — [bold]{name}[/bold] sprint analizi çalışıyor...\n")
1316
+
1317
+ with console.status("[dim]Commit'ler analiz ediliyor...[/dim]"):
1318
+ sonuc = calculate_sprint_health(kok, db_yolu, baslangic, bitis, name)
1319
+
1320
+ sprint_id = save_sprint_result(sonuc, db_yolu)
1321
+
1322
+ durum_renk = {
1323
+ "SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"
1324
+ }.get(sonuc.durum, "white")
1325
+
1326
+ console.print(
1327
+ Panel(
1328
+ f"[bold]Sprint:[/bold] {sonuc.sprint_adi}\n"
1329
+ f"[bold]Tarih:[/bold] {start} → {end}\n"
1330
+ f"[bold]Sağlık Skoru:[/bold] [{durum_renk}]{sonuc.health_score:.1f}/100 ({sonuc.durum})[/{durum_renk}]\n"
1331
+ f"[bold]Toplam Commit:[/bold] {sonuc.toplam_commit}\n"
1332
+ f"[bold]Ort. Anlama:[/bold] {f'{sonuc.avg_understanding:.1f}/5' if sonuc.avg_understanding else 'Veri yok'}\n"
1333
+ f"[bold]AI Oranı:[/bold] %{sonuc.ai_orani * 100:.0f} yüksek riskli\n"
1334
+ f"[bold]Borç Delta:[/bold] {sonuc.debt_delta_saati:.1f} saat/commit",
1335
+ title=f"[bold cyan]🏃 Sprint Sağlık Raporu — #{sprint_id}[/bold cyan]",
1336
+ border_style=durum_renk,
1337
+ padding=(1, 2),
1338
+ )
1339
+ )
1340
+ console.print(f"[dim]Sprint #{sprint_id} kaydedildi.[/dim]\n")
1341
+
1342
+
1343
+ @sprint_app.command("health")
1344
+ def sprint_sagligi(
1345
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1346
+ ) -> None:
1347
+ """Son sprint'in sağlık skorunu göster."""
1348
+ from codedna.db import get_latest_sprint
1349
+ from codedna.plan import is_feature_available
1350
+
1351
+ if not is_feature_available("sprint_health"):
1352
+ console.print(
1353
+ Panel(
1354
+ "[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]\n"
1355
+ "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]",
1356
+ border_style="yellow",
1357
+ )
1358
+ )
1359
+ raise typer.Exit(1)
1360
+
1361
+ kok = repo or find_git_root() or Path.cwd()
1362
+ db_yolu = _get_db(kok)
1363
+ init_db(db_yolu)
1364
+
1365
+ sprint = get_latest_sprint(db_path=db_yolu)
1366
+ if not sprint:
1367
+ console.print("[yellow]Henüz kayıtlı sprint yok.[/yellow]")
1368
+ console.print("[dim]Oluşturmak için:[/dim] [cyan]codedna sprint create --name 'Sprint 1' --start 2026-06-01 --end 2026-06-14[/cyan]")
1369
+ return
1370
+
1371
+ skor = sprint["health_score"] or 0.0
1372
+ durum = "SAĞLIKLI" if skor >= 80 else "DİKKAT" if skor >= 50 else "RİSKLİ"
1373
+ durum_renk = {"SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"}[durum]
1374
+
1375
+ from datetime import datetime as dt
1376
+ bas = dt.fromtimestamp(sprint["start_date"]).strftime("%Y-%m-%d") if sprint["start_date"] else "?"
1377
+ bit = dt.fromtimestamp(sprint["end_date"]).strftime("%Y-%m-%d") if sprint["end_date"] else "?"
1378
+
1379
+ anlama_val = sprint["avg_understanding"]
1380
+ anlama_str = f"{anlama_val:.1f}/5" if anlama_val else "Veri yok"
1381
+ delta_val = sprint["debt_delta_hours"] or 0.0
1382
+
1383
+ console.print()
1384
+ console.print(
1385
+ Panel(
1386
+ f"[bold]Sprint:[/bold] {sprint['sprint_name']}\n"
1387
+ f"[bold]Tarih:[/bold] {bas} → {bit}\n"
1388
+ f"[bold]Sağlık Skoru:[/bold] [{durum_renk}]{skor:.1f}/100 ({durum})[/{durum_renk}]\n"
1389
+ f"[bold]Ort. Anlama:[/bold] {anlama_str}\n"
1390
+ f"[bold]Borç Delta:[/bold] {delta_val:.1f} saat/commit",
1391
+ title="[bold cyan]🏃 Son Sprint Sağlığı[/bold cyan]",
1392
+ border_style=durum_renk,
1393
+ padding=(1, 2),
1394
+ )
1395
+ )
1396
+
1397
+
1398
+ @sprint_app.command("history")
1399
+ def sprint_gecmisi(
1400
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1401
+ limit: int = typer.Option(10, "--limit", "-n", help="Gösterilecek sprint sayısı"),
1402
+ ) -> None:
1403
+ """Geçmiş sprint'leri tablo olarak göster."""
1404
+ from codedna.db import get_sprint_history as db_sprint_gecmisi
1405
+ from codedna.plan import is_feature_available
1406
+ from datetime import datetime as dt
1407
+
1408
+ if not is_feature_available("sprint_health"):
1409
+ console.print("[bold yellow]🔒 Bu özellik Team planında mevcut.[/bold yellow]")
1410
+ raise typer.Exit(1)
1411
+
1412
+ kok = repo or find_git_root() or Path.cwd()
1413
+ db_yolu = _get_db(kok)
1414
+ init_db(db_yolu)
1415
+
1416
+ sprintler = db_sprint_gecmisi(limit=limit, db_path=db_yolu)
1417
+ if not sprintler:
1418
+ console.print("[yellow]Henüz kayıtlı sprint yok.[/yellow]")
1419
+ return
1420
+
1421
+ tablo = Table(
1422
+ title=f"[bold cyan]🏃 Sprint Geçmişi — Son {len(sprintler)}[/bold cyan]",
1423
+ border_style="dim",
1424
+ show_lines=True,
1425
+ header_style="bold",
1426
+ )
1427
+ tablo.add_column("Sprint", min_width=16)
1428
+ tablo.add_column("Tarih Aralığı", min_width=22)
1429
+ tablo.add_column("Sağlık", justify="center", min_width=14)
1430
+ tablo.add_column("Anlama", justify="center", min_width=10)
1431
+ tablo.add_column("Borç Delta", justify="right", min_width=11)
1432
+
1433
+ for s in sprintler:
1434
+ skor = s["health_score"] or 0.0
1435
+ durum = "SAĞLIKLI" if skor >= 80 else "DİKKAT" if skor >= 50 else "RİSKLİ"
1436
+ renk = {"SAĞLIKLI": "green", "DİKKAT": "yellow", "RİSKLİ": "red"}[durum]
1437
+
1438
+ bas = dt.fromtimestamp(s["start_date"]).strftime("%Y-%m-%d") if s["start_date"] else "?"
1439
+ bit = dt.fromtimestamp(s["end_date"]).strftime("%Y-%m-%d") if s["end_date"] else "?"
1440
+
1441
+ anlama_str = f"{s['avg_understanding']:.1f}/5" if s["avg_understanding"] else "—"
1442
+ delta_str = f"{s['debt_delta_hours']:.1f}s" if s["debt_delta_hours"] else "—"
1443
+
1444
+ tablo.add_row(
1445
+ s["sprint_name"] or "?",
1446
+ f"{bas} → {bit}",
1447
+ f"[{renk}]{skor:.0f}/100 {durum}[/{renk}]",
1448
+ anlama_str,
1449
+ delta_str,
1450
+ )
1451
+
1452
+ console.print()
1453
+ console.print(tablo)
1454
+ console.print()
1455
+
1456
+
1457
+ # ---------------------------------------------------------------------------
1458
+ # codedna plan
1459
+ # ---------------------------------------------------------------------------
1460
+ @app.command()
1461
+ def plan(
1462
+ komut: Optional[str] = typer.Argument(
1463
+ None,
1464
+ help="'activate', 'demo' veya doğrudan lisans anahtarı",
1465
+ ),
1466
+ anahtar: Optional[str] = typer.Argument(
1467
+ None,
1468
+ help="Lisans anahtarı (activate/demo ile birlikte kullanılır)",
1469
+ ),
1470
+ ) -> None:
1471
+ """Mevcut planı göster, demo lisans aktif et veya lisans anahtarı ile plan aktif et.
1472
+
1473
+ Kullanım:
1474
+ codedna plan # mevcut planı göster
1475
+ codedna plan activate <KEY> # lisans aktif et (CDNA-PRO/TEAM/ENT-...)
1476
+ codedna plan demo # Enterprise demo lisans aktif et (hızlı test)
1477
+ codedna plan demo pro # Pro demo lisans aktif et
1478
+ codedna plan <KEY> # kısa yol
1479
+ """
1480
+ from codedna.plan import (
1481
+ Plan as PlanEnum,
1482
+ get_current_plan,
1483
+ activate_license,
1484
+ activate_demo_license,
1485
+ get_plan_limits,
1486
+ )
1487
+
1488
+ # "demo [plan]" syntax'ı
1489
+ if komut == "demo":
1490
+ demo_plan_str = (anahtar or "enterprise").lower()
1491
+ try:
1492
+ demo_plan = PlanEnum(demo_plan_str)
1493
+ except ValueError:
1494
+ console.print(f"[bold red]Hata:[/bold red] Geçersiz plan: {demo_plan_str!r}")
1495
+ console.print("Geçerli planlar: free, pro, team, enterprise")
1496
+ raise typer.Exit(1)
1497
+ aktif_plan = activate_demo_license(demo_plan)
1498
+ console.print(
1499
+ Panel(
1500
+ f"[bold green]✓ Demo lisans aktif edildi![/bold green]\n\n"
1501
+ f"[bold]Plan:[/bold] [cyan]{aktif_plan.value.upper()}[/cyan]\n"
1502
+ f"[bold yellow]⚠️ Bu bir DEMO lisanstır — production için gerçek key gerekir.[/bold yellow]\n\n"
1503
+ f"Gerçek lisans için:\n"
1504
+ f"[dim]codedna plan activate CDNA-{demo_plan_str.upper()[:3]}-XXXX-XXXX-XXXX[/dim]",
1505
+ title="[bold cyan]🧬 CodeDNA — Demo Plan[/bold cyan]",
1506
+ border_style="cyan",
1507
+ padding=(1, 2),
1508
+ )
1509
+ )
1510
+ return
1511
+
1512
+ # "activate <KEY>" veya doğrudan "<KEY>" syntax'ı destekle
1513
+ lisans_anahtari: Optional[str] = None
1514
+ if komut == "activate" and anahtar:
1515
+ lisans_anahtari = anahtar
1516
+ elif komut and komut != "activate":
1517
+ lisans_anahtari = komut
1518
+
1519
+ if lisans_anahtari:
1520
+ # Lisans aktifleştirme
1521
+ try:
1522
+ aktif_plan = activate_license(lisans_anahtari)
1523
+ console.print(
1524
+ Panel(
1525
+ f"[bold green]✓ Lisans aktif edildi![/bold green]\n\n"
1526
+ f"[bold]Plan:[/bold] [cyan]{aktif_plan.value.upper()}[/cyan]\n"
1527
+ f"[bold]Anahtar:[/bold] [dim]{lisans_anahtari[:12]}...[/dim]",
1528
+ border_style="green",
1529
+ padding=(1, 2),
1530
+ )
1531
+ )
1532
+ except ValueError as e:
1533
+ console.print(f"[bold red]Hata:[/bold red] {e}")
1534
+ raise typer.Exit(1)
1535
+ return
1536
+
1537
+ # Mevcut planı göster
1538
+ mevcut = get_current_plan()
1539
+ limitler = get_plan_limits()
1540
+
1541
+ plan_renk = {
1542
+ PlanEnum.FREE: "dim",
1543
+ PlanEnum.PRO: "cyan",
1544
+ PlanEnum.TEAM: "green",
1545
+ PlanEnum.ENTERPRISE: "yellow",
1546
+ }.get(mevcut, "white")
1547
+
1548
+ tablo = Table(border_style="dim", show_header=False, padding=(0, 1))
1549
+ tablo.add_column("Özellik", style="dim")
1550
+ tablo.add_column("Değer", style="white")
1551
+
1552
+ for k, v in limitler.items():
1553
+ if isinstance(v, bool):
1554
+ goster = "[green]✓[/green]" if v else "[red]✗[/red]"
1555
+ elif isinstance(v, int) and v == -1:
1556
+ goster = "[dim]Sınırsız[/dim]"
1557
+ else:
1558
+ goster = str(v)
1559
+ tablo.add_row(k.replace("_", " ").title(), goster)
1560
+
1561
+ console.print()
1562
+ console.print(
1563
+ Panel(
1564
+ f"[bold]Mevcut Plan / Current Plan:[/bold] [{plan_renk}]{mevcut.value.upper()}[/{plan_renk}]\n\n"
1565
+ + (
1566
+ "[dim]Lisans aktif etmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]"
1567
+ if mevcut == PlanEnum.FREE
1568
+ else "[dim]Yükseltmek için:[/dim] [cyan]codedna plan activate <LICENSE_KEY>[/cyan]"
1569
+ ),
1570
+ title="[bold cyan]🧬 CodeDNA Plan[/bold cyan]",
1571
+ border_style="cyan",
1572
+ padding=(1, 2),
1573
+ )
1574
+ )
1575
+ console.print(tablo)
1576
+ console.print()
1577
+
1578
+
1579
+ # ---------------------------------------------------------------------------
1580
+ # codedna dashboard
1581
+ # ---------------------------------------------------------------------------
1582
+ @app.command()
1583
+ def dashboard(
1584
+ api_port: int = typer.Option(8000, "--api-port", help="FastAPI port numarası"),
1585
+ ui_port: int = typer.Option(3000, "--ui-port", help="Next.js dashboard port numarası"),
1586
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1587
+ ) -> None:
1588
+ """FastAPI + Next.js dashboard'u başlat ve tarayıcıda aç."""
1589
+ import os
1590
+ import subprocess
1591
+ import time
1592
+ import webbrowser
1593
+
1594
+ kok = repo or find_git_root() or Path.cwd()
1595
+ db_yolu = _get_db(kok)
1596
+ init_db(db_yolu)
1597
+
1598
+ # dashboard/ klasörünü bul — CLI'nin bulunduğu yere göre veya CWD'ye göre
1599
+ dashboard_yollari = [
1600
+ Path(__file__).parent.parent / "dashboard",
1601
+ Path.cwd() / "dashboard",
1602
+ kok / "dashboard",
1603
+ ]
1604
+ dashboard_kok = next((p for p in dashboard_yollari if (p / "package.json").exists()), None)
1605
+
1606
+ if not dashboard_kok:
1607
+ console.print(
1608
+ "[bold red]Hata:[/bold red] dashboard/ klasörü bulunamadı.\n"
1609
+ "[dim]codedna proje kökünde 'dashboard/' klasörü olmalı.[/dim]"
1610
+ )
1611
+ raise typer.Exit(1)
1612
+
1613
+ console.print()
1614
+ console.print(
1615
+ Panel(
1616
+ f"[bold cyan]🧬 CodeDNA Panosu[/bold cyan] başlatılıyor...\n\n"
1617
+ f"[bold]API:[/bold] [link]http://localhost:{api_port}[/link]\n"
1618
+ f"[bold]Dashboard:[/bold] [link]http://localhost:{ui_port}[/link]\n\n"
1619
+ "[dim]Durdurmak için Ctrl+C[/dim]",
1620
+ border_style="cyan",
1621
+ padding=(1, 2),
1622
+ )
1623
+ )
1624
+
1625
+ # Ortam değişkenlerini ayarla
1626
+ env = os.environ.copy()
1627
+ env["CODEDNA_REPO_PATH"] = str(kok)
1628
+ env["CODEDNA_DB_PATH"] = str(db_yolu)
1629
+ env["NEXT_PUBLIC_API_URL"] = f"http://localhost:{api_port}"
1630
+
1631
+ # FastAPI sürecini başlat — venv Python'unu kullan
1632
+ import sys
1633
+ python_bin = sys.executable
1634
+
1635
+ api_proc = subprocess.Popen(
1636
+ [
1637
+ python_bin, "-m", "uvicorn",
1638
+ "codedna.api:app",
1639
+ "--host", "127.0.0.1",
1640
+ "--port", str(api_port),
1641
+ ],
1642
+ env=env,
1643
+ stdout=subprocess.DEVNULL,
1644
+ stderr=subprocess.DEVNULL,
1645
+ )
1646
+
1647
+ # Next.js npm run dev başlat — node_modules/.bin/next'i doğrudan çağır
1648
+ next_bin = dashboard_kok / "node_modules" / ".bin" / "next"
1649
+ npm_cmd = str(next_bin) if next_bin.exists() else "npm"
1650
+ ui_cmd = (
1651
+ [npm_cmd, "dev", "--port", str(ui_port)]
1652
+ if next_bin.exists()
1653
+ else ["npm", "run", "dev", "--", "--port", str(ui_port)]
1654
+ )
1655
+ ui_proc = subprocess.Popen(
1656
+ ui_cmd,
1657
+ cwd=str(dashboard_kok),
1658
+ env=env,
1659
+ stdout=subprocess.DEVNULL,
1660
+ stderr=subprocess.DEVNULL,
1661
+ )
1662
+
1663
+ # Başlaması için bekle, sonra tarayıcıyı aç
1664
+ console.print("[dim]Sunucular başlatılıyor...[/dim]")
1665
+ time.sleep(4)
1666
+
1667
+ try:
1668
+ webbrowser.open(f"http://localhost:{ui_port}")
1669
+ console.print(f"[green]✓[/green] Tarayıcı açıldı: [link]http://localhost:{ui_port}[/link]")
1670
+ console.print("[dim]Ctrl+C ile durdur[/dim]\n")
1671
+
1672
+ # Her iki süreci bekle
1673
+ api_proc.wait()
1674
+ except KeyboardInterrupt:
1675
+ console.print("\n[yellow]Durduruluyor...[/yellow]")
1676
+ finally:
1677
+ # Her iki süreci de temizle
1678
+ for proc in [api_proc, ui_proc]:
1679
+ try:
1680
+ proc.terminate()
1681
+ proc.wait(timeout=3)
1682
+ except Exception:
1683
+ try:
1684
+ proc.kill()
1685
+ except Exception:
1686
+ pass
1687
+ console.print("[dim]CodeDNA durduruldu.[/dim]")
1688
+
1689
+
1690
+ # ---------------------------------------------------------------------------
1691
+ # codedna uninstall (bonus)
1692
+ # ---------------------------------------------------------------------------
1693
+ @app.command()
1694
+ def uninstall(
1695
+ repo: Optional[Path] = typer.Option(None, "--repo", "-r", help="Git repo dizini"),
1696
+ ) -> None:
1697
+ """CodeDNA hook'unu kaldır."""
1698
+ kok = repo or find_git_root()
1699
+ if uninstall_hook(kok):
1700
+ console.print("[green]✓[/green] CodeDNA kaldırıldı.")
1701
+ else:
1702
+ raise typer.Exit(1)
1703
+
1704
+
1705
+ # ---------------------------------------------------------------------------
1706
+ # codedna natureco (Pro+ özellik — NatureCo CLI entegrasyonu)
1707
+ # ---------------------------------------------------------------------------
1708
+ @app.command()
1709
+ def natureco(
1710
+ komut: Optional[str] = typer.Argument(
1711
+ None,
1712
+ help="'connect', 'sync', 'auto-scan', 'status' veya doğrudan dosya yolu",
1713
+ ),
1714
+ dosya: Optional[str] = typer.Argument(
1715
+ None,
1716
+ help="Dosya yolu (sync/auto-scan için)",
1717
+ ),
1718
+ ) -> None:
1719
+ """NatureCo CLI ile entegre ol (Pro+ plan gerekir).
1720
+
1721
+ NatureCo CLI kod ürettiğinde otomatik CodeDNA analizi yap.
1722
+
1723
+ Kullanım:
1724
+ codedna natureco connect # NatureCo CLI'ya bağlan
1725
+ codedna natureco status # Bağlantı durumu
1726
+ codedna natureco sync <file> # Dosyayı tarat
1727
+ codedna natureco auto-scan # Post-hook: commit'te otomatik scan
1728
+ codedna natureco disconnect # Bağlantıyı kes
1729
+ """
1730
+ from codedna.plan import get_current_plan, Plan as PlanEnum, is_feature_available
1731
+
1732
+ # Pro+ plan gerekli
1733
+ if not is_feature_available("natureco_integration"):
1734
+ plan = get_current_plan()
1735
+ console.print(
1736
+ Panel(
1737
+ f"[bold yellow]🔒 NatureCo entegrasyonu Pro+ plan gerektirir.[/bold yellow]\n\n"
1738
+ f"Mevcut planınız: [cyan]{plan.value.upper()}[/cyan]\n\n"
1739
+ f"Demo için:\n"
1740
+ f"[dim]codedna plan demo pro[/dim]\n\n"
1741
+ f"Upgrade:\n"
1742
+ f"[dim]https://codedna.dev/pricing[/dim]",
1743
+ title="[bold cyan]🧬 CodeDNA — NatureCo Integration[/bold cyan]",
1744
+ border_style="yellow",
1745
+ padding=(1, 2),
1746
+ )
1747
+ )
1748
+ raise typer.Exit(1)
1749
+
1750
+ if not komut or komut == "status":
1751
+ return _natureco_status()
1752
+ elif komut == "connect":
1753
+ return _natureco_connect()
1754
+ elif komut == "disconnect":
1755
+ return _natureco_disconnect()
1756
+ elif komut == "sync":
1757
+ return _natureco_sync(dosya)
1758
+ elif komut == "auto-scan":
1759
+ return _natureco_auto_scan()
1760
+ else:
1761
+ console.print(f"[bold red]Bilinmeyen komut:[/bold red] {komut!r}")
1762
+ console.print("Geçerli: connect, status, sync, auto-scan, disconnect")
1763
+ raise typer.Exit(1)
1764
+
1765
+
1766
+ def _natureco_config_path() -> Path:
1767
+ """NatureCo entegrasyon ayar dosyası yolu."""
1768
+ return Path.home() / ".codedna" / "natureco.json"
1769
+
1770
+
1771
+ def _natureco_status() -> None:
1772
+ """Bağlantı durumunu göster."""
1773
+ config_path = _natureco_config_path()
1774
+ if config_path.exists():
1775
+ import json as _json
1776
+ data = _json.loads(config_path.read_text())
1777
+ console.print(
1778
+ Panel(
1779
+ f"[bold green]✓ NatureCo CLI bağlı[/bold green]\n\n"
1780
+ f"[bold]Hook:[/bold] {'[green]Aktif[/green]' if data.get('auto_scan') else '[yellow]Pasif[/yellow]'}\n"
1781
+ f"[bold]Son sync:[/bold] {data.get('last_sync', 'Hiç')}\n"
1782
+ f"[bold]Toplam dosya:[/bold] {data.get('total_files', 0)}\n"
1783
+ f"[bold]Bağlandı:[/bold] {data.get('connected_at', 'Bilinmiyor')}",
1784
+ title="[bold cyan]🧬 CodeDNA ↔ NatureCo[/bold cyan]",
1785
+ border_style="green",
1786
+ padding=(1, 2),
1787
+ )
1788
+ )
1789
+ else:
1790
+ console.print(
1791
+ Panel(
1792
+ "[bold yellow]NatureCo CLI henüz bağlı değil.[/bold yellow]\n\n"
1793
+ "Bağlamak için:\n"
1794
+ "[dim]codedna natureco connect[/dim]",
1795
+ title="[bold cyan]🧬 CodeDNA ↔ NatureCo[/bold cyan]",
1796
+ border_style="yellow",
1797
+ padding=(1, 2),
1798
+ )
1799
+ )
1800
+
1801
+
1802
+ def _natureco_connect() -> None:
1803
+ """NatureCo CLI ile bağlantı kur."""
1804
+ import json as _json
1805
+ import time as _time
1806
+
1807
+ config_path = _natureco_config_path()
1808
+ config_path.parent.mkdir(parents=True, exist_ok=True)
1809
+
1810
+ # NatureCo CLI yüklü mü kontrol et
1811
+ nc_path = None
1812
+ for p in ["/usr/local/bin/natureco", "/opt/homebrew/bin/natureco",
1813
+ str(Path.home() / ".npm-global/bin/natureco")]:
1814
+ if Path(p).exists():
1815
+ nc_path = p
1816
+ break
1817
+
1818
+ # PATH'te ara
1819
+ if not nc_path:
1820
+ import shutil
1821
+ nc_path = shutil.which("natureco")
1822
+
1823
+ if nc_path:
1824
+ console.print(f"[green]✓[/green] NatureCo CLI bulundu: [dim]{nc_path}[/dim]")
1825
+ else:
1826
+ console.print("[yellow]⚠[/yellow] NatureCo CLI PATH'te bulunamadı")
1827
+ console.print("[dim] Yine de entegrasyon kuruldu — NatureCo CLI kurulunca çalışacak[/dim]")
1828
+
1829
+ config = {
1830
+ "connected": True,
1831
+ "natureco_cli_path": nc_path,
1832
+ "auto_scan": True, # Varsayılan açık
1833
+ "last_sync": None,
1834
+ "total_files": 0,
1835
+ "connected_at": _time.strftime("%Y-%m-%d %H:%M:%S"),
1836
+ }
1837
+ config_path.write_text(_json.dumps(config, indent=2))
1838
+
1839
+ console.print(
1840
+ Panel(
1841
+ "[bold green]✓ NatureCo CLI bağlantısı kuruldu![/bold green]\n\n"
1842
+ "Artık NatureCo CLI ile üretilen her kod otomatik taranacak.\n\n"
1843
+ "[bold]Sonraki adımlar:[/bold]\n"
1844
+ "1. NatureCo CLI'da kod üretin (`natureco code`)\n"
1845
+ "2. CodeDNA otomatik scan yapar\n"
1846
+ "3. `codedna natureco status` ile durumu kontrol edin",
1847
+ title="[bold green]🧬 CodeDNA ↔ NatureCo Bağlandı[/bold green]",
1848
+ border_style="green",
1849
+ padding=(1, 2),
1850
+ )
1851
+ )
1852
+
1853
+
1854
+ def _natureco_disconnect() -> None:
1855
+ """Bağlantıyı kes."""
1856
+ config_path = _natureco_config_path()
1857
+ if config_path.exists():
1858
+ config_path.unlink()
1859
+ console.print("[green]✓[/green] NatureCo bağlantısı kesildi.")
1860
+ else:
1861
+ console.print("[yellow]Zaten bağlı değil.[/yellow]")
1862
+
1863
+
1864
+ def _natureco_sync(dosya: Optional[str]) -> None:
1865
+ """Bir dosyayı CodeDNA ile tara."""
1866
+ import json as _json
1867
+ import time as _time
1868
+
1869
+ if not dosya:
1870
+ console.print("[bold red]Hata:[/bold red] Dosya yolu gerekli")
1871
+ console.print("Kullanım: [dim]codedna natureco sync <dosya>[/dim]")
1872
+ raise typer.Exit(1)
1873
+
1874
+ file_path = Path(dosya)
1875
+ if not file_path.exists():
1876
+ console.print(f"[bold red]Hata:[/bold red] Dosya bulunamadı: {dosya}")
1877
+ raise typer.Exit(1)
1878
+
1879
+ # Repo kökünü bul
1880
+ kok = find_git_root() or Path.cwd()
1881
+ db_yolu = _get_db(kok)
1882
+
1883
+ # Scoring yap (analyzer kullanarak)
1884
+ from codedna.analyzer import analyze_file
1885
+
1886
+ console.print(f"[dim]Taranıyor:[/dim] {file_path.name}")
1887
+ result = analyze_file(file_path, single_commit_ratio=0.0)
1888
+
1889
+ if result:
1890
+ console.print(
1891
+ Panel(
1892
+ f"[bold green]✓ Tarama tamamlandı[/bold green]\n\n"
1893
+ f"[bold]Dosya:[/bold] {file_path.name}\n"
1894
+ f"[bold]AI olasılığı:[/bold] [cyan]%{result.ai_probability * 100:.0f}[/cyan]\n"
1895
+ f"[bold]Karmaşıklık:[/bold] {result.complexity_score:.1f}\n"
1896
+ f"[bold]Yorum oranı:[/bold] {result.comment_ratio * 100:.0f}%\n",
1897
+ title="[bold cyan]🧬 Tarama Sonucu[/bold cyan]",
1898
+ border_style="cyan",
1899
+ padding=(1, 2),
1900
+ )
1901
+ )
1902
+
1903
+ # Config'i güncelle
1904
+ config_path = _natureco_config_path()
1905
+ if config_path.exists():
1906
+ cfg = _json.loads(config_path.read_text())
1907
+ cfg["last_sync"] = _time.strftime("%Y-%m-%d %H:%M:%S")
1908
+ cfg["total_files"] = cfg.get("total_files", 0) + 1
1909
+ config_path.write_text(_json.dumps(cfg, indent=2))
1910
+ else:
1911
+ console.print("[yellow]Dosya analiz edilemedi.[/yellow]")
1912
+
1913
+
1914
+ def _natureco_auto_scan() -> None:
1915
+ """Auto-scan hook'unu aktif/pasif yap."""
1916
+ import json as _json
1917
+
1918
+ config_path = _natureco_config_path()
1919
+ if not config_path.exists():
1920
+ console.print("[bold red]Hata:[/bold red] Önce connect gerekli")
1921
+ console.print("[dim]codedna natureco connect[/dim]")
1922
+ raise typer.Exit(1)
1923
+
1924
+ cfg = _json.loads(config_path.read_text())
1925
+ cfg["auto_scan"] = not cfg.get("auto_scan", False)
1926
+ config_path.write_text(_json.dumps(cfg, indent=2))
1927
+
1928
+ state = "[green]aktif[/green]" if cfg["auto_scan"] else "[yellow]pasif[/yellow]"
1929
+ console.print(f"[green]✓[/green] Auto-scan {state}")
1930
+
1931
+
1932
+ # ---------------------------------------------------------------------------
1933
+ # Yardımcı fonksiyonlar
1934
+ # ---------------------------------------------------------------------------
1935
+ def _kisalt_yol(tam_yol: str, kok: str) -> str:
1936
+ """Uzun yolları repo köküne göre kısalt."""
1937
+ try:
1938
+ goreceli = Path(tam_yol).relative_to(Path(kok))
1939
+ yol_str = str(goreceli)
1940
+ if len(yol_str) > 40:
1941
+ parcalar = Path(yol_str).parts
1942
+ if len(parcalar) > 3:
1943
+ return f".../{'/'.join(parcalar[-2:])}"
1944
+ return yol_str
1945
+ except ValueError:
1946
+ return tam_yol[-40:] if len(tam_yol) > 40 else tam_yol
1947
+
1948
+
1949
+ def _risk_etiketi(yuzde: float) -> tuple[str, str]:
1950
+ """AI yüzdesine göre risk etiketi ve renk döndür."""
1951
+ if yuzde >= 70:
1952
+ return "YÜKSEK", "red"
1953
+ elif yuzde >= 40:
1954
+ return "ORTA", "yellow"
1955
+ else:
1956
+ return "DÜŞÜK", "green"
1957
+
1958
+
1959
+ def main() -> None:
1960
+ """CLI giriş noktası."""
1961
+ app()
1962
+
1963
+
1964
+ if __name__ == "__main__":
1965
+ main()