devlinker 1.3.3__tar.gz → 1.3.5__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 (31) hide show
  1. devlinker-1.3.5/LICENSE +21 -0
  2. {devlinker-1.3.3 → devlinker-1.3.5}/PKG-INFO +3 -1
  3. devlinker-1.3.5/devlinker/detection_state.py +66 -0
  4. devlinker-1.3.5/devlinker/global_state.py +5 -0
  5. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/proxy.py +60 -21
  6. devlinker-1.3.5/devlinker/share.py +32 -0
  7. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/tunnel.py +52 -9
  8. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/PKG-INFO +3 -1
  9. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/SOURCES.txt +2 -0
  10. {devlinker-1.3.3 → devlinker-1.3.5}/pyproject.toml +1 -1
  11. devlinker-1.3.3/devlinker/detection_state.py +0 -58
  12. devlinker-1.3.3/devlinker/share.py +0 -54
  13. {devlinker-1.3.3 → devlinker-1.3.5}/README.md +0 -0
  14. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/__init__.py +0 -0
  15. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/config.py +0 -0
  16. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/detector.py +0 -0
  17. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/detector_ai.py +0 -0
  18. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/doctor.py +0 -0
  19. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/fix.py +0 -0
  20. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/fixer.py +0 -0
  21. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/inspect.py +0 -0
  22. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/logger.py +0 -0
  23. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/main.py +0 -0
  24. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/monitor.py +0 -0
  25. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker/runner.py +0 -0
  26. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/dependency_links.txt +0 -0
  27. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/entry_points.txt +0 -0
  28. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/requires.txt +0 -0
  29. {devlinker-1.3.3 → devlinker-1.3.5}/devlinker.egg-info/top_level.txt +0 -0
  30. {devlinker-1.3.3 → devlinker-1.3.5}/setup.cfg +0 -0
  31. {devlinker-1.3.3 → devlinker-1.3.5}/setup.py +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DevLinker Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.3
3
+ Version: 1.3.5
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
8
+ License-File: LICENSE
8
9
  Requires-Dist: click
9
10
  Requires-Dist: docker
10
11
  Requires-Dist: fastapi
@@ -13,6 +14,7 @@ Requires-Dist: pyngrok
13
14
  Requires-Dist: requests
14
15
  Requires-Dist: uvicorn
15
16
  Requires-Dist: websockets
17
+ Dynamic: license-file
16
18
 
17
19
  # Dev Linker
18
20
 
@@ -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,5 @@
1
+ # Stores global state for devlinker proxy and tunnel
2
+ STATE = {
3
+ "proxy_port": 8000,
4
+ "tunnel": None
5
+ }
@@ -23,23 +23,33 @@ _recent_requests = []
23
23
  _recent_lock = threading.Lock()
24
24
 
25
25
  class RequestInspector:
26
- def analyze(self, path, status, target):
26
+ def analyze(self, path, status, target, method=None, response_text=None):
27
27
  warnings = []
28
- # 1. Missing /api prefix
29
- if not path.startswith("/api") and target == "backend":
30
- issue = "Possible missing '/api' prefix"
31
- state.add(issue, level="MEDIUM", category="routing")
32
- warnings.append(issue)
33
- # 2. 404 detection
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)
34
44
  if status == 404:
35
- issue = "Route not found → check backend route"
36
- state.add(issue, level="HIGH", category="routing")
37
- warnings.append(issue)
45
+ issue = f"Route not found → check backend route: {path}"
46
+ if state.add(issue, level="HIGH", category="routing"):
47
+ warnings.append(issue)
38
48
  # 3. Upstream failure
39
49
  if status == 502:
40
- issue = "Backend unreachable"
41
- state.add(issue, level="HIGH", category="network")
42
- warnings.append(issue)
50
+ issue = f"Backend unreachable: {path}"
51
+ if state.add(issue, level="HIGH", category="network"):
52
+ warnings.append(issue)
43
53
  # Log request for inspector
44
54
  with _recent_lock:
45
55
  _recent_requests.append({"path": path, "status": status, "target": target})
@@ -142,12 +152,15 @@ async def _forward_http(request: Request) -> Response:
142
152
  if target_port is None:
143
153
  if request.url.path.startswith("/api"):
144
154
  status = 503
145
- warnings = inspector.analyze(request.url.path, status, "backend")
155
+ warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
146
156
  for w in warnings:
147
- print_warning(w)
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)
148
161
  return PlainTextResponse("Backend is not configured.", status_code=status)
149
162
  status = 503
150
- warnings = inspector.analyze(request.url.path, status, "frontend")
163
+ warnings = inspector.analyze(request.url.path, status, "frontend", method=request.method)
151
164
  for w in warnings:
152
165
  print_warning(w)
153
166
  return PlainTextResponse("Frontend is not configured.", status_code=status)
@@ -169,7 +182,7 @@ async def _forward_http(request: Request) -> Response:
169
182
  )
170
183
  except httpx.RequestError as exc:
171
184
  status = 502
172
- warnings = inspector.analyze(request.url.path, status, "backend")
185
+ warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
173
186
  for w in warnings:
174
187
  print_warning(w)
175
188
  ai_suggestions = ai.analyze_failure(str(exc))
@@ -178,17 +191,43 @@ async def _forward_http(request: Request) -> Response:
178
191
  return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=status)
179
192
 
180
193
  # Analyze response for warnings and fixes
181
- warnings = inspector.analyze(request.url.path, upstream.status_code, "backend")
194
+ warnings = inspector.analyze(
195
+ request.url.path,
196
+ upstream.status_code,
197
+ "backend",
198
+ method=request.method,
199
+ response_text=upstream.text
200
+ )
182
201
  for w in warnings:
183
- print_warning(w)
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)
184
206
  ai_suggestions = ai.analyze_failure(str(upstream.text))
185
207
  for s in ai_suggestions:
186
208
  print_fix(s)
187
209
 
210
+ # Only inject loader for HTML responses, not localhost
211
+ headers = _filter_response_headers(dict(upstream.headers))
212
+ content_type = headers.get("content-type", "")
213
+ is_html = "text/html" in content_type
214
+ is_public = not (request.client and request.client.host in ("127.0.0.1", "localhost"))
215
+ content = upstream.content
216
+ if is_html and is_public:
217
+ try:
218
+ html = content.decode(upstream.encoding or "utf-8", errors="replace")
219
+ # Only inject if </body> exists
220
+ if "</body>" in html:
221
+ with open(__import__('os').path.join(__import__('os').path.dirname(__file__), "devlinker_loader_snippet.html"), encoding="utf-8") as f:
222
+ loader = f.read()
223
+ html = html.replace("</body>", loader + "</body>")
224
+ content = html.encode(upstream.encoding or "utf-8")
225
+ except Exception:
226
+ pass
188
227
  return Response(
189
- content=upstream.content,
228
+ content=content,
190
229
  status_code=upstream.status_code,
191
- headers=_filter_response_headers(dict(upstream.headers)),
230
+ headers=headers,
192
231
  )
193
232
 
194
233
 
@@ -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,5 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
+ def stop_tunnel():
4
+ """Stop all active tunnels (Cloudflare/ngrok)."""
5
+ # Stop ngrok tunnels
6
+ try:
7
+ for tunnel in ngrok.get_tunnels():
8
+ public_url = getattr(tunnel, "public_url", None)
9
+ if public_url is not None:
10
+ ngrok.disconnect(public_url)
11
+ except Exception:
12
+ pass
13
+ # Stop cloudflared processes
14
+ global _CLOUDFLARED_PROCESSES
15
+ for proc in _CLOUDFLARED_PROCESSES:
16
+ try:
17
+ proc.terminate()
18
+ except Exception:
19
+ pass
20
+ _CLOUDFLARED_PROCESSES.clear()
21
+
3
22
  import re
4
23
  import shutil
5
24
  import subprocess
@@ -42,7 +61,21 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
42
61
  stdout, _ = process.communicate(timeout=startup_timeout)
43
62
  output = stdout or ""
44
63
  except TimeoutExpired as exc:
45
- output = (exc.stdout or "") + (exc.stderr or "")
64
+ # exc.stdout and exc.stderr may be str, bytes, bytearray, or memoryview; convert to str
65
+ def to_str(val):
66
+ if isinstance(val, str):
67
+ return val
68
+ if isinstance(val, (bytes, bytearray)):
69
+ return val.decode(errors="replace")
70
+ if isinstance(val, memoryview):
71
+ return val.tobytes().decode(errors="replace")
72
+ return str(val) if val is not None else ""
73
+ exc_stdout = to_str(exc.stdout)
74
+ exc_stderr = to_str(exc.stderr)
75
+ output = exc_stdout + exc_stderr
76
+
77
+ if not isinstance(output, str):
78
+ output = str(output)
46
79
 
47
80
  url = _extract_trycloudflare_url(output)
48
81
  if url:
@@ -55,22 +88,30 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
55
88
 
56
89
  def _disconnect_existing_tunnels() -> None:
57
90
  for tunnel in ngrok.get_tunnels():
58
- ngrok.disconnect(tunnel.public_url)
91
+ public_url = getattr(tunnel, "public_url", None)
92
+ if public_url is not None:
93
+ ngrok.disconnect(public_url)
59
94
 
60
95
 
61
- def _start_ngrok_tunnel(proxy_port: int) -> str:
96
+ def _start_ngrok_tunnel(proxy_port: int) -> str | None:
62
97
  try:
63
- tunnel = ngrok.connect(proxy_port)
64
- return tunnel.public_url
98
+ tunnel = ngrok.connect(str(proxy_port))
99
+ public_url = getattr(tunnel, "public_url", None)
100
+ if public_url is not None:
101
+ return public_url
102
+ return None
65
103
  except PyngrokError as exc:
66
104
  message = str(exc).lower()
67
105
 
68
106
  # If a prior endpoint is still active, disconnect and retry once.
69
- if "already online" in message or "endpoint" in message and "online" in message:
107
+ if "already online" in message or ("endpoint" in message and "online" in message):
70
108
  try:
71
109
  _disconnect_existing_tunnels()
72
- tunnel = ngrok.connect(proxy_port)
73
- return tunnel.public_url
110
+ tunnel = ngrok.connect(str(proxy_port))
111
+ public_url = getattr(tunnel, "public_url", None)
112
+ if public_url is not None:
113
+ return public_url
114
+ return None
74
115
  except PyngrokError as retry_exc:
75
116
  raise RuntimeError(
76
117
  f"Failed to start ngrok tunnel after disconnect retry: {retry_exc}"
@@ -97,7 +138,9 @@ def start_tunnel(proxy_port: int = 8000) -> tuple[str, str]:
97
138
 
98
139
  try:
99
140
  ngrok_url = _start_ngrok_tunnel(proxy_port)
100
- return "ngrok", ngrok_url
141
+ if ngrok_url is not None:
142
+ return "ngrok", ngrok_url
143
+ raise RuntimeError("Ngrok tunnel did not return a public URL.")
101
144
  except RuntimeError as ngrok_error:
102
145
  raise RuntimeError(
103
146
  "No tunnel available.\n"
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.3
3
+ Version: 1.3.5
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
8
+ License-File: LICENSE
8
9
  Requires-Dist: click
9
10
  Requires-Dist: docker
10
11
  Requires-Dist: fastapi
@@ -13,6 +14,7 @@ Requires-Dist: pyngrok
13
14
  Requires-Dist: requests
14
15
  Requires-Dist: uvicorn
15
16
  Requires-Dist: websockets
17
+ Dynamic: license-file
16
18
 
17
19
  # Dev Linker
18
20
 
@@ -1,3 +1,4 @@
1
+ LICENSE
1
2
  README.md
2
3
  pyproject.toml
3
4
  setup.py
@@ -9,6 +10,7 @@ devlinker/detector_ai.py
9
10
  devlinker/doctor.py
10
11
  devlinker/fix.py
11
12
  devlinker/fixer.py
13
+ devlinker/global_state.py
12
14
  devlinker/inspect.py
13
15
  devlinker/logger.py
14
16
  devlinker/main.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlinker"
7
- version = "1.3.3"
7
+ version = "1.3.5"
8
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" }
@@ -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,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