File size: 22,495 Bytes
cce8120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

echo "============================================================"
echo " DOLOR3V BACKEND β€” MISSING CONTRACTS + REAL TOOL REPAIR"
echo "============================================================"

echo
echo "===== 1. FIX app.py cms_router IMPORT CRASH ====="

python3 - <<'PY'
from pathlib import Path

p = Path("app.py")
src = p.read_text()

# Remove the broken cms_router line if it's there without an import
if "app.include_router(cms_router)" in src and "from" not in src.split("cms_router")[0].split("\n")[-1]:
    src = src.replace("app.include_router(cms_router)\n", "")
    p.write_text(src)
    print("[FIXED] Removed unresolved cms_router from app.py")
else:
    print("[OK] app.py cms_router already handled or import present")
PY

echo
echo "===== 2. ADD /api/deploy/settings ROUTE ====="

mkdir -p backend/api/routes

cat > backend/api/routes/deploy_settings.py << 'PYEOF'
"""
Production deployment-settings route.
GET  /api/deploy/settings  β€” returns current deployment configuration
POST /api/deploy/settings  β€” updates deployment configuration
"""
from __future__ import annotations

import json
import logging
import os
from pathlib import Path
from typing import Any

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

logger = logging.getLogger("dolor3v.deploy.settings")

router = APIRouter(prefix="/api/deploy", tags=["deploy"])

SETTINGS_FILE = Path(os.environ.get("DEPLOY_SETTINGS_PATH", "/tmp/deploy_settings.json"))

DEFAULTS: dict[str, Any] = {
    "target": os.environ.get("DEPLOY_TARGET", "cloudflare"),
    "cloudflare_account_id": os.environ.get("CLOUDFLARE_ACCOUNT_ID", ""),
    "cloudflare_api_token": "",
    "render_service_id": os.environ.get("RENDER_SERVICE_ID", ""),
    "hf_space": os.environ.get("HF_SPACE", "Daviddolor/Travelerdev"),
    "auto_deploy": False,
    "build_command": "npm run build",
    "output_dir": ".next",
    "environment": os.environ.get("ENVIRONMENT", "production"),
    "backend_url": os.environ.get(
        "TRAVELER_BACKEND_URL",
        os.environ.get("NEXT_PUBLIC_BACKEND_URL", ""),
    ),
}


def _load() -> dict[str, Any]:
    if SETTINGS_FILE.exists():
        try:
            return {**DEFAULTS, **json.loads(SETTINGS_FILE.read_text())}
        except Exception:
            pass
    return dict(DEFAULTS)


def _save(data: dict[str, Any]) -> None:
    SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
    SETTINGS_FILE.write_text(json.dumps(data, indent=2))


class DeploySettingsUpdate(BaseModel):
    target: str | None = None
    cloudflare_account_id: str | None = None
    cloudflare_api_token: str | None = None
    render_service_id: str | None = None
    hf_space: str | None = None
    auto_deploy: bool | None = None
    build_command: str | None = None
    output_dir: str | None = None
    environment: str | None = None
    backend_url: str | None = None


@router.get("/settings")
async def get_deploy_settings() -> dict[str, Any]:
    """Return current deployment configuration (secrets redacted)."""
    settings = _load()
    redacted = {**settings}
    if redacted.get("cloudflare_api_token"):
        redacted["cloudflare_api_token"] = "***"
    return {"success": True, "settings": redacted}


@router.post("/settings")
async def update_deploy_settings(body: DeploySettingsUpdate) -> dict[str, Any]:
    """Persist deployment configuration updates."""
    current = _load()
    updates = body.model_dump(exclude_none=True)
    current.update(updates)
    try:
        _save(current)
    except Exception as exc:
        raise HTTPException(status_code=500, detail=f"Failed to persist settings: {exc}") from exc
    return {"success": True, "updated": list(updates.keys())}
PYEOF

echo "[CREATED] backend/api/routes/deploy_settings.py"

echo
echo "===== 3. REAL GITHUB SEARCH TOOL ====="

cat > backend/tools/github_search.py << 'PYEOF'
"""
Real GitHub repository + code search via GitHub REST API.
No auth required for public repos (60 req/hr).
Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr.
"""
from __future__ import annotations

