devlinker 1.4.6__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {devlinker-1.4.6/devlinker.egg-info → devlinker-1.5.0}/PKG-INFO +15 -6
  2. devlinker-1.5.0/devlinker/dashboard_html.py +84 -0
  3. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detector_ai.py +1 -1
  4. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/devlinker_loader_instant.html +4 -1
  5. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/doctor.py +3 -3
  6. devlinker-1.5.0/devlinker/env_utils.py +36 -0
  7. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/fix.py +8 -2
  8. devlinker-1.5.0/devlinker/fixer.py +27 -0
  9. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/main.py +42 -212
  10. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/proxy.py +33 -121
  11. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/tunnel.py +31 -25
  12. {devlinker-1.4.6 → devlinker-1.5.0/devlinker.egg-info}/PKG-INFO +15 -6
  13. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/SOURCES.txt +4 -1
  14. devlinker-1.5.0/devlinker.egg-info/requires.txt +25 -0
  15. {devlinker-1.4.6 → devlinker-1.5.0}/pyproject.toml +14 -6
  16. devlinker-1.5.0/tests/test_proxy_runtime.py +105 -0
  17. devlinker-1.4.6/devlinker/fixer.py +0 -19
  18. devlinker-1.4.6/devlinker.egg-info/requires.txt +0 -10
  19. {devlinker-1.4.6 → devlinker-1.5.0}/LICENSE +0 -0
  20. {devlinker-1.4.6 → devlinker-1.5.0}/MANIFEST.in +0 -0
  21. {devlinker-1.4.6 → devlinker-1.5.0}/README.md +0 -0
  22. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/__init__.py +0 -0
  23. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/config.py +0 -0
  24. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detection_state.py +0 -0
  25. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detector.py +0 -0
  26. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/devlinker_loader_snippet.html +0 -0
  27. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/inspect.py +0 -0
  28. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/logger.py +0 -0
  29. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/monitor.py +0 -0
  30. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/runner.py +0 -0
  31. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/runtime_api.py +0 -0
  32. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/dependency_links.txt +0 -0
  33. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/entry_points.txt +0 -0
  34. {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/top_level.txt +0 -0
  35. {devlinker-1.4.6 → devlinker-1.5.0}/setup.cfg +0 -0
  36. {devlinker-1.4.6 → devlinker-1.5.0}/setup.py +0 -0
@@ -1,21 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.4.6
3
+ Version: 1.5.0
4
4
  Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
5
  Author-email: Mani <mani1028@users.noreply.github.com>
6
- Requires-Python: >=3.7
6
+ Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: click
10
- Requires-Dist: docker
11
10
  Requires-Dist: fastapi
12
11
  Requires-Dist: httpx
13
- Requires-Dist: pyngrok
14
- Requires-Dist: qrcode[pil]
12
+ Requires-Dist: PyYAML
15
13
  Requires-Dist: requests
16
- Requires-Dist: rich
17
14
  Requires-Dist: uvicorn
18
15
  Requires-Dist: websockets
16
+ Provides-Extra: docker
17
+ Requires-Dist: docker; extra == "docker"
18
+ Provides-Extra: tunnel
19
+ Requires-Dist: pyngrok; extra == "tunnel"
20
+ Provides-Extra: ui
21
+ Requires-Dist: rich; extra == "ui"
22
+ Provides-Extra: support
23
+ Requires-Dist: qrcode[pil]; extra == "support"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Provides-Extra: all
27
+ Requires-Dist: devlinker[docker,support,tunnel,ui]; extra == "all"
19
28
  Dynamic: license-file
20
29
 
21
30
  # Dev Linker
@@ -0,0 +1,84 @@
1
+ LOGS_DASHBOARD_HTML = """<!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>DevLinker API Logs</title>
7
+ <style>
8
+ :root { --bg:#f4f7fb; --card:#ffffff; --ink:#0f172a; --muted:#64748b; --ok:#065f46; --warn:#92400e; --err:#991b1b; --line:#dbe3ee; }
9
+ body { margin:0; font-family:"Segoe UI","Trebuchet MS",sans-serif; background: radial-gradient(circle at top left,#e7f1ff,transparent 45%), var(--bg); color:var(--ink); }
10
+ .wrap { max-width: 1100px; margin: 28px auto; padding: 0 16px; }
11
+ .card { background: var(--card); border:1px solid var(--line); border-radius:14px; box-shadow: 0 10px 25px rgba(15,23,42,.06); overflow:hidden; }
12
+ h1 { margin:0; font-size: 1.4rem; }
13
+ .head { padding: 14px 16px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--line); }
14
+ .meta { color:var(--muted); font-size:.9rem; }
15
+ table { width:100%; border-collapse: collapse; }
16
+ th,td { padding:10px 12px; border-bottom:1px solid var(--line); text-align:left; font-size:.9rem; }
17
+ th { color:var(--muted); font-weight:600; }
18
+ .s2 { color: var(--ok); font-weight: 700; }
19
+ .s4 { color: var(--warn); font-weight: 700; }
20
+ .s5 { color: var(--err); font-weight: 700; }
21
+ .path { font-family:Consolas, monospace; }
22
+ .empty { padding: 20px; color: var(--muted); }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div class="wrap">
27
+ <div class="card">
28
+ <div class="head">
29
+ <h1>API Logs Dashboard</h1>
30
+ <div class="meta" id="meta">Waiting for traffic...</div>
31
+ </div>
32
+ <div id="content" class="empty">No requests yet.</div>
33
+ </div>
34
+ </div>
35
+ <script>
36
+ function statusClass(code){
37
+ if(code >= 500) return 's5';
38
+ if(code >= 400) return 's4';
39
+ return 's2';
40
+ }
41
+ function ago(ms){
42
+ const d = Date.now() - ms;
43
+ if (d < 1000) return 'now';
44
+ if (d < 60000) return Math.floor(d/1000) + 's ago';
45
+ return Math.floor(d/60000) + 'm ago';
46
+ }
47
+ function render(items){
48
+ const content = document.getElementById('content');
49
+ const meta = document.getElementById('meta');
50
+ if(!items.length){
51
+ content.className = 'empty';
52
+ content.textContent = 'No requests yet.';
53
+ meta.textContent = 'Waiting for traffic...';
54
+ return;
55
+ }
56
+ const rows = items.slice().reverse().map(item => {
57
+ const status = Number(item.status || 0);
58
+ const lat = item.latency_ms == null ? '-' : item.latency_ms + 'ms';
59
+ return '<tr>' +
60
+ '<td>' + (item.method || '-') + '</td>' +
61
+ '<td class="path">' + (item.path || '-') + '</td>' +
62
+ '<td><span class="' + statusClass(status) + '">' + status + '</span></td>' +
63
+ '<td>' + (item.target || '-') + '</td>' +
64
+ '<td>' + lat + '</td>' +
65
+ '<td>' + (item.ts ? ago(item.ts) : '-') + '</td>' +
66
+ '</tr>';
67
+ }).join('');
68
+ content.className = '';
69
+ content.innerHTML = '<table><thead><tr><th>Method</th><th>Path</th><th>Status</th><th>Target</th><th>Latency</th><th>When</th></tr></thead><tbody>' + rows + '</tbody></table>';
70
+ meta.textContent = items.length + ' requests captured';
71
+ }
72
+ async function tick(){
73
+ try{
74
+ const resp = await fetch('/__devlinker/logs', {cache:'no-store'});
75
+ const data = await resp.json();
76
+ render(Array.isArray(data.items) ? data.items : []);
77
+ }catch(_){
78
+ }
79
+ }
80
+ tick();
81
+ setInterval(tick, 1500);
82
+ </script>
83
+ </body>
84
+ </html>"""
@@ -1,4 +1,4 @@
1
- class DevLinkerAI:
1
+ class IssueHints:
2
2
  def analyze_prefix_mismatch(
3
3
  self,
4
4
  api_path: str,
@@ -104,7 +104,10 @@
104
104
  const minTime = 500;
105
105
  const start = Date.now();
106
106
  try {
107
- const resp = await fetch(window.location.href, { headers: { "X-DevLinker-Instant": "1" } });
107
+ const resp = await fetch(window.location.href, {
108
+ headers: { "X-DevLinker-Instant": "1" },
109
+ cache: "no-store",
110
+ });
108
111
  const html = await resp.text();
109
112
  const elapsed = Date.now() - start;
110
113
  const delay = Math.max(0, minTime - elapsed);
@@ -1,5 +1,5 @@
1
1
  import click
2
- from devlinker.detector_ai import DevLinkerAI
2
+ from devlinker.detector_ai import IssueHints
3
3
  from devlinker.logger import print_fix
4
4
  from devlinker.runtime_api import fetch_issues, proxy_base_url
5
5
 
@@ -18,7 +18,7 @@ def doctor():
18
18
 
19
19
  issues = payload.get("items", [])
20
20
  categories = payload.get("categories", {})
21
- ai = DevLinkerAI()
21
+ hints = IssueHints()
22
22
  print("\n🩺 DevLinker Health Dashboard\n" + ("═" * 36))
23
23
  # Grouped status summary
24
24
  if not categories:
@@ -45,6 +45,6 @@ def doctor():
45
45
  issue_text = issue.get("issue", "")
46
46
  if not issue_text:
47
47
  continue
48
- suggestions = ai.analyze_failure(issue_text)
48
+ suggestions = hints.analyze_failure(issue_text)
49
49
  for s in suggestions:
50
50
  print_fix(s)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ _START_MARKER = "# devlinker-managed:start"
6
+ _END_MARKER = "# devlinker-managed:end"
7
+
8
+
9
+ def write_frontend_api_env(proxy_port: int, frontend_dir: str = "frontend") -> bool:
10
+ """Write VITE_API_URL into frontend/.env.local. Returns True when updated."""
11
+ frontend_path = Path(frontend_dir)
12
+ if not frontend_path.is_dir():
13
+ return False
14
+
15
+ env_path = frontend_path / ".env.local"
16
+ managed_block = (
17
+ f"{_START_MARKER}\n"
18
+ f"VITE_API_URL=http://localhost:{proxy_port}\n"
19
+ f"{_END_MARKER}"
20
+ )
21
+
22
+ try:
23
+ existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
24
+ if _START_MARKER in existing and _END_MARKER in existing:
25
+ before, _, tail = existing.partition(_START_MARKER)
26
+ _, _, after = tail.partition(_END_MARKER)
27
+ updated = f"{before}{managed_block}{after}"
28
+ elif existing.strip():
29
+ updated = f"{existing.rstrip()}\n\n{managed_block}\n"
30
+ else:
31
+ updated = f"{managed_block}\n"
32
+
33
+ env_path.write_text(updated, encoding="utf-8")
34
+ return True
35
+ except OSError:
36
+ return False
@@ -1,6 +1,6 @@
1
1
  import click
2
2
  from devlinker.fixer import DevLinkerFixer
3
- from devlinker.runtime_api import fetch_issues, proxy_base_url
3
+ from devlinker.runtime_api import fetch_issues, fetch_status, proxy_base_url
4
4
 
5
5
  @click.command()
6
6
  def fix():
@@ -16,9 +16,15 @@ def fix():
16
16
  return
17
17
 
18
18
  issues = payload.get("items", [])
19
+ try:
20
+ status = fetch_status()
21
+ proxy_port = int(status.get("proxy_port") or 8000)
22
+ except Exception:
23
+ proxy_port = 8000
24
+
19
25
  fixer = DevLinkerFixer()
20
26
  print("\n🔧 Applying fixes...")
21
- results = fixer.apply_fixes(issues)
27
+ results = fixer.apply_fixes(issues, proxy_port=proxy_port)
22
28
  print("\n🔧 Fix Results")
23
29
  for r in results:
24
30
  print(f"✔ {r}")
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from devlinker.env_utils import write_frontend_api_env
4
+
5
+
6
+ class DevLinkerFixer:
7
+ def apply_fixes(self, issues: list, proxy_port: int = 8000) -> list[str]:
8
+ fixes: list[str] = []
9
+ wrote_env = False
10
+
11
+ for issue in issues:
12
+ desc = issue[0] if isinstance(issue, (list, tuple)) else issue.get("issue", "")
13
+ lowered = desc.lower()
14
+
15
+ needs_env = "cors" in lowered or "missing '/api'" in lowered or "missing /api" in lowered
16
+ if not needs_env or wrote_env:
17
+ continue
18
+
19
+ if write_frontend_api_env(proxy_port):
20
+ fixes.append(f"Updated frontend/.env.local with VITE_API_URL=http://localhost:{proxy_port}")
21
+ wrote_env = True
22
+ elif "cors" in lowered:
23
+ fixes.append("Route API calls through /api/* using the DevLinker proxy URL")
24
+ else:
25
+ fixes.append("Replace hardcoded localhost URLs with relative /api paths in frontend code")
26
+
27
+ return fixes