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.
- {devlinker-1.4.6/devlinker.egg-info → devlinker-1.5.0}/PKG-INFO +15 -6
- devlinker-1.5.0/devlinker/dashboard_html.py +84 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detector_ai.py +1 -1
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/devlinker_loader_instant.html +4 -1
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/doctor.py +3 -3
- devlinker-1.5.0/devlinker/env_utils.py +36 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/fix.py +8 -2
- devlinker-1.5.0/devlinker/fixer.py +27 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/main.py +42 -212
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/proxy.py +33 -121
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/tunnel.py +31 -25
- {devlinker-1.4.6 → devlinker-1.5.0/devlinker.egg-info}/PKG-INFO +15 -6
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/SOURCES.txt +4 -1
- devlinker-1.5.0/devlinker.egg-info/requires.txt +25 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/pyproject.toml +14 -6
- devlinker-1.5.0/tests/test_proxy_runtime.py +105 -0
- devlinker-1.4.6/devlinker/fixer.py +0 -19
- devlinker-1.4.6/devlinker.egg-info/requires.txt +0 -10
- {devlinker-1.4.6 → devlinker-1.5.0}/LICENSE +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/MANIFEST.in +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/README.md +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/__init__.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/config.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detection_state.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/detector.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/devlinker_loader_snippet.html +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/inspect.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/logger.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/monitor.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/runner.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker/runtime_api.py +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.4.6 → devlinker-1.5.0}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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:
|
|
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>"""
|
|
@@ -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, {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|