""" Gitea Service Adapter Handles Idea Dossier repository creation, full document tree synchronization, organization management (under 'thinkstorm' org), and OAuth2 authentication. """ import json import time import re import base64 import urllib.request import urllib.parse import urllib.error import asyncio import subprocess from pathlib import Path from typing import Dict, Any, Optional, List from .base import BaseServiceAdapter, ServiceHealth from ..config import config, BASE_DIR ARTIFACTS_DIR = BASE_DIR / "data" / "artifacts" ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True) class GiteaAdapter(BaseServiceAdapter): def __init__(self, endpoint: str = "https://git.labyricorn.com", api_token: str = ""): super().__init__(service_id="gitea", endpoint=endpoint, api_key=api_token or config.services.gitea_api_token) self.org_name = "thinkstorm" def get_effective_token(self) -> str: """Retrieves configured API token from instance, DB configuration, or environment.""" if self.api_key: return self.api_key try: from ..database import get_db with get_db() as conn: row = conn.execute("SELECT api_key_raw FROM service_configurations WHERE id = 'gitea'").fetchone() if row and row["api_key_raw"]: return row["api_key_raw"] except Exception: pass return config.services.gitea_api_token or "" async def check_health(self) -> ServiceHealth: start = time.time() try: url = f"{self.endpoint}/api/v1/version" req = urllib.request.Request(url, headers={"User-Agent": "ThinkStorm-Orchestrator/0.1"}) loop = asyncio.get_running_loop() def fetch(): with urllib.request.urlopen(req, timeout=5.0) as resp: return resp.read() raw = await loop.run_in_executor(None, fetch) data = json.loads(raw.decode("utf-8")) version = data.get("version", "unknown") elapsed = int((time.time() - start) * 1000) return ServiceHealth( service_id=self.service_id, healthy=True, endpoint=self.endpoint, message=f"Gitea online (v{version})", response_time_ms=elapsed, extra={"version": version} ) except Exception as e: elapsed = int((time.time() - start) * 1000) return ServiceHealth( service_id=self.service_id, healthy=False, endpoint=self.endpoint, message=f"Gitea connection error: {str(e)}", response_time_ms=elapsed ) def _sync_files_via_api(self, token: str, repo_slug: str, files_dict: Dict[str, str]) -> None: """Commits or updates multiple files directly into the Gitea repository via Contents API.""" script_payload = { "token": token, "repo": f"{self.org_name}/{repo_slug}", "files": files_dict } py_code = f""" import urllib.request, json, base64, sys data = json.loads({json.dumps(json.dumps(script_payload))}) token = data['token'] repo = data['repo'] files = data['files'] for path, content in files.items(): get_url = f'https://git.labyricorn.com/api/v1/repos/{{repo}}/contents/{{path}}' sha = None try: req = urllib.request.Request(get_url, headers={{'Authorization': f'token {{token}}', 'User-Agent': 'ThinkStorm/0.1'}}) with urllib.request.urlopen(req, timeout=8) as resp: d = json.loads(resp.read().decode('utf-8')) sha = d.get('sha') except Exception: pass payload = {{ 'content': base64.b64encode(content.encode('utf-8')).decode('utf-8'), 'message': f'Sync {{path}} into ThinkStorm dossier', 'branch': 'main' }} if sha: payload['sha'] = sha method = 'PUT' else: method = 'POST' url = f'https://git.labyricorn.com/api/v1/repos/{{repo}}/contents/{{path}}' req = urllib.request.Request( url, data=json.dumps(payload).encode('utf-8'), headers={{'Content-Type': 'application/json', 'Authorization': f'token {{token}}', 'User-Agent': 'ThinkStorm/0.1'}}, method=method ) try: with urllib.request.urlopen(req, timeout=8) as resp: pass except Exception as e: print(f'File sync notice for {{path}}: {{e}}') """ try: subprocess.run( ["ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "root@10.138.2.48", f"python3 -c {json.dumps(py_code)}"], capture_output=True, text=True, timeout=25 ) except Exception as e: print(f"[Gitea] Remote file commit notice: {e}") async def persist_idea_dossier_repo( self, idea_id: str, title: str, summary: str, original_text: str, categories: List[str], tags: List[str], lifecycle_state: str, research_docs: Dict[str, str], outputs: Dict[str, str], provenance_runs: List[Dict[str, Any]], existing_repo_url: Optional[str] = None ) -> Dict[str, Any]: """Creates a dedicated Gitea Project repository and synchronizes all files into it.""" # 1. Local disk directory idea_dir = ARTIFACTS_DIR / idea_id idea_dir.mkdir(parents=True, exist_ok=True) (idea_dir / "research").mkdir(parents=True, exist_ok=True) (idea_dir / "provenance").mkdir(parents=True, exist_ok=True) (idea_dir / "outputs").mkdir(parents=True, exist_ok=True) cat_str = ", ".join(categories) if categories else "General" tag_str = ", ".join(f"#{t}" for t in tags) if tags else "None" # 2. Build README.md readme_md = ( f"# {idea_id}: {title}\n\n" f"**Lifecycle State:** `{lifecycle_state}` \n" f"**Categories:** {cat_str} \n" f"**Tags:** {tag_str} \n\n" f"## Executive Summary\n{summary}\n\n" f"## Original Submission Prompt\n> {original_text.strip()}\n\n" f"## Project Structure\n" f"- `research/`: Automated competitor analysis, prior art, deep research, and technical feasibility reports.\n" f"- `outputs/`: Multi-modal work track deliverables (articles, code scaffolds, business models).\n" f"- `provenance/`: Execution run telemetry, model audit trails, and token attribution logs.\n" f"- `metadata.json`: Machine-readable metadata schema.\n" ) (idea_dir / "README.md").write_text(readme_md, encoding="utf-8") (idea_dir / "idea.md").write_text(readme_md, encoding="utf-8") # 3. Build metadata.json meta = { "id": idea_id, "title": title, "summary": summary, "categories": categories, "tags": tags, "lifecycle_state": lifecycle_state, "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "organization": self.org_name } meta_json_str = json.dumps(meta, indent=2) (idea_dir / "metadata.json").write_text(meta_json_str, encoding="utf-8") # 4. Research docs files_to_sync = { "README.md": readme_md, "metadata.json": meta_json_str } for filename, content in research_docs.items(): safe_name = filename.replace("/", "_") (idea_dir / "research" / safe_name).write_text(content, encoding="utf-8") files_to_sync[f"research/{safe_name}"] = content # 5. Outputs for filename, content in outputs.items(): out_path = idea_dir / "outputs" / filename out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(content, encoding="utf-8") files_to_sync[f"outputs/{filename}"] = content # 6. Provenance for run in provenance_runs: run_id = run.get("id", f"run-{int(time.time())}") (idea_dir / "provenance" / f"{run_id}.json").write_text(json.dumps(run, indent=2), encoding="utf-8") # 7. Gitea Repository sync under 'thinkstorm' organization clean_slug = re.sub(r'[^a-zA-Z0-9_-]', '-', idea_id.lower()).strip('-') repo_name = f"ts-{clean_slug.replace('ts-', '')}" repo_url = existing_repo_url or f"{self.endpoint}/{self.org_name}/{repo_name}" clone_url = f"{self.endpoint}/{self.org_name}/{repo_name}.git" ssh_url = f"ssh://git@git.labyricorn.com:22/{self.org_name}/{repo_name}.git" token = self.get_effective_token() if token: loop = asyncio.get_running_loop() def sync_gitea(): try: # 1. Create repo under org if missing create_script = f""" import urllib.request, json token = '{token}' repo_name = '{repo_name}' org = '{self.org_name}' # Ensure org try: req = urllib.request.Request(f'https://git.labyricorn.com/api/v1/orgs/{{org}}', headers={{'Authorization': f'token {{token}}'}}) with urllib.request.urlopen(req, timeout=5): pass except Exception: try: req = urllib.request.Request('https://git.labyricorn.com/api/v1/orgs', data=json.dumps({{'username': org, 'visibility': 'public'}}).encode(), headers={{'Content-Type': 'application/json', 'Authorization': f'token {{token}}'}}, method='POST') with urllib.request.urlopen(req, timeout=5): pass except Exception: pass # Create repo try: payload = {{'name': repo_name, 'description': f'[{idea_id}] {title}', 'private': False, 'auto_init': True}} req = urllib.request.Request(f'https://git.labyricorn.com/api/v1/orgs/{{org}}/repos', data=json.dumps(payload).encode(), headers={{'Content-Type': 'application/json', 'Authorization': f'token {{token}}'}}, method='POST') with urllib.request.urlopen(req, timeout=6): pass except Exception: pass """ subprocess.run( ["ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "root@10.138.2.48", f"python3 -c {json.dumps(create_script)}"], capture_output=True, text=True, timeout=10 ) # 2. Push full file tree to Gitea self._sync_files_via_api(token, repo_name, files_to_sync) return f"{self.endpoint}/{self.org_name}/{repo_name}" except Exception as e: print(f"[Gitea] Sync error: {e}") return repo_url try: actual_url = await loop.run_in_executor(None, sync_gitea) repo_url = actual_url or repo_url except Exception as e: print(f"[Gitea] Async sync exception: {e}") return { "repo_name": f"{self.org_name}/{repo_name}", "repo_url": repo_url, "clone_url": clone_url, "ssh_url": ssh_url, "local_path": str(idea_dir) } async def persist_work_track_outputs( self, idea_id: str, track_name: str, outputs: Dict[str, str] ) -> Dict[str, Any]: """Persists Work Track deliverables to local dossier and commits to Gitea project.""" idea_dir = ARTIFACTS_DIR / idea_id out_subdir = idea_dir / "outputs" / track_name.lower().replace(" ", "-") out_subdir.mkdir(parents=True, exist_ok=True) files_to_sync = {} for filename, content in outputs.items(): file_path = out_subdir / filename file_path.write_text(content, encoding="utf-8") files_to_sync[f"outputs/{track_name.lower().replace(' ', '-')}/{filename}"] = content clean_slug = re.sub(r'[^a-zA-Z0-9_-]', '-', idea_id.lower()).strip('-') repo_name = f"ts-{clean_slug.replace('ts-', '')}" repo_url = f"{self.endpoint}/{self.org_name}/{repo_name}" token = self.get_effective_token() if token and files_to_sync: loop = asyncio.get_running_loop() try: await loop.run_in_executor(None, lambda: self._sync_files_via_api(token, repo_name, files_to_sync)) except Exception as e: print(f"[Gitea] Work track sync notice: {e}") return { "status": "SUCCESS", "path": str(out_subdir), "repo_url": repo_url } async def graduate_project(self, idea_id: str, title: str, summary: str, repo_name: str = "") -> Dict[str, Any]: """Graduates an incubated Coding Project work track to a dedicated repository under 'thinkstorm'.""" clean_title = re.sub(r'[^a-zA-Z0-9_-]', '-', title.lower()).strip('-')[:30] or "project" slug = repo_name or f"ts-{idea_id.lower()}-{clean_title}" repo_url = f"{self.endpoint}/{self.org_name}/{slug}" token = self.get_effective_token() if token: loop = asyncio.get_running_loop() def create_remote(): try: create_script = f""" import urllib.request, json token = '{token}' slug = '{slug}' org = '{self.org_name}' payload = {{'name': slug, 'description': f'[{idea_id}] {title}', 'private': False, 'auto_init': True}} req = urllib.request.Request(f'https://git.labyricorn.com/api/v1/orgs/{{org}}/repos', data=json.dumps(payload).encode(), headers={{'Content-Type': 'application/json', 'Authorization': f'token {{token}}'}}, method='POST') try: with urllib.request.urlopen(req, timeout=6): pass except Exception: pass """ subprocess.run( ["ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "root@10.138.2.48", f"python3 -c {json.dumps(create_script)}"], capture_output=True, text=True, timeout=10 ) return f"{self.endpoint}/{self.org_name}/{slug}" except Exception as e: print(f"[Gitea] Project graduation notice: {e}") return repo_url actual_url = await loop.run_in_executor(None, create_remote) repo_url = actual_url or repo_url return { "repo_name": f"{self.org_name}/{slug}", "repo_url": repo_url, "graduated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) } async def verify_oauth_user(self, access_token: str) -> Optional[Dict[str, Any]]: """Fetches user profile details using Gitea OAuth access token.""" url = f"{self.endpoint}/api/v1/user" req = urllib.request.Request( url, headers={ "Authorization": f"token {access_token}", "User-Agent": "ThinkStorm-Orchestrator/0.1" } ) loop = asyncio.get_running_loop() def fetch(): with urllib.request.urlopen(req, timeout=8.0) as resp: return resp.read() try: raw = await loop.run_in_executor(None, fetch) return json.loads(raw.decode("utf-8")) except Exception as e: print(f"[Gitea] OAuth user verify error: {e}") return None