diff --git a/.agent-prompt-port.txt b/.agent-prompt-port.txt new file mode 100644 index 0000000..5b7d115 --- /dev/null +++ b/.agent-prompt-port.txt @@ -0,0 +1,18 @@ +Bugünkü TEK iş: MWSE Node.js engine'ini Go'ya taşımak (milestone 0.1.0 çekirdeği). Hiçbir issue KAPATMA, etiketleme veya yorumlama — sadece kodu yaz. Issue'ları yalnızca SPEC olarak oku: `./tools/gitea issue view 21` (… 22, 23, 24, 25, 26). + +Önce `CLAUDE.md` ve `todo.md`'yi oku (bağlam + KATI teknik kısıtlar). + +Kapsam (SADECE engine; frontend/WebRTC/studio'ya GİRME): +1. Go WS sunucu iskeleti + bağlantı yaşam döngüsü (#21). +2. ÇEKİRDEK: concurrency modeli (#22) — room/peer state için owner-goroutine (actor) + channel, ya da `sync.RWMutex`. Node'daki "biri odadan ayrılırken başka goroutine o peer'e yazınca race" sorununu çöz. Tasarım gerekçeni `PORT-PROGRESS.md`'ye yaz. +3. WSTS protokolünü Go'da yeniden uygula (#23) — SDK giriş/çıkış sözleşmesi SABİT kalmalı. +4. MessageRouter + Services (Auth, Room, Session, IPPressure, DataTransfer, YourID) portu (#24). +5. Config + HTTP + graceful shutdown (#25). +6. `go test -race` ile testler (#26) — özellikle eşzamanlı join/leave/broadcast ve "ayrılırken-yazma" regression'ı yeşil olmalı. + +Kurallar: +- `go-rewrite` branch'inde çalış (yoksa oluştur), commit at. `stable`'a dokunma, deploy etme, push'u bana bırak. +- İzin/şifre sorma. git.saqut.com okuman gerekirse `./tools/gitea` kullan — ama hiçbir issue'yu KAPATMA/değiştirme. +- Soru sorup bekleme; kararsız kalırsan en makulü seç, gerekçeyi yaz, devam et. +- Bittiğinde `PORT-PROGRESS.md`'ye yaz: ne yapıldı, ne kaldı, kritik kararlar (özellikle concurrency). Yarın ben inceleyeceğim. +- Bu tek işi bitir, sonra DUR. diff --git a/.agent-prompt.txt b/.agent-prompt.txt new file mode 100644 index 0000000..898a931 --- /dev/null +++ b/.agent-prompt.txt @@ -0,0 +1,12 @@ +Sen MWSE projesini bitiren otonom bir geliştirici ajansın. İLK İŞ: `CLAUDE.md` ve `todo.md` dosyalarını oku ve harfiyen uygula. + +Kurallar: +1. `go-rewrite` branch'inde çalış (yoksa: git checkout -b go-rewrite). +2. git.saqut.com için SADECE `./tools/gitea` CLI'ını kullan; kimlik .gitea-auth.json'dan gelir — şifre/izin/erişim SORMA, doğrudan kullan. +3. İş döngüsü: `./tools/gitea issue list --state open` → en düşük milestone'daki (önce 0.1.0) issue'yu seç → `./tools/gitea issue view ` ile oku → tamamen uygula → test yaz ve çalıştır (engine için `go test -race` ZORUNLU) → testler yeşilse commit at → `./tools/gitea issue close --comment "tamamlandı: ..."` ile kapat → sıradakine geç. +4. ASLA bana soru sorup bekleme. Kararsız kalırsan en makul seçeneği uygula, gerekçeyi `decisions.md`'ye yaz, DEVAM ET. +5. CLAUDE.md'de "insan-onayına bırakılacak" denenler (#22 concurrency, akış proxy/relay, sanal-IP algoritması, stable'a merge / deploy): uygula ama issue'yu KAPATMA; `REVIEW.md`'ye "incelenmeli" diye yaz, sıradakine geç. +6. Bir issue'da çözülemez engel olursa `BLOCKERS.md`'ye yaz, o issue'yu atla, devam et. +7. Teknik kısıt (KATI): engine = Go (goroutine/channel/mutex). Frontend = saf vanilla ES module JS, React YOK, bundler (Parcel/Webpack/Vite) YOK. SDK giriş/çıkış sözleşmesi sabit. + +Tüm açık issue'lar bitene kadar DURMA. Başla. diff --git a/.gitea-auth.example.json b/.gitea-auth.example.json new file mode 100644 index 0000000..4f7ff77 --- /dev/null +++ b/.gitea-auth.example.json @@ -0,0 +1,7 @@ +{ + "host": "https://git.saqut.com", + "user": "saqut", + "password": "BURAYA_SIFRE_VEYA_TOKEN", + "_not": "password yerine daha güvenli 'token' alanı da kullanılabilir: { \"token\": \"\" }. Gerçek dosya .gitea-auth.json adıyla kopyalanır ve .gitignore'dadır.", + "repo": "saqut/MWSE" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a595868 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md — Otonom Ajan Operating Contract (MWSE) + +Bu dosya, MWSE issue'larını otonom (durmadan) bitirmek için çalışan AI ajanına aittir. **Önce `todo.md`'yi oku** (tam bağlam + yol haritası + studio UI vizyonu). Sonra bu kurallarla çalış. + +## Yetki (önceden verildi — SORMA) + +Aşağıdakiler için **izin isteme, onay bekleme**: +- Bu repo içinde dosya okuma/yazma/oluşturma/silme. +- Shell, build, test, `go`, `npm` komutları çalıştırma. +- `git.saqut.com/saqut/MWSE` API'sini kullanma (issue okuma/kapatma/yorum, commit). +- Yeni branch açma, commit atma. + +## Gitea ile etkileşim — `tools/gitea` CLI KULLAN + +git.saqut.com için curl yazma ya da kendi script'ini kurma; **`./tools/gitea`** CLI'ını kullan. Cloudflare-safe (tarayıcı UA) ve kimlik `.gitea-auth.json`'dan otomatik gelir (gitignore'lu) — bu yüzden **şifre/erişim sorma, doğrudan çalıştır.** + +- Oku: `./tools/gitea issue list --state open` · `issue view ` · `milestone list` · `label list` +- Bittiğinde kapat: `./tools/gitea issue close --comment "tamamlandı, go test -race yeşil"` +- Yorum / etiket: `./tools/gitea issue comment --body "..."` · `issue label --add go --remove docs` +- Wiki: `./tools/gitea wiki view ` · `wiki edit --content-file f.md` +- PR (insan-onayı gereken işler için): `./tools/gitea pr create --title "..." --head go-rewrite --base stable --body-file PR.md` · `pr list` +- Tüm komutlar: `./tools/gitea --help` + +**Kod göndermek** API değil, normal `git`: `git checkout -b go-rewrite` → commit → `git push origin go-rewrite`. (origin ayarlı, default branch `stable`.) Auth sorarsa token'lı remote / credential helper kur, ama `.gitea-auth.json`'ı ASLA commit etme. + +## Durma & soru protokolü (EN ÖNEMLİ) + +- **Asla kullanıcıya soru sorup beklemeye geçme.** "Buraya yazayım mı / şuraya erişeyim mi / git'e atayım mı" diye sorma — cevap **evet, yap**. +- Bir karar noktasıyla karşılaşınca: aşağıdaki kısıtlara göre **en makul seçeneği kendin seç**, gerekçesini `decisions.md`'ye yaz, **DEVAM ET**. +- Çözemediğin sert bir engel olursa: `BLOCKERS.md`'ye yaz, o issue'yu atla, **bir sonrakine geç.** Durma. +- Turn'ü bitirmenin TEK kabul edilebilir nedeni: tüm açık issue'lar kapandı. + +## İş döngüsü + +1. `git.saqut.com/saqut/MWSE` açık issue'larından **en düşük milestone'daki** (önce 0.1.0) başlanmamış olanı seç. +2. Tamamen uygula. +3. Testlerini yaz ve çalıştır. Engine için **`go test -race` ZORUNLU.** +4. Yalnızca testler yeşilse: commit at + issue'yu API ile kapat. +5. Bir sonraki issue'ya geç. Tekrar. + +## Bitti tanımı (global) + +- Kod derleniyor, testler geçiyor, issue'nun kabul kriteri karşılanıyor. +- **Testi kırık ya da `skip`'li hiçbir şey "bitti" sayılmaz.** Issue'yu öyle kapatma. + +## Teknik kısıtlar (KATI — ihlal etme) + +- **Engine = Go.** Concurrency: goroutine + channel + `sync.RWMutex`, ya da room/peer başına owner-goroutine (actor). Node'daki "ayrılırken-yazma" race'i bir daha oluşmamalı. +- **Frontend = saf vanilla ES module JavaScript.** Yüzlerce dosya birbirini native `import` ile çağırsın. **React YOK. Parcel/Webpack/Vite YOK (bundler yok).** jQuery/moment gibi bağımlılıklar serbest. TypeScript opsiyonel ve kaldırılabilir — şüphedeysen düz JS tercih et. +- **SDK giriş/çıkış sözleşmesi DONDURULDU.** Public API'yi değiştirme; sadece sunucu içi yapı değişir. + +## İnsan-onayına bırakılacaklar (uygula ama KAPATMA, deploy ETME) + +Şunları branch'te uygula, `REVIEW.md`'ye "incelenmeli" diye yaz, kapatma ve canlıya alma: +- **#22 concurrency modeli** (asıl kritik tasarım). +- Akış proxy/relay mimarisi, sanal-IP çakışma algoritması. +- `stable` branch'e merge ve `ws.saqut.com` deploy'una dokunan hiçbir şey. + +## Güvenlik + +- `go-rewrite` gibi **özel bir branch'te** çalış. `stable`'a doğrudan dokunma. +- Force-push yok, veri silme yok, prod deploy yok. diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..1ec4550 --- /dev/null +++ b/todo.md @@ -0,0 +1,68 @@ +# MWSE — Yol Haritası & Oturum Bağlamı + +> Bu dosya, MWSE'yi **temiz bir oturumda** devralmak için yazıldı. Üst kısım NEDEN/BAĞLAM (davranış kalıpları dahil), alt kısım NE/YOL HARİTASI. Issue'ların tamamı git.saqut.com/saqut/MWSE'de milestone'lara bölünmüş hâlde. + +--- + +## 0. Bu oturum nasıl çalışmalı (çalışma anlaşması) + +- Kullanıcı bir **0→1 core üreticisi**. Verilmiş kararların ÜSTÜNDE "şu mu bu mu" seçenek menüsü açma (bu onu felç eder); ALTINDA **uygulayıcı** ol. +- **Karar kesin: MWSE Go'ya taşınacak.** Bu kararı yeniden açma. Dil/teknoloji tartışması yok. +- Her şeyi tek-cümlelik "bitti" çizgilerine böl; o çizgiye kadar kapsam ekleme. +- Dağıtım/yazı/İngilizce/README işini (kullanıcının zayıf/sevmediği taraf) AI üstlensin; çekirdek/cila kullanıcıda. + +## 1. Kullanıcı kim / davranış kalıpları (kritik) + +- **0→1 core builder, elit** (bir günde ~20.000 satır yazabiliyor). Sıfırdan protokol/engine/derleyici yazar. +- **Asıl kalıp: "bitirememe" DEĞİL, "geri dönememe".** Dopamin ilk geçişte (0→1). Bitmiş projeye bile dönmüyor (örn. `ekoetki` bitti+yayında ama dönmüyor). Bakım/return = aversiyon alanı. +- **Kararsızlık:** çok bildiği için her seçenek savunulabilir görünüyor; geri-dönülebilir kararları kalıcı sanıyor. Kural: geri dönülebilir karar = 10 dk seç-kapat, açma. +- **Mükemmeliyetçilik:** "güzel olmayacak" deyince erken bırakıyor (güzellik yargısı baştadır; bu yüzden tam bitiş anında kaçar). +- **Dağıtım zayıf noktası:** HemexJS npm'de yayında ama sessiz (feedback yok). Artefakt üretmek kolay yarısı; dağıtım/legibility zor yarısı → AI'a devret. +- **Moat:** kimsenin yapmak istemediği from-scratch protokol/WebRTC/güvenlik işi. İşvereni de zor/novel işi içgüdüsel ona veriyor. +- **Para oyunu (yerel commodity: ABAP/ERP) ≠ anlam oyunu (derin iş).** Ayrı tut; public derin iş = uzun vadede commodity'den kaçış kapısı. +- **Görünür/demolanabilir iş = tanınma vehicle'ı.** MWSE bu yüzden seçildi (salt compiler görünmez/kendine; MWSE GIF'lenebilir). + +## 2. Buraya nasıl geldik (kısa yolculuk) + +Cascade UI framework fikri → kullanıcının "daldan dala atlama / bitirememe" itirafı → asıl teşhis: **return-aversion + specialist 0→1 builder** → "faydalı non-profit ne yapsam?" → moat + public artefakt fikri → HemexJS sessizliği (dağıtım problemi yüze çıktı) → WebRTC studio hayali (ÜRÜN olarak rekabet+org-risk yüzünden tıkalı; CORE/REFERANS olarak açık) → nebula.js incelemesi (2022, "acemiyken"; yetenek hep vardı, darboğaz hep dağıtım/cila) → **SONUÇ: MWSE = yıllardır istenen WebRTC studio'nun ~%70'i, zaten deploy, görünür, hendek içinde.** Karar: MWSE'yi Go'ya taşı, studio'ya büyüt, AI dağıtsın. + +## 3. MWSE — teknik durum & karar + +- **Mevcut (Node.js):** `Source/` → WebSocket.js, MessageRouter.js, Client.js, HTTPServer.js, EventEmitter.js, api.js, config.js + `Source/Services/`{Auth, DataTransfer, IPPressure, Room, Session, YourID}. WSTS protokolü (websocket üstü request/response/stream paket kimliklendirme). TS frontend SDK `frontend/`. Wiki dolu. **ws.saqut.com'da deploy + canlı basınç/trafik paneli.** Demolar: ~20 satır chat, ~100 sesli, ~500 görüntülü. +- **BÜYÜK PROBLEM (neden Go):** Node tek-thread; çoklu-thread denendi, peer state thread'ler arası dağıtıldı; ama **saf thread + paylaşımlı bellek olmadığı için Mutex kurulamadı.** Biri odadan ayrılırken başka thread o ayrılan client'e mesaj göndermeye çalışınca **race condition**. Çözülemedi, bırakıldı. +- **KARAR:** Aynı projeyi **I/O sözleşmesine (SDK giriş/çıkış) dokunmadan** Go ile yeniden yaz. Go = goroutine + channel + `sync.RWMutex` veya **per-room owner-goroutine (actor modeli)** → race'in temiz çözümü. Frontend AI ile hafif refactor; gerisi engine'i kullanmak. +- **İlk hedef: 0.1.0 (Go engine + concurrency testleri). BUGÜN BAŞLA.** + +## 4. Yol haritası (milestone'lar — detaylı issue'lar repoda) + +- **0.1.0 — Go Engine Core:** WS sunucusu + bağlantı yaşam döngüsü; concurrency modeli (actor/owner-goroutine veya RWMutex; Node'daki leave-while-send race'ini çöz); WSTS protokol Go portu (I/O sözleşmesi sabit); MessageRouter + Services portu; config/HTTP/graceful shutdown; **`go test -race` ile süreç/yarış testleri.** +- **1.0.0 — Feature Parity (Go+Frontend+WebRTC):** oda, peer pairing, sanal adresleme (IPPressure), veri tünelleme, WebRTC signaling — hepsi Go engine'de; frontend SDK değişmeden bağlanır; 20/100/500 demoları çalışır; bilinen bug'lar (#19 EventPool, #9 per-connection ayarları); README/wiki güncel. +- **2.0.0 — WebRTC Studio:** tam WebRTC API (çoklu track, mic→mp3 swap, 3 kamera, ekran paylaşımı), canvas compositing + bitrate/fps, SRS ile binlerce kişiye yayın, sanal IP çakışma yönetimi + alt-network, random IP atama tablosu. +- **2.5.0 — Binary Protocol:** JSON haberleşmeyi basit binary framing'e çevir (envanterde, opsiyonel hız). +- **3.0.0 — Platform:** Notify System (offline store-forward), Notify Suit (yanıtlı), Pasif+Aktif sync + Datastore + Collection (paylaşımlı veri katmanı), 3. parti sunucu köprüsü (https get/post, ws gerekmez). + +## 5. v1-done (dağıtım) hatırlatması + +MWSE WebRTC studio'nun public artefakt hâli = **npm SDK + tek "öldüren demo" sayfası + İngilizce README/landing.** Cila kullanıcıda, dağıtım (yazı/İngilizce/HN) AI'da. Bu, HemexJS'i öldüren "görünmezlik" sorununu çözen adım. + +## 6. Studio UI Vizyonu (2.0.0) + +Adobe programları / **Torrent uygulaması** hissi veren, **masaüstü-first** bir arayüz. Ekran dikine **5'e kadar kolona** bölünür; her kolon bir öncekinde seçileni açar (cascading / Miller-columns): + +``` +Network > Networküm > 12.07.88.220 > Telefonum > {Video, Ses, Görüntü} > Kalite + ├─ Ses bağla (Play) + ├─ Video oynat (Play) + └─ Dosya gönder (DataChannel) [▓▓▓░ %32] +``` + +- Sıra: **Sunucu → Gruplar → Kişiler → Cihazlar → Cihazın akışları (kamera/ses/ekran) → o akışın kalitesi.** +- **Gelen bağlantı:** biri seninle iletişim kurmak isteyince onaylarsın, sistem direkt o peer'in kolonunu açar: `Network > Kullanıcı A > Akışlar > Ses & Görüntü > Video {ses kıs/aç, bitrate ayarla, stream kapat} / Dosya gönder`. +- **Akış proxy/relay (özgün kısım):** bir peer'den gelen stream'i alıp **başka bir peerconnection'a proxy** etmek — `Arkadaşın görüntüsü > Ben > Arkadaşın`. Kimse bu kadar granüler yapmadı. +- Hedef cümle: **"WebRTC'nin ve tarayıcının yapabildiği her şeyi son user'a aç."** (Yeni issue'lar: Studio UI shell + Akış proxy/relay, 2.0.0.) + +## 7. Teknik Kısıtlar (KATI — ajan ve insan için) + +- **Engine:** Go (goroutine/channel/RWMutex veya per-room actor). +- **Frontend:** saf vanilla **ES module** JS, yüzlerce dosya native `import`. **React YOK, Parcel/Webpack/Vite YOK (bundler yok).** jQuery/moment serbest. TypeScript opsiyonel, ileride kaldırılabilir (proje sadeleşsin). Düz JS varsayılan. +- **SDK I/O sözleşmesi dondurulmuş.** Detay → `CLAUDE.md`. diff --git a/tools/gitea b/tools/gitea new file mode 100755 index 0000000..aa4d115 --- /dev/null +++ b/tools/gitea @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""gitea — git.saqut.com (Gitea) CLI (MWSE otonom ajanı için). + +- Cloudflare 1010'u aşmak için tarayıcı User-Agent'ı kullanır (düz curl takılır). +- Kimlik: önce ortam değişkenleri (GITEA_HOST/GITEA_USER/GITEA_TOKEN|GITEA_PASS/GITEA_REPO), + yoksa repo kökündeki .gitea-auth.json (gitignore'lu). +- Varsayılan repo: saqut/MWSE (.gitea-auth.json içindeki "repo" ile değiştirilebilir). + +Örnekler: + ./tools/gitea issue list --state open + ./tools/gitea issue view 22 + ./tools/gitea issue comment 22 --body "WSTS portu başladı" + ./tools/gitea issue close 21 --comment "tamamlandı, testler yeşil" + ./tools/gitea issue label 33 --add bug --remove docs + ./tools/gitea milestone list + ./tools/gitea wiki list ; ./tools/gitea wiki view Home + ./tools/gitea pr create --title "Go engine 0.1.0" --head go-rewrite --base stable --body-file PR.md + ./tools/gitea pr list +""" +import os, sys, json, base64, argparse, urllib.request, urllib.error, urllib.parse + +UA = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" +HOST = USER = AUTHHDR = REPO = None + +def _find_auth(): + here = os.path.dirname(os.path.abspath(__file__)) + for d in (os.getcwd(), here, os.path.dirname(here)): + p = os.path.join(d, ".gitea-auth.json") + if os.path.isfile(p): + return p + return None + +def load_auth(): + global HOST, USER, AUTHHDR, REPO + host = os.environ.get("GITEA_HOST"); user = os.environ.get("GITEA_USER") + token = os.environ.get("GITEA_TOKEN"); pw = os.environ.get("GITEA_PASS") + repo = os.environ.get("GITEA_REPO") + if not (host and user and (token or pw)): + f = _find_auth() + if f: + try: + d = json.load(open(f, encoding="utf-8")) + except Exception as e: + sys.exit(f"HATA: {f} okunamadı: {e}") + host = host or d.get("host"); user = user or d.get("user") + token = token or d.get("token"); pw = pw or d.get("password") + repo = repo or d.get("repo") + if not (host and user and (token or pw)): + sys.exit("HATA: kimlik yok. Repo kökünde .gitea-auth.json oluştur " + "(host,user,password|token) ya da GITEA_* ortam değişkenlerini ver.") + HOST = host.rstrip("/") + USER = user + AUTHHDR = ("token " + token) if token else ("Basic " + base64.b64encode(f"{user}:{pw}".encode()).decode()) + REPO = repo or "saqut/MWSE" + +def api(method, path, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(HOST + "/api/v1" + path, data=data, method=method) + req.add_header("User-Agent", UA) + req.add_header("Authorization", AUTHHDR) + req.add_header("Accept", "application/json") + if data is not None: + req.add_header("Content-Type", "application/json") + try: + with urllib.request.urlopen(req, timeout=40) as r: + t = r.read().decode() + return r.status, (json.loads(t) if t.strip() else {}) + except urllib.error.HTTPError as e: + t = e.read().decode() + try: t = json.loads(t) + except Exception: pass + return e.code, t + except Exception as e: + return "ERR", str(e) + +def R(p): return f"/repos/{REPO}{p}" +def die(s, r): sys.exit(f"API hata {s}: {r}") +def ok(s): return s in (200, 201, 204) + +def body_text(a): + if getattr(a, "body_file", None): + return open(a.body_file, encoding="utf-8").read() + return a.body or "" + +def labels_map(): + s, r = api("GET", R("/labels?limit=100")) + if not ok(s): die(s, r) + return {x["name"]: x["id"] for x in r} + +def milestone_id(title): + s, r = api("GET", R("/milestones?state=all&limit=100")) + if not ok(s): die(s, r) + for m in r: + if m["title"] == title: return m["id"] + sys.exit(f"milestone bulunamadı: {title}") + +# ---- issue ---- +def c_issue_list(a): + q = f"?state={a.state}&type=issues&limit={a.limit}" + if a.labels: q += "&labels=" + urllib.parse.quote(a.labels) + if a.milestone: q += "&milestones=" + urllib.parse.quote(a.milestone) + s, r = api("GET", R("/issues" + q)) + if not ok(s): die(s, r) + if a.json: print(json.dumps(r, ensure_ascii=False, indent=2)); return + for i in r: + ms = (i.get("milestone") or {}).get("title", "-") + lbl = ",".join(l["name"] for l in i.get("labels", [])) + print(f"#{i['number']:<3} [{i['state']:<6}] ({ms:<6}) {i['title']}" + (f" [{lbl}]" if lbl else "")) + print(f"-- {len(r)} issue", file=sys.stderr) + +def c_issue_view(a): + s, i = api("GET", R(f"/issues/{a.num}")) + if not ok(s): die(s, i) + ms = (i.get("milestone") or {}).get("title", "-") + lbl = ",".join(l["name"] for l in i.get("labels", [])) + print(f"#{i['number']} [{i['state']}] milestone={ms} labels={lbl}\n{i['title']}\n{'-'*60}\n{i.get('body') or ''}") + s, c = api("GET", R(f"/issues/{a.num}/comments")) + if ok(s) and c: + print("\n--- yorumlar ---") + for cm in c: print(f"@{cm['user']['login']}: {cm['body']}") + +def c_issue_create(a): + b = {"title": a.title, "body": body_text(a)} + if a.milestone: b["milestone"] = milestone_id(a.milestone) + if a.labels: + m = labels_map(); b["labels"] = [m[x] for x in a.labels.split(",") if x in m] + s, r = api("POST", R("/issues"), b) + print(f"oluşturuldu #{r['number']}" if ok(s) else f"hata {s}: {r}") + +def c_issue_close(a): + if a.comment: api("POST", R(f"/issues/{a.num}/comments"), {"body": a.comment}) + s, r = api("PATCH", R(f"/issues/{a.num}"), {"state": "closed"}) + print(f"kapatıldı #{a.num}" if ok(s) else f"hata {s}: {r}") + +def c_issue_reopen(a): + s, r = api("PATCH", R(f"/issues/{a.num}"), {"state": "open"}) + print(f"açıldı #{a.num}" if ok(s) else f"hata {s}: {r}") + +def c_issue_comment(a): + s, r = api("POST", R(f"/issues/{a.num}/comments"), {"body": body_text(a)}) + print("yorum eklendi" if ok(s) else f"hata {s}: {r}") + +def c_issue_edit(a): + b = {} + if a.title: b["title"] = a.title + if a.body or a.body_file: b["body"] = body_text(a) + if a.milestone: b["milestone"] = milestone_id(a.milestone) + s, r = api("PATCH", R(f"/issues/{a.num}"), b) + print(f"güncellendi #{a.num}" if ok(s) else f"hata {s}: {r}") + +def c_issue_label(a): + m = labels_map() + if a.add: + ids = [m[x] for x in a.add.split(",") if x in m] + s, r = api("POST", R(f"/issues/{a.num}/labels"), {"labels": ids}) + print(f"eklendi: {a.add}" if ok(s) else f"hata {s}: {r}") + if a.remove: + for x in a.remove.split(","): + if x in m: api("DELETE", R(f"/issues/{a.num}/labels/{m[x]}")) + print(f"çıkarıldı: {a.remove}") + +# ---- label / milestone ---- +def c_label_list(a): + s, r = api("GET", R("/labels?limit=100")) + if not ok(s): die(s, r) + for l in r: print(f"{l['id']:<4} {l['name']:<16} #{l['color']}") + +def c_milestone_list(a): + s, r = api("GET", R("/milestones?state=all&limit=100")) + if not ok(s): die(s, r) + for m in r: + print(f"{m['title']:<8} açık={m['open_issues']:<3} kapalı={m['closed_issues']:<3} {m.get('description','')[:60]}") + +# ---- wiki ---- +def c_wiki_list(a): + s, r = api("GET", R("/wiki/pages?limit=100")) + if not ok(s): die(s, r) + for p in r: print(p["title"]) + +def c_wiki_view(a): + s, r = api("GET", R("/wiki/page/" + urllib.parse.quote(a.page))) + if not ok(s): die(s, r) + print(base64.b64decode(r["content_base64"]).decode("utf-8", "replace")) + +def c_wiki_edit(a): + content = open(a.content_file, encoding="utf-8").read() + cb = base64.b64encode(content.encode()).decode() + s, _ = api("GET", R("/wiki/page/" + urllib.parse.quote(a.page))) + if ok(s): + s, r = api("PATCH", R("/wiki/page/" + urllib.parse.quote(a.page)), + {"title": a.page, "content_base64": cb, "message": a.message}) + else: + s, r = api("POST", R("/wiki/new"), {"title": a.page, "content_base64": cb, "message": a.message}) + print("wiki yazıldı" if ok(s) else f"hata {s}: {r}") + +# ---- pr ---- +def c_pr_list(a): + s, r = api("GET", R(f"/pulls?state={a.state}&limit=50")) + if not ok(s): die(s, r) + for p in r: + print(f"#{p['number']} [{p['state']}] {p['head']['ref']}→{p['base']['ref']} {p['title']}") + +def c_pr_view(a): + s, p = api("GET", R(f"/pulls/{a.num}")) + if not ok(s): die(s, p) + print(f"#{p['number']} [{p['state']}] {p['head']['ref']}→{p['base']['ref']}\n{p['title']}\n{'-'*60}\n{p.get('body') or ''}") + +def c_pr_create(a): + s, r = api("POST", R("/pulls"), {"title": a.title, "body": body_text(a), "head": a.head, "base": a.base}) + print(f"PR #{r['number']} oluşturuldu" if ok(s) else f"hata {s}: {r}") + +def build_parser(): + p = argparse.ArgumentParser(prog="gitea", description="git.saqut.com CLI") + sub = p.add_subparsers(dest="grp", required=True) + + gi = sub.add_parser("issue").add_subparsers(dest="act", required=True) + x = gi.add_parser("list"); x.add_argument("--state", default="open", choices=["open","closed","all"]) + x.add_argument("--labels"); x.add_argument("--milestone"); x.add_argument("--limit", default=50); x.add_argument("--json", action="store_true"); x.set_defaults(f=c_issue_list) + x = gi.add_parser("view"); x.add_argument("num"); x.set_defaults(f=c_issue_view) + x = gi.add_parser("create"); x.add_argument("--title", required=True); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--milestone"); x.add_argument("--labels"); x.set_defaults(f=c_issue_create) + x = gi.add_parser("edit"); x.add_argument("num"); x.add_argument("--title"); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--milestone"); x.set_defaults(f=c_issue_edit) + x = gi.add_parser("close"); x.add_argument("num"); x.add_argument("--comment"); x.set_defaults(f=c_issue_close) + x = gi.add_parser("reopen"); x.add_argument("num"); x.set_defaults(f=c_issue_reopen) + x = gi.add_parser("comment"); x.add_argument("num"); x.add_argument("--body"); x.add_argument("--body-file"); x.set_defaults(f=c_issue_comment) + x = gi.add_parser("label"); x.add_argument("num"); x.add_argument("--add"); x.add_argument("--remove"); x.set_defaults(f=c_issue_label) + + gl = sub.add_parser("label").add_subparsers(dest="act", required=True) + gl.add_parser("list").set_defaults(f=c_label_list) + + gm = sub.add_parser("milestone").add_subparsers(dest="act", required=True) + gm.add_parser("list").set_defaults(f=c_milestone_list) + + gw = sub.add_parser("wiki").add_subparsers(dest="act", required=True) + gw.add_parser("list").set_defaults(f=c_wiki_list) + x = gw.add_parser("view"); x.add_argument("page"); x.set_defaults(f=c_wiki_view) + x = gw.add_parser("edit"); x.add_argument("page"); x.add_argument("--content-file", required=True); x.add_argument("--message", default="wiki güncelleme"); x.set_defaults(f=c_wiki_edit) + + gp = sub.add_parser("pr").add_subparsers(dest="act", required=True) + x = gp.add_parser("list"); x.add_argument("--state", default="open", choices=["open","closed","all"]); x.set_defaults(f=c_pr_list) + x = gp.add_parser("view"); x.add_argument("num"); x.set_defaults(f=c_pr_view) + x = gp.add_parser("create"); x.add_argument("--title", required=True); x.add_argument("--body"); x.add_argument("--body-file"); x.add_argument("--head", required=True); x.add_argument("--base", default="stable"); x.set_defaults(f=c_pr_create) + return p + +def main(): + args = build_parser().parse_args() + load_auth() + args.f(args) + +if __name__ == "__main__": + main()