import logging
import os
import time
from typing import Any

import httpx

logger = logging.getLogger("dolor3v.tools.github_search")

_GITHUB_API = "https://api.github.com"
_HEADERS_BASE = {
    "Accept": "application/vnd.github+json",
    "X-GitHub-Api-Version": "2022-11-28",
    "User-Agent": "DOLOR3V-TravelerDev/1.0",
}


def _auth_headers() -> dict[str, str]:
    token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")
    if token:
        return {**_HEADERS_BASE, "Authorization": f"Bearer {token}"}
    return dict(_HEADERS_BASE)


async def search_repositories(
    query: str,
    sort: str = "stars",
    order: str = "desc",
    per_page: int = 10,
) -> dict[str, Any]:
    """Search GitHub repositories. Returns real API data."""
    url = f"{_GITHUB_API}/search/repositories"
    params = {"q": query, "sort": sort, "order": order, "per_page": per_page}

    t0 = time.monotonic()
    async with httpx.AsyncClient(timeout=15.0) as client:
        resp = await client.get(url, params=params, headers=_auth_headers())

    latency_ms = round((time.monotonic() - t0) * 1000)

    if resp.status_code == 403:
        remaining = resp.headers.get("X-RateLimit-Remaining", "?")
        reset = resp.headers.get("X-RateLimit-Reset", "?")
        return {
            "error": "GitHub rate limit exceeded",
            "remaining": remaining,
            "reset_at": reset,
            "tip": "Set GH_TOKEN or GITHUB_TOKEN env var for 5000 req/hr",
        }

    if resp.status_code != 200:
        return {
            "error": f"GitHub API returned {resp.status_code}",
            "body": resp.text[:500],
        }

    data = resp.json()
    items = data.get("items", [])

    results = []
    for item in items:
        results.append({
            "name": item.get("full_name"),
            "description": item.get("description"),
            "stars": item.get("stargazers_count"),
            "forks": item.get("forks_count"),
            "language": item.get("language"),
            "url": item.get("html_url"),
            "clone_url": item.get("clone_url"),
            "topics": item.get("topics", []),
            "updated_at": item.get("updated_at"),
            "open_issues": item.get("open_issues_count"),
        })

    return {
        "query": query,
        "total_count": data.get("total_count", 0),
        "returned": len(results),
        "latency_ms": latency_ms,
        "results": results,
        "rate_limit_remaining": resp.headers.get("X-RateLimit-Remaining", "unknown"),
    }


async def search_code(
    query: str,
    per_page: int = 10,
) -> dict[str, Any]:
    """Search GitHub code. Requires GH_TOKEN for reliable access."""
    url = f"{_GITHUB_API}/search/code"
    params = {"q": query, "per_page": per_page}

    token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")
    if not token:
        return {
            "error": "Code search requires authentication",
            "tip": "Set GH_TOKEN env var",
        }

    t0 = time.monotonic()
    async with httpx.AsyncClient(timeout=15.0) as client:
        resp = await client.get(url, params=params, headers=_auth_headers())

    latency_ms = round((time.monotonic() - t0) * 1000)

    if resp.status_code != 200:
        return {"error": f"GitHub code search returned {resp.status_code}", "body": resp.text[:500]}

    data = resp.json()
    items = data.get("items", [])

    return {
        "query": query,
        "total_count": data.get("total_count", 0),
        "latency_ms": latency_ms,
        "results": [
            {
                "name": i.get("name"),
                "path": i.get("path"),
                "repo": i.get("repository", {}).get("full_name"),
                "url": i.get("html_url"),
                "sha": i.get("sha"),
            }
            for i in items
        ],
    }
PYEOF

echo "[CREATED] backend/tools/github_search.py"

echo
echo "===== 4. REAL ANDROID DEVELOPER DOCS LOOKUP ====="

cat > backend/tools/android_docs.py << 'PYEOF'
"""
Real Android Developer documentation lookup.
Fetches from developer.android.com search and reference pages.
No authentication required.
"""
from __future__ import annotations

import logging
import re
import time
from typing import Any
from urllib.parse import quote_plus

import httpx

logger = logging.getLogger("dolor3v.tools.android_docs")

