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.
Files changed (31) hide show
  1. {devlinker-1.3.1 → devlinker-1.3.4}/PKG-INFO +2 -2
  2. devlinker-1.3.4/devlinker/config.py +8 -0
  3. devlinker-1.3.4/devlinker/detection_state.py +66 -0
  4. devlinker-1.3.4/devlinker/doctor.py +27 -0
  5. devlinker-1.3.4/devlinker/global_state.py +5 -0
  6. devlinker-1.3.4/devlinker/inspect.py +14 -0
  7. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/main.py +22 -0
  8. devlinker-1.3.4/devlinker/monitor.py +19 -0
  9. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/proxy.py +51 -19
  10. devlinker-1.3.4/devlinker/share.py +32 -0
  11. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/tunnel.py +16 -0
  12. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/PKG-INFO +2 -2
  13. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/SOURCES.txt +4 -0
  14. {devlinker-1.3.1 → devlinker-1.3.4}/pyproject.toml +2 -2
  15. devlinker-1.3.1/devlinker/detection_state.py +0 -58
  16. devlinker-1.3.1/devlinker/doctor.py +0 -16
  17. devlinker-1.3.1/devlinker/share.py +0 -54
  18. {devlinker-1.3.1 → devlinker-1.3.4}/README.md +0 -0
  19. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/__init__.py +0 -0
  20. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/detector.py +0 -0
  21. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/detector_ai.py +0 -0
  22. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/fix.py +0 -0
  23. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/fixer.py +0 -0
  24. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/logger.py +0 -0
  25. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker/runner.py +0 -0
  26. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/dependency_links.txt +0 -0
  27. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/entry_points.txt +0 -0
  28. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/requires.txt +0 -0
  29. {devlinker-1.3.1 → devlinker-1.3.4}/devlinker.egg-info/top_level.txt +0 -0
  30. {devlinker-1.3.1 → devlinker-1.3.4}/setup.cfg +0 -0
  31. {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.1
4
- Summary: AI-powered linking and automation tool
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,8 @@
1
+ import yaml
2
+ import os
3
+
4
+ def load_config(config_path="devlinker.yaml"):
5
+ if not os.path.exists(config_path):
6
+ return {}
7
+ with open(config_path, "r") as f:
8
+ return yaml.safe_load(f) or {}
@@ -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,5 @@
1
+ # Stores global state for devlinker proxy and tunnel
2
+ STATE = {
3
+ "proxy_port": 8000,
4
+ "tunnel": None
5
+ }
@@ -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
- # 1. Missing /api prefix
24
- if not path.startswith("/api") and target == "backend":
25
- issue = "Possible missing '/api' prefix"
26
- state.add(issue, level="MEDIUM", category="routing")
27
- warnings.append(issue)
28
- # 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)
29
44
  if status == 404:
30
- issue = "Route not found → check backend route"
31
- state.add(issue, level="HIGH", category="routing")
32
- 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)
33
48
  # 3. Upstream failure
34
49
  if status == 502:
35
- issue = "Backend unreachable"
36
- state.add(issue, level="HIGH", category="network")
37
- warnings.append(issue)
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
- 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)
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(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
+ )
172
201
  for w in warnings:
173
- 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)
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.1
4
- Summary: AI-powered linking and automation tool
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.1"
8
- description = "AI-powered linking and automation tool"
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