Ajan çalışma sözleşmesi + gitea CLI + yol haritası dosyaları
CLAUDE.md (operating contract), todo.md (roadmap), tools/gitea CLI ve .gitea-auth.example.json depoya eklendi. Gerçek .gitea-auth.json gitignore'da. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c058feb22d
commit
f079ef5325
|
|
@ -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.
|
||||||
|
|
@ -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 <num>` 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 <num> --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.
|
||||||
|
|
@ -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\": \"<access-token>\" }. Gerçek dosya .gitea-auth.json adıyla kopyalanır ve .gitignore'dadır.",
|
||||||
|
"repo": "saqut/MWSE"
|
||||||
|
}
|
||||||
|
|
@ -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 <num>` · `milestone list` · `label list`
|
||||||
|
- Bittiğinde kapat: `./tools/gitea issue close <num> --comment "tamamlandı, go test -race yeşil"`
|
||||||
|
- Yorum / etiket: `./tools/gitea issue comment <num> --body "..."` · `issue label <num> --add go --remove docs`
|
||||||
|
- Wiki: `./tools/gitea wiki view <sayfa>` · `wiki edit <sayfa> --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.
|
||||||
|
|
@ -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`.
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue