devlinker 1.3.1__tar.gz → 1.3.4__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.3.1 → devlinker-1.3.4}/PKG-INFO +2 -2
- devlinker-1.3.4/devlinker/config.py +8 -0
- devlinker-1.3.4/devlinker/detection_state.py +66 -0
- devlinker-1.3.4/devlinker/doctor.py +27 -0
- devlinker-1.3.4/devlinker/global_state.py +5 -0
- devlinker-1.3.4/devlinker/inspect.py +14 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/main.py +22 -0
- devlinker-1.3.4/devlinker/monitor.py +19 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/proxy.py +51 -19
- devlinker-1.3.4/devlinker/share.py +32 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/tunnel.py +16 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/PKG-INFO +2 -2
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/SOURCES.txt +4 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/pyproject.toml +2 -2
- devlinker-1.3.1/devlinker/detection_state.py +0 -58
- devlinker-1.3.1/devlinker/doctor.py +0 -16
- devlinker-1.3.1/devlinker/share.py +0 -54
- {devlinker-1.3.1 → devlinker-1.3.4}/README.md +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/__init__.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/detector.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/detector_ai.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/fix.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/fixer.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/logger.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/runner.py +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/setup.cfg +0 -0
- {devlinker-1.3.1 → devlinker-1.3.4}/setup.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.3.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.3.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
6
|
Requires-Python: >=3.7
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
class DetectionState:
|
|
3
|
+
def __init__(self):
|
|
4
|
+
self.issues = []
|
|
5
|
+
self.counts = {}
|
|
6
|
+
self.levels = {}
|
|
7
|
+
self.categories = {}
|
|
8
|
+
|
|
9
|
+
def add(self, issue, level="MEDIUM", category="general"):
|
|
10
|
+
key = issue.strip().lower()
|
|
11
|
+
if key in self.counts:
|
|
12
|
+
self.counts[key] += 1
|
|
13
|
+
return False # already shown
|
|
14
|
+
else:
|
|
15
|
+
self.counts[key] = 1
|
|
16
|
+
self.issues.append({
|
|
17
|
+
"issue": issue,
|
|
18
|
+
"level": level,
|
|
19
|
+
"category": category
|
|
20
|
+
})
|
|
21
|
+
return True # first time
|
|
22
|
+
|
|
23
|
+
def get_count(self, issue):
|
|
24
|
+
key = issue.strip().lower()
|
|
25
|
+
return self.counts.get(key, 0)
|
|
26
|
+
|
|
27
|
+
def should_print(self, issue):
|
|
28
|
+
key = issue.strip().lower()
|
|
29
|
+
return self.counts.get(key, 0) == 1
|
|
30
|
+
|
|
31
|
+
def get_issues(self):
|
|
32
|
+
return [
|
|
33
|
+
(i["issue"], i["level"], self.get_count(i["issue"]), i["category"])
|
|
34
|
+
for i in self.issues
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def summary(self):
|
|
38
|
+
summary = {}
|
|
39
|
+
for i in self.issues:
|
|
40
|
+
level = i["level"]
|
|
41
|
+
summary.setdefault(level, set()).add(i["issue"])
|
|
42
|
+
return summary
|
|
43
|
+
|
|
44
|
+
def _get_category(self, issue):
|
|
45
|
+
for i in self.issues:
|
|
46
|
+
if i["issue"] == issue:
|
|
47
|
+
return i["category"]
|
|
48
|
+
return "general"
|
|
49
|
+
|
|
50
|
+
def report(self):
|
|
51
|
+
print("\n🩺 DevLinker Doctor Report\n────────────────────────")
|
|
52
|
+
# Group by category
|
|
53
|
+
for i in self.issues:
|
|
54
|
+
issue = i["issue"]
|
|
55
|
+
level = i["level"]
|
|
56
|
+
category = i["category"]
|
|
57
|
+
count = self.get_count(issue)
|
|
58
|
+
if level == "HIGH":
|
|
59
|
+
print(f"❌ {issue} (x{count})")
|
|
60
|
+
elif level == "MEDIUM":
|
|
61
|
+
print(f"⚠️ {issue} (x{count})")
|
|
62
|
+
else:
|
|
63
|
+
print(f"💡 {issue} (x{count})")
|
|
64
|
+
|
|
65
|
+
# Singleton instance
|
|
66
|
+
state = DetectionState()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.detection_state import state
|
|
3
|
+
from devlinker.detector_ai import DevLinkerAI
|
|
4
|
+
from devlinker.logger import print_fix
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
def doctor():
|
|
8
|
+
"""Run DevLinker diagnostics and print a health dashboard."""
|
|
9
|
+
ai = DevLinkerAI()
|
|
10
|
+
print("\n🩺 DevLinker Health Dashboard\n" + ("═" * 36))
|
|
11
|
+
# Grouped status summary
|
|
12
|
+
categories = state.categories
|
|
13
|
+
for category, issues in categories.items():
|
|
14
|
+
if not issues:
|
|
15
|
+
status = "✅"
|
|
16
|
+
else:
|
|
17
|
+
high = any(state.levels.get(issue, "MEDIUM") == "HIGH" for issue in issues)
|
|
18
|
+
warn = any(state.levels.get(issue, "MEDIUM") == "MEDIUM" for issue in issues)
|
|
19
|
+
status = "⚠️" if high or warn else "✅"
|
|
20
|
+
print(f"{category.title():<10}: {status}")
|
|
21
|
+
print("\nDetails:")
|
|
22
|
+
state.report()
|
|
23
|
+
print("\nFix Suggestions:")
|
|
24
|
+
for issue, level, count, category in state.get_issues():
|
|
25
|
+
suggestions = ai.analyze_failure(issue)
|
|
26
|
+
for s in suggestions:
|
|
27
|
+
print_fix(s)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.proxy import _recent_requests
|
|
3
|
+
|
|
4
|
+
@click.command()
|
|
5
|
+
def inspect():
|
|
6
|
+
"""Show recent API calls and statuses."""
|
|
7
|
+
click.secho("\n🔍 Recent API Calls (last 50):\n" + ("═" * 36), fg="cyan", bold=True)
|
|
8
|
+
if not _recent_requests:
|
|
9
|
+
click.secho("No API calls recorded yet.", fg="yellow")
|
|
10
|
+
return
|
|
11
|
+
for req in _recent_requests[-50:]:
|
|
12
|
+
status = req["status"]
|
|
13
|
+
emoji = "✅" if status < 400 else ("⚠️" if status < 500 else "❌")
|
|
14
|
+
click.secho(f"{emoji} {req['target']:<8} {req['path']:<30} → {status}", fg="white")
|
|
@@ -13,7 +13,11 @@ from .runner import detect_backend_port, start_servers
|
|
|
13
13
|
from .tunnel import start_tunnel
|
|
14
14
|
from .doctor import doctor
|
|
15
15
|
from .fix import fix
|
|
16
|
+
|
|
16
17
|
from .share import share, unshare
|
|
18
|
+
from .config import load_config
|
|
19
|
+
from .inspect import inspect
|
|
20
|
+
from .monitor import monitor
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def _is_port_in_use(port: int) -> bool:
|
|
@@ -147,6 +151,7 @@ def _wait_for_readiness(
|
|
|
147
151
|
help="Show WLAN sharing URL for devices on the same network.",
|
|
148
152
|
)
|
|
149
153
|
@click.option("--debug", is_flag=True, hidden=True, help="Enable debug logging.")
|
|
154
|
+
|
|
150
155
|
def cli(
|
|
151
156
|
frontend: int | None,
|
|
152
157
|
backend_port_override: int | None,
|
|
@@ -158,6 +163,20 @@ def cli(
|
|
|
158
163
|
lan_enabled: bool,
|
|
159
164
|
debug: bool,
|
|
160
165
|
) -> None:
|
|
166
|
+
# Load config file if present
|
|
167
|
+
config = load_config()
|
|
168
|
+
# Use config values as defaults if CLI args are not set
|
|
169
|
+
if frontend is None:
|
|
170
|
+
frontend = config.get("frontend")
|
|
171
|
+
if backend_port_override is None:
|
|
172
|
+
backend_port_override = config.get("backend")
|
|
173
|
+
if proxy_port == 8000 and config.get("proxy_port"):
|
|
174
|
+
proxy_port = config["proxy_port"]
|
|
175
|
+
if not url and config.get("tunnel") is True:
|
|
176
|
+
url = True
|
|
177
|
+
if config.get("api_prefix"):
|
|
178
|
+
# Optionally pass api_prefix to proxy if needed in future
|
|
179
|
+
pass
|
|
161
180
|
|
|
162
181
|
started = time.perf_counter()
|
|
163
182
|
banner = "\n" + ("═" * 36) + f"\n⚡ Dev Linker v{__version__} ⚡\n" + ("═" * 36)
|
|
@@ -272,11 +291,14 @@ def main():
|
|
|
272
291
|
pass
|
|
273
292
|
|
|
274
293
|
|
|
294
|
+
|
|
275
295
|
main.add_command(cli)
|
|
276
296
|
main.add_command(doctor)
|
|
277
297
|
main.add_command(fix)
|
|
278
298
|
main.add_command(share)
|
|
279
299
|
main.add_command(unshare)
|
|
300
|
+
main.add_command(inspect)
|
|
301
|
+
main.add_command(monitor)
|
|
280
302
|
|
|
281
303
|
if __name__ == "__main__":
|
|
282
304
|
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# API Monitor CLI command for health/status dashboard
|
|
2
|
+
import click
|
|
3
|
+
from devlinker.detection_state import state
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
def monitor():
|
|
7
|
+
"""Show API health/status dashboard."""
|
|
8
|
+
click.secho("\n📡 API Monitor Dashboard\n" + ("═" * 36), fg="cyan", bold=True)
|
|
9
|
+
categories = state.categories
|
|
10
|
+
for category, issues in categories.items():
|
|
11
|
+
if not issues:
|
|
12
|
+
status = "✅"
|
|
13
|
+
else:
|
|
14
|
+
high = any(state.levels.get(issue, "MEDIUM") == "HIGH" for issue in issues)
|
|
15
|
+
warn = any(state.levels.get(issue, "MEDIUM") == "MEDIUM" for issue in issues)
|
|
16
|
+
status = "⚠️" if high or warn else "✅"
|
|
17
|
+
click.secho(f"{category.title():<10}: {status}", fg="white")
|
|
18
|
+
click.secho("\nDetails:", fg="magenta")
|
|
19
|
+
state.report()
|
|
@@ -16,25 +16,45 @@ from websockets.exceptions import ConnectionClosed
|
|
|
16
16
|
app = FastAPI()
|
|
17
17
|
|
|
18
18
|
# --- RequestInspector: Real-time request analyzer ---
|
|
19
|
+
|
|
19
20
|
from devlinker.detection_state import state
|
|
21
|
+
import threading
|
|
22
|
+
_recent_requests = []
|
|
23
|
+
_recent_lock = threading.Lock()
|
|
24
|
+
|
|
20
25
|
class RequestInspector:
|
|
21
|
-
def analyze(self, path, status, target):
|
|
26
|
+
def analyze(self, path, status, target, method=None, response_text=None):
|
|
22
27
|
warnings = []
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
warnings
|
|
28
|
-
|
|
28
|
+
# Ignore static files and paths
|
|
29
|
+
static_exts = [".js", ".css", ".ico", ".png", ".jpg", ".jpeg", ".svg", ".woff", ".woff2", ".ttf", ".map"]
|
|
30
|
+
IGNORE_PATHS = ["/@vite", "/assets", "/favicon.ico", "/src", "/node_modules"]
|
|
31
|
+
if any(path.endswith(ext) for ext in static_exts):
|
|
32
|
+
return warnings
|
|
33
|
+
if any(path.startswith(p) for p in IGNORE_PATHS):
|
|
34
|
+
return warnings
|
|
35
|
+
# Only warn for missing /api prefix if status is 404, method is POST/PUT/DELETE, and not static/ignored
|
|
36
|
+
if status == 404 and method and method.upper() in ["POST", "PUT", "DELETE"]:
|
|
37
|
+
if not path.startswith("/api"):
|
|
38
|
+
# Optionally, check response_text for "Not Found"
|
|
39
|
+
if response_text is None or "not found" in response_text.lower():
|
|
40
|
+
issue = f"Possible missing '/api' prefix on {path} [{method}]"
|
|
41
|
+
if state.add(issue, level="MEDIUM", category="routing"):
|
|
42
|
+
warnings.append(issue)
|
|
43
|
+
# 2. 404 detection (general)
|
|
29
44
|
if status == 404:
|
|
30
|
-
issue = "Route not found → check backend route"
|
|
31
|
-
state.add(issue, level="HIGH", category="routing")
|
|
32
|
-
|
|
45
|
+
issue = f"Route not found → check backend route: {path}"
|
|
46
|
+
if state.add(issue, level="HIGH", category="routing"):
|
|
47
|
+
warnings.append(issue)
|
|
33
48
|
# 3. Upstream failure
|
|
34
49
|
if status == 502:
|
|
35
|
-
issue = "Backend unreachable"
|
|
36
|
-
state.add(issue, level="HIGH", category="network")
|
|
37
|
-
|
|
50
|
+
issue = f"Backend unreachable: {path}"
|
|
51
|
+
if state.add(issue, level="HIGH", category="network"):
|
|
52
|
+
warnings.append(issue)
|
|
53
|
+
# Log request for inspector
|
|
54
|
+
with _recent_lock:
|
|
55
|
+
_recent_requests.append({"path": path, "status": status, "target": target})
|
|
56
|
+
if len(_recent_requests) > 50:
|
|
57
|
+
_recent_requests.pop(0)
|
|
38
58
|
return warnings
|
|
39
59
|
|
|
40
60
|
FRONTEND: Optional[int] = None
|
|
@@ -132,12 +152,15 @@ async def _forward_http(request: Request) -> Response:
|
|
|
132
152
|
if target_port is None:
|
|
133
153
|
if request.url.path.startswith("/api"):
|
|
134
154
|
status = 503
|
|
135
|
-
warnings = inspector.analyze(request.url.path, status, "backend")
|
|
155
|
+
warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
|
|
136
156
|
for w in warnings:
|
|
137
|
-
|
|
157
|
+
if "/api" in w:
|
|
158
|
+
print_warning(f"API routing issue detected\n👉 {request.url.path} returned 404\n👉 Try: /api{request.url.path}")
|
|
159
|
+
else:
|
|
160
|
+
print_warning(w)
|
|
138
161
|
return PlainTextResponse("Backend is not configured.", status_code=status)
|
|
139
162
|
status = 503
|
|
140
|
-
warnings = inspector.analyze(request.url.path, status, "frontend")
|
|
163
|
+
warnings = inspector.analyze(request.url.path, status, "frontend", method=request.method)
|
|
141
164
|
for w in warnings:
|
|
142
165
|
print_warning(w)
|
|
143
166
|
return PlainTextResponse("Frontend is not configured.", status_code=status)
|
|
@@ -159,7 +182,7 @@ async def _forward_http(request: Request) -> Response:
|
|
|
159
182
|
)
|
|
160
183
|
except httpx.RequestError as exc:
|
|
161
184
|
status = 502
|
|
162
|
-
warnings = inspector.analyze(request.url.path, status, "backend")
|
|
185
|
+
warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
|
|
163
186
|
for w in warnings:
|
|
164
187
|
print_warning(w)
|
|
165
188
|
ai_suggestions = ai.analyze_failure(str(exc))
|
|
@@ -168,9 +191,18 @@ async def _forward_http(request: Request) -> Response:
|
|
|
168
191
|
return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=status)
|
|
169
192
|
|
|
170
193
|
# Analyze response for warnings and fixes
|
|
171
|
-
warnings = inspector.analyze(
|
|
194
|
+
warnings = inspector.analyze(
|
|
195
|
+
request.url.path,
|
|
196
|
+
upstream.status_code,
|
|
197
|
+
"backend",
|
|
198
|
+
method=request.method,
|
|
199
|
+
response_text=upstream.text
|
|
200
|
+
)
|
|
172
201
|
for w in warnings:
|
|
173
|
-
|
|
202
|
+
if "/api" in w:
|
|
203
|
+
print_warning(f"API routing issue detected\n👉 {request.url.path} returned 404\n👉 Try: /api{request.url.path}")
|
|
204
|
+
else:
|
|
205
|
+
print_warning(w)
|
|
174
206
|
ai_suggestions = ai.analyze_failure(str(upstream.text))
|
|
175
207
|
for s in ai_suggestions:
|
|
176
208
|
print_fix(s)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import click
|
|
3
|
+
from devlinker.global_state import STATE
|
|
4
|
+
from devlinker.tunnel import start_tunnel
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
def share():
|
|
8
|
+
"""Enable public tunnel at runtime (no restart)."""
|
|
9
|
+
if STATE["tunnel"]:
|
|
10
|
+
click.secho("⚠️ Already shared", fg="yellow")
|
|
11
|
+
return
|
|
12
|
+
try:
|
|
13
|
+
provider, url = start_tunnel(STATE["proxy_port"])
|
|
14
|
+
STATE["tunnel"] = url
|
|
15
|
+
click.secho("\n🌍 Public Sharing Enabled\n" + ("─" * 24), fg="green", bold=True)
|
|
16
|
+
click.secho("✔ Tunnel connected", fg="green")
|
|
17
|
+
click.secho(f"\nPublic URL:\n{url}\n", fg="cyan", bold=True)
|
|
18
|
+
click.secho("📤 Share this link with your team", fg="magenta")
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
click.secho(f"[WARN] Tunnel failed: {exc}", fg="red")
|
|
21
|
+
click.secho("[INFO] Next step: install cloudflared or configure ngrok auth.", fg="yellow")
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
def unshare():
|
|
25
|
+
"""Disable public tunnel at runtime (no restart)."""
|
|
26
|
+
if not STATE["tunnel"]:
|
|
27
|
+
click.secho("⚠️ No active tunnel", fg="yellow")
|
|
28
|
+
return
|
|
29
|
+
from devlinker.tunnel import stop_tunnel
|
|
30
|
+
stop_tunnel()
|
|
31
|
+
STATE["tunnel"] = None
|
|
32
|
+
click.secho("🛑 Sharing stopped", fg="red", bold=True)
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
def stop_tunnel():
|
|
2
|
+
"""Stop all active tunnels (Cloudflare/ngrok)."""
|
|
3
|
+
# Stop ngrok tunnels
|
|
4
|
+
try:
|
|
5
|
+
for tunnel in ngrok.get_tunnels():
|
|
6
|
+
ngrok.disconnect(tunnel.public_url)
|
|
7
|
+
except Exception:
|
|
8
|
+
pass
|
|
9
|
+
# Stop cloudflared processes
|
|
10
|
+
global _CLOUDFLARED_PROCESSES
|
|
11
|
+
for proc in _CLOUDFLARED_PROCESSES:
|
|
12
|
+
try:
|
|
13
|
+
proc.terminate()
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
_CLOUDFLARED_PROCESSES.clear()
|
|
1
17
|
from __future__ import annotations
|
|
2
18
|
|
|
3
19
|
import re
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.3.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.3.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
6
|
Requires-Python: >=3.7
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -2,14 +2,18 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
setup.py
|
|
4
4
|
devlinker/__init__.py
|
|
5
|
+
devlinker/config.py
|
|
5
6
|
devlinker/detection_state.py
|
|
6
7
|
devlinker/detector.py
|
|
7
8
|
devlinker/detector_ai.py
|
|
8
9
|
devlinker/doctor.py
|
|
9
10
|
devlinker/fix.py
|
|
10
11
|
devlinker/fixer.py
|
|
12
|
+
devlinker/global_state.py
|
|
13
|
+
devlinker/inspect.py
|
|
11
14
|
devlinker/logger.py
|
|
12
15
|
devlinker/main.py
|
|
16
|
+
devlinker/monitor.py
|
|
13
17
|
devlinker/proxy.py
|
|
14
18
|
devlinker/runner.py
|
|
15
19
|
devlinker/share.py
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devlinker"
|
|
7
|
-
version = "1.3.
|
|
8
|
-
description = "
|
|
7
|
+
version = "1.3.4"
|
|
8
|
+
description = "A lightweight proxy that combines your frontend and backend into one link for easy development and sharing."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Mani", email = "mani1028@users.noreply.github.com" }
|
|
11
11
|
]
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
class DetectionState:
|
|
3
|
-
def __init__(self):
|
|
4
|
-
self.issues = []
|
|
5
|
-
self.counts = {}
|
|
6
|
-
self.levels = {}
|
|
7
|
-
self.categories = {}
|
|
8
|
-
|
|
9
|
-
def add(self, issue, level="MEDIUM", category="general"):
|
|
10
|
-
self.issues.append(issue)
|
|
11
|
-
self.counts[issue] = self.counts.get(issue, 0) + 1
|
|
12
|
-
self.levels[issue] = level
|
|
13
|
-
self.categories.setdefault(category, []).append(issue)
|
|
14
|
-
|
|
15
|
-
def should_print(self, issue):
|
|
16
|
-
return self.counts.get(issue, 0) == 1
|
|
17
|
-
|
|
18
|
-
def get_issues(self):
|
|
19
|
-
return [(issue, self.levels.get(issue, "MEDIUM"), self.counts[issue], self._get_category(issue)) for issue in self.issues]
|
|
20
|
-
|
|
21
|
-
def summary(self):
|
|
22
|
-
summary = {}
|
|
23
|
-
for issue in self.issues:
|
|
24
|
-
level = self.levels.get(issue, "MEDIUM")
|
|
25
|
-
summary.setdefault(level, set()).add(issue)
|
|
26
|
-
return summary
|
|
27
|
-
|
|
28
|
-
def _get_category(self, issue):
|
|
29
|
-
for cat, issues in self.categories.items():
|
|
30
|
-
if issue in issues:
|
|
31
|
-
return cat
|
|
32
|
-
return "general"
|
|
33
|
-
|
|
34
|
-
def report(self):
|
|
35
|
-
print("\n🩺 DevLinker Doctor Report\n────────────────────────")
|
|
36
|
-
# Group by category
|
|
37
|
-
for category, issues in self.categories.items():
|
|
38
|
-
if not issues:
|
|
39
|
-
continue
|
|
40
|
-
if category == "network":
|
|
41
|
-
print("\n🌐 Network Issues")
|
|
42
|
-
elif category == "routing":
|
|
43
|
-
print("\n🔀 Routing Issues")
|
|
44
|
-
elif category == "cors":
|
|
45
|
-
print("\n🔐 CORS Issues")
|
|
46
|
-
else:
|
|
47
|
-
print(f"\n{category.title()} Issues")
|
|
48
|
-
for issue in set(issues):
|
|
49
|
-
level = self.levels.get(issue, "MEDIUM")
|
|
50
|
-
if level == "HIGH":
|
|
51
|
-
print(f"❌ {issue}")
|
|
52
|
-
elif level == "MEDIUM":
|
|
53
|
-
print(f"⚠️ {issue}")
|
|
54
|
-
else:
|
|
55
|
-
print(f"💡 {issue}")
|
|
56
|
-
|
|
57
|
-
# Singleton instance
|
|
58
|
-
state = DetectionState()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
from devlinker.detection_state import state
|
|
3
|
-
from devlinker.detector_ai import DevLinkerAI
|
|
4
|
-
from devlinker.logger import print_fix
|
|
5
|
-
|
|
6
|
-
@click.command()
|
|
7
|
-
def doctor():
|
|
8
|
-
"""Run DevLinker diagnostics and print a doctor report."""
|
|
9
|
-
ai = DevLinkerAI()
|
|
10
|
-
# Doctor now uses real-time, categorized issues from the global state
|
|
11
|
-
state.report()
|
|
12
|
-
print("\nFix Suggestions:")
|
|
13
|
-
for issue, level, count, category in state.get_issues():
|
|
14
|
-
suggestions = ai.analyze_failure(issue)
|
|
15
|
-
for s in suggestions:
|
|
16
|
-
print_fix(s)
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
from devlinker.tunnel import start_tunnel
|
|
3
|
-
|
|
4
|
-
# Global tunnel state
|
|
5
|
-
_tunnel_info = {
|
|
6
|
-
"provider": None,
|
|
7
|
-
"public_url": None,
|
|
8
|
-
"active": False,
|
|
9
|
-
"proxy_port": None,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
@click.command()
|
|
13
|
-
def share():
|
|
14
|
-
"""Enable public tunnel at runtime (no restart)."""
|
|
15
|
-
from devlinker.main import _select_proxy_port
|
|
16
|
-
import sys
|
|
17
|
-
proxy_port = 8000
|
|
18
|
-
banner = "\n" + ("═" * 36) + "\n🌍 DevLinker Share Mode\n" + ("═" * 36)
|
|
19
|
-
if _tunnel_info["active"]:
|
|
20
|
-
click.secho(f"{banner}\n\n🔗 Tunnel already active:", fg="yellow", bold=True)
|
|
21
|
-
click.secho(f" {_tunnel_info['public_url']}\n", fg="cyan", bold=True)
|
|
22
|
-
return
|
|
23
|
-
try:
|
|
24
|
-
click.secho(f"{banner}\n\n🌍 Enabling public tunnel...", fg="green", bold=True)
|
|
25
|
-
provider, public_url = start_tunnel(proxy_port)
|
|
26
|
-
_tunnel_info["provider"] = provider
|
|
27
|
-
_tunnel_info["public_url"] = public_url
|
|
28
|
-
_tunnel_info["active"] = True
|
|
29
|
-
_tunnel_info["proxy_port"] = proxy_port
|
|
30
|
-
click.secho(f"\n[OK] Tunnel provider: {provider}", fg="blue")
|
|
31
|
-
click.secho(f"[OK] Public URL:", fg="blue")
|
|
32
|
-
click.secho(f" {public_url}\n", fg="cyan", bold=True)
|
|
33
|
-
click.secho("Tip: Press Ctrl+Click to open link", fg="magenta")
|
|
34
|
-
click.secho("[INFO] Share this link with collaborators.\n", fg="magenta")
|
|
35
|
-
except Exception as exc:
|
|
36
|
-
click.secho(f"[WARN] Tunnel failed: {exc}", fg="red")
|
|
37
|
-
click.secho("[INFO] Next step: install cloudflared or configure ngrok auth.", fg="yellow")
|
|
38
|
-
sys.exit(1)
|
|
39
|
-
|
|
40
|
-
@click.command()
|
|
41
|
-
def unshare():
|
|
42
|
-
"""Disable public tunnel at runtime (no restart)."""
|
|
43
|
-
banner = "\n" + ("═" * 36) + "\n🛑 DevLinker Unshare Mode\n" + ("═" * 36)
|
|
44
|
-
if not _tunnel_info["active"]:
|
|
45
|
-
click.secho(f"{banner}\n\nNo tunnel is currently active.\n", fg="yellow", bold=True)
|
|
46
|
-
return
|
|
47
|
-
# In a real implementation, stop the tunnel process here
|
|
48
|
-
click.secho(f"{banner}\n\n🛑 Disabling tunnel:", fg="red", bold=True)
|
|
49
|
-
click.secho(f" {_tunnel_info['public_url']}\n", fg="cyan", bold=True)
|
|
50
|
-
_tunnel_info["provider"] = None
|
|
51
|
-
_tunnel_info["public_url"] = None
|
|
52
|
-
_tunnel_info["active"] = False
|
|
53
|
-
_tunnel_info["proxy_port"] = None
|
|
54
|
-
click.secho("[OK] Tunnel disabled.\n", fg="green", bold=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|