_BASE = "https://developer.android.com"
_SEARCH_URL = f"{_BASE}/s/results"
_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Linux; Android 14; Pixel 8) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0.0.0 Mobile Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
}

# Curated fast-path: well-known Android API references
_KNOWN_REFS: dict[str, str] = {
    "activity": "/reference/android/app/Activity",
    "fragment": "/reference/androidx/fragment/app/Fragment",
    "viewmodel": "/reference/androidx/lifecycle/ViewModel",
    "livedata": "/reference/androidx/lifecycle/LiveData",
    "room": "/reference/androidx/room/package-summary",
    "compose": "/jetpack/compose",
    "navigation": "/guide/navigation",
    "workmanager": "/reference/androidx/work/WorkManager",
    "coroutines": "/kotlin/coroutines",
    "hilt": "/training/dependency-injection/hilt-android",
    "retrofit": "https://square.github.io/retrofit/",
    "jetpack": "/jetpack",
    "manifest": "/guide/topics/manifest/manifest-intro",
    "permissions": "/guide/topics/permissions/overview",
    "intent": "/reference/android/content/Intent",
    "service": "/reference/android/app/Service",
    "broadcastreceiver": "/reference/android/content/BroadcastReceiver",
    "contentprovider": "/reference/android/content/ContentProvider",
    "recyclerview": "/reference/androidx/recyclerview/widget/RecyclerView",
    "constraintlayout": "/reference/androidx/constraintlayout/widget/ConstraintLayout",
    "gradle": "/build/releases/gradle-plugin",
    "apk": "/studio/build/build-variants",
    "aab": "/guide/app-bundle",
    "proguard": "/studio/build/shrink-code",
    "keystore": "/training/articles/keystore",
}


def _fast_path(query: str) -> str | None:
    q = query.lower().strip()
    for key, path in _KNOWN_REFS.items():
        if key in q:
            return path if path.startswith("http") else f"{_BASE}{path}"
    return None


async def lookup_android_docs(
    query: str,
    max_results: int = 5,
) -> dict[str, Any]:
    """
    Fetch real Android Developer documentation.
    Tries fast-path known references first, then falls back to search.
    """
    t0 = time.monotonic()

    # Fast path for known APIs
    fast_url = _fast_path(query)
    results = []

    async with httpx.AsyncClient(
        timeout=20.0,
        follow_redirects=True,
        headers=_HEADERS,
    ) as client:

        if fast_url:
            try:
                resp = await client.get(fast_url)
                if resp.status_code == 200:
                    # Extract title and description from HTML
                    html = resp.text
                    title_match = re.search(r"<title[^>]*>([^<]+)</title>", html, re.I)
                    title = title_match.group(1).strip() if title_match else query

                    # Extract meta description
                    desc_match = re.search(
                        r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']',
                        html, re.I
                    )
                    description = desc_match.group(1).strip() if desc_match else ""

                    # Extract h1/h2 headings as sections
                    headings = re.findall(r"<h[12][^>]*>([^<]+)</h[12]>", html, re.I)
                    sections = [re.sub(r"\s+", " ", h).strip() for h in headings[:8]]

                    results.append({
                        "title": title,
                        "url": fast_url,
                        "description": description,
                        "sections": sections,
                        "source": "fast_path",
                    })
            except Exception as exc:
                logger.warning("Fast path fetch failed for %s: %s", fast_url, exc)

        # Search fallback
        if not results:
            try:
                search_url = f"{_BASE}/s/results?q={quote_plus(query)}"
                resp = await client.get(search_url)
                html = resp.text if resp.status_code == 200 else ""

                # Parse search result links
                links = re.findall(
                    r'href=["\'](/(?:reference|guide|training|jetpack|develop|studio)[^"\'#?]*)["\']',
                    html,
                )
                seen: set[str] = set()
                for link in links:
                    if link not in seen:
                        seen.add(link)
                        full_url = f"{_BASE}{link}"
                        results.append({
                            "title": link.split("/")[-1].replace("-", " ").title(),
                            "url": full_url,
                            "description": "",
                            "source": "search",
                        })
                    if len(results) >= max_results:
                        break
            except Exception as exc:
                logger.warning("Android docs search failed: %s", exc)

        # If still nothing, return known reference index
        if not results:
            results = [
                {"title": k.title(), "url": f"{_BASE}{v}" if not v.startswith("http") else v, "source": "index"}
                for k, v in list(_KNOWN_REFS.items())[:max_results]
            ]

    latency_ms = round((time.monotonic() - t0) * 1000)

    return {
        "query": query,
        "results": results[:max_results],
        "latency_ms": latency_ms,
        "source_base": _BASE,
    }
PYEOF

echo "[CREATED] backend/tools/android_docs.py"

echo
echo "===== 5. WIRE NEW TOOLS INTO MCP DISPATCH ====="

cat > backend/tools/__init__.py << 'PYEOF'
"""DOLOR3V production tool implementations."""
from .github_search import search_repositories, search_code
from .android_docs import lookup_android_docs

__all__ = ["search_repositories", "search_code", "lookup_android_docs"]
PYEOF

echo "[CREATED] backend/tools/__init__.py"

echo
echo "===== 6. REGISTER ALL MISSING ROUTES IN app_part1 ====="

# Find app_part1 to understand existing structure
echo "[INFO] app_part1.py first 60 lines:"
head -60 app_part1.py 2>/dev/null || echo "[WARN] app_part1.py not found"

echo
echo "===== 7. CREATE app_routes_extension.py ====="

cat > app_routes_extension.py << 'PYEOF'
"""
DOLOR3V backend route extension.
Registers all missing production contracts not in app_part1.
Import this from app.py after app_part1.
"""
from __future__ import annotations

import logging
import time
from typing import Any

from fastapi import APIRouter
from pydantic import BaseModel

from backend.api.routes.deploy_settings import router as deploy_router
from backend.tools.github_search import search_repositories, search_code
from backend.tools.android_docs import lookup_android_docs

logger = logging.getLogger("dolor3v.routes.extension")

ext_router = APIRouter()


# ── /api/workspace/fix ──────────────────────────────────────────────

class WorkspaceFixRequest(BaseModel):
    path: str
    content: str
    operation: str = "replace"


@ext_router.post("/api/workspace/fix")
async def workspace_fix(body: WorkspaceFixRequest) -> dict[str, Any]:
    """Apply a file-level patch to the active workspace."""
    if not body.path or not body.content:
        return {"success": False, "error": "path and content are required"}
    if body.operation not in ("replace", "patch", "create"):
        return {"success": False, "error": f"unsupported operation: {body.operation}"}
    # Real workspace mutation goes through preview_workspace
    try:
        from backend.preview.workspace import preview_workspace
        await preview_workspace.save_file(body.path, body.content)
        return {"success": True, "path": body.path, "operation": body.operation}
    except Exception as exc:
        logger.error("workspace_fix failed: %s", exc)
        return {"success": False, "error": str(exc)}


# ── /api/github/search ──────────────────────────────────────────────

class GitHubSearchRequest(BaseModel):
    query: str
    sort: str = "stars"
    per_page: int = 10


@ext_router.post("/api/github/search")
async def github_search(body: GitHubSearchRequest) -> dict[str, Any]:
    """Real GitHub repository search via GitHub REST API."""
    return await search_repositories(body.query, sort=body.sort, per_page=body.per_page)


@ext_router.get("/api/github/search")
async def github_search_get(q: str = "fastapi", sort: str = "stars", per_page: int = 10) -> dict[str, Any]:
    return await search_repositories(q, sort=sort, per_page=per_page)


# ── /api/android/docs ───────────────────────────────────────────────

@ext_router.get("/api/android/docs")
async def android_docs(q: str = "Activity", max_results: int = 5) -> dict[str, Any]:
    """Real Android Developer documentation lookup."""
    return await lookup_android_docs(q, max_results=max_results)


@ext_router.post("/api/android/docs")
async def android_docs_post(body: dict) -> dict[str, Any]:
    query = body.get("query", body.get("q", "Activity"))
    return await lookup_android_docs(query)


# ── /api/deploy/settings (alias via ext) ────────────────────────────
# deploy_router is included separately below


# ── /api/agent/run ──────────────────────────────────────────────────

class AgentRunRequest(BaseModel):
    message: str
    provider: str = "auto"
    model: str | None = None
    stream: bool = False


@ext_router.post("/api/agent/run")
async def agent_run(body: AgentRunRequest) -> dict[str, Any]:
    """Route agent prompts through the LLM gateway."""
    t0 = time.monotonic()
    try:
        from backend.llm.gateway import ModelGateway
        gateway = ModelGateway()
        result = await gateway.generate(
            prompt=body.message,
            intent="agent",
            model_hint=body.model,
        )
        return {
            "response": result,
            "provider": body.provider,
            "latency_ms": round((time.monotonic() - t0) * 1000),
        }
    except Exception as exc:
        logger.error("agent_run failed: %s", exc)
        return {"error": str(exc), "latency_ms": round((time.monotonic() - t0) * 1000)}


def register(app) -> None:
    """Call this from app.py to mount all extension routes."""
    app.include_router(ext_router)
    app.include_router(deploy_router)
    logger.info("Extension routes registered: workspace/fix, github/search, android/docs, deploy/settings, agent/run")
PYEOF

echo "[CREATED] app_routes_extension.py"

echo
echo "===== 8. PATCH app.py TO IMPORT EXTENSION ====="

python3 - <<'PY'
from pathlib import Path

p = Path("app.py")
src = p.read_text()

injection = "\nfrom app_routes_extension import register as _register_ext\n_register_ext(app)\n"

if "app_routes_extension" not in src:
    # Append after the last import/include block
    src = src.rstrip() + "\n" + injection
    p.write_text(src)
    print("[PATCHED] app.py now imports extension routes")
else:
    print("[OK] app.py already imports extension routes")
PY

echo
echo "===== 9. VERIFY IMPORT CHAIN ====="

python3 -c "
import sys
sys.path.insert(0, '.')
errors = []

try:
    from backend.tools.github_search import search_repositories
    print('[PASS] github_search import')
except Exception as e:
    errors.append(f'github_search: {e}')
    print(f'[FAIL] github_search: {e}')

try:
    from backend.tools.android_docs import lookup_android_docs
    print('[PASS] android_docs import')
except Exception as e:
    errors.append(f'android_docs: {e}')
    print(f'[FAIL] android_docs: {e}')

try:
    from backend.api.routes.deploy_settings import router
    print('[PASS] deploy_settings router import')
except Exception as e:
    errors.append(f'deploy_settings: {e}')
    print(f'[FAIL] deploy_settings: {e}')

try:
    from app_routes_extension import register
    print('[PASS] app_routes_extension import')
except Exception as e:
    errors.append(f'extension: {e}')
    print(f'[FAIL] extension: {e}')

if errors:
    print(f'\n[FAIL] {len(errors)} import(s) failed')
    sys.exit(1)
else:
    print('\n[PASS] All imports clean')
"

echo
echo "===== 10. LIVE TEST NEW TOOLS ====="

python3 -c "
import asyncio
from backend.tools.github_search import search_repositories
from backend.tools.android_docs import lookup_android_docs

async def run():
    print('[TEST] GitHub search: fastapi')
    r = await search_repositories('fastapi', per_page=3)
    if 'error' in r:
        print(f'  [WARN] {r[\"error\"]}')
    else:
        print(f'  [PASS] total_count={r[\"total_count\"]} returned={r[\"returned\"]} latency={r[\"latency_ms\"]}ms')
        for repo in r['results'][:2]:
            print(f'    β†’ {repo[\"name\"]} ⭐{repo[\"stars\"]}')

    print('[TEST] Android docs: Activity')
    d = await lookup_android_docs('Activity', max_results=3)
    print(f'  [PASS] latency={d[\"latency_ms\"]}ms results={len(d[\"results\"])}')
    for res in d['results'][:2]:
        print(f'    β†’ {res[\"title\"]} β€” {res[\"url\"]}')

asyncio.run(run())
"

echo
echo "===== 11. COMMIT AND PUSH TO HF ====="

echo "[nameserver fix]"
echo "nameserver 1.1.1.1" > /etc/resolv.conf

git add -A
git commit -m "feat: add deploy/settings, real github search, android docs, workspace/fix, agent/run routes"
git push hf main

echo
echo "============================================================"
echo " BACKEND CONTRACTS COMPLETE β€” PUSHING TO HF"
echo "============================================================"