devlinker 1.4.2__tar.gz → 1.4.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 (33) hide show
  1. {devlinker-1.4.2 → devlinker-1.4.4}/MANIFEST.in +0 -1
  2. {devlinker-1.4.2/devlinker.egg-info → devlinker-1.4.4}/PKG-INFO +20 -3
  3. {devlinker-1.4.2 → devlinker-1.4.4}/README.md +19 -2
  4. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/detector.py +4 -4
  5. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/main.py +11 -7
  6. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/proxy.py +30 -5
  7. devlinker-1.4.4/devlinker/share.py +64 -0
  8. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/tunnel.py +44 -21
  9. {devlinker-1.4.2 → devlinker-1.4.4/devlinker.egg-info}/PKG-INFO +20 -3
  10. {devlinker-1.4.2 → devlinker-1.4.4}/pyproject.toml +1 -1
  11. devlinker-1.4.2/devlinker/share.py +0 -32
  12. {devlinker-1.4.2 → devlinker-1.4.4}/LICENSE +0 -0
  13. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/__init__.py +0 -0
  14. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/config.py +0 -0
  15. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/detection_state.py +0 -0
  16. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/detector_ai.py +0 -0
  17. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/devlinker_loader_instant.html +0 -0
  18. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/devlinker_loader_snippet.html +0 -0
  19. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/doctor.py +0 -0
  20. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/fix.py +0 -0
  21. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/fixer.py +0 -0
  22. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/global_state.py +0 -0
  23. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/inspect.py +0 -0
  24. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/logger.py +0 -0
  25. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/monitor.py +0 -0
  26. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker/runner.py +0 -0
  27. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker.egg-info/SOURCES.txt +0 -0
  28. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker.egg-info/dependency_links.txt +0 -0
  29. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker.egg-info/entry_points.txt +0 -0
  30. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker.egg-info/requires.txt +0 -0
  31. {devlinker-1.4.2 → devlinker-1.4.4}/devlinker.egg-info/top_level.txt +0 -0
  32. {devlinker-1.4.2 → devlinker-1.4.4}/setup.cfg +0 -0
  33. {devlinker-1.4.2 → devlinker-1.4.4}/setup.py +0 -0
@@ -1,3 +1,2 @@
1
1
  include devlinker/devlinker_loader_instant.html
2
- include devlinker/devlinker_loader_minimal.html
3
2
  include devlinker/devlinker_loader_snippet.html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.4.2
3
+ Version: 1.4.4
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
@@ -130,6 +130,7 @@ If DevLinker helps you ship faster, consider supporting the project:
130
130
  - `devlinker support` — Show UPI support QR code in terminal
131
131
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
132
132
  - `devlinker share` — Enable public tunnel at runtime (no restart)
133
+ - `devlinker share --proxy-port 18000` — Enable public tunnel for a custom proxy port
133
134
  - `devlinker unshare` — Disable public tunnel at runtime
134
135
  - `devlinker doctor` — Diagnose issues, see categorized problems and fixes
135
136
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
@@ -278,14 +279,16 @@ devlinker --docker
278
279
 
279
280
  ## Tunnel and Sharing Modes
280
281
 
281
- By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
282
+ By default, DevLinker starts **fast local proxy only** (no tunnel). It prints a LAN URL when it can detect a local network interface, and you can share that link with devices on the same Wi-Fi/LAN.
283
+
284
+ For access from another network, start with a public tunnel using the `--url` flag:
282
285
 
283
286
 
284
287
  ```bash
285
288
  devlinker --url
286
289
  ```
287
290
 
288
- This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
291
+ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The output will show:
289
292
 
290
293
  ```text
291
294
  🌍 Enabling public tunnel...
@@ -296,6 +299,20 @@ This will start the proxy and open a public tunnel (Cloudflare or ngrok). The ou
296
299
  ℹ Share this link with collaborators.
297
300
  ```
298
301
 
302
+ If you already started DevLinker and want to turn on sharing without restarting, use:
303
+
304
+ ```bash
305
+ devlinker share
306
+ ```
307
+
308
+ If you use a custom proxy port, pass it explicitly:
309
+
310
+ ```bash
311
+ devlinker share --proxy-port 18000
312
+ ```
313
+
314
+ If your friend is on the same Wi-Fi/LAN, use the printed LAN URL like `http://192.168.x.x:<proxy-port>`. If they are outside your network, use the public tunnel URL instead.
315
+
299
316
  To force tunnel off (even if --url is passed):
300
317
 
301
318
  ```bash
@@ -110,6 +110,7 @@ If DevLinker helps you ship faster, consider supporting the project:
110
110
  - `devlinker support` — Show UPI support QR code in terminal
111
111
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
112
112
  - `devlinker share` — Enable public tunnel at runtime (no restart)
113
+ - `devlinker share --proxy-port 18000` — Enable public tunnel for a custom proxy port
113
114
  - `devlinker unshare` — Disable public tunnel at runtime
114
115
  - `devlinker doctor` — Diagnose issues, see categorized problems and fixes
115
116
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
@@ -258,14 +259,16 @@ devlinker --docker
258
259
 
259
260
  ## Tunnel and Sharing Modes
260
261
 
261
- By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
262
+ By default, DevLinker starts **fast local proxy only** (no tunnel). It prints a LAN URL when it can detect a local network interface, and you can share that link with devices on the same Wi-Fi/LAN.
263
+
264
+ For access from another network, start with a public tunnel using the `--url` flag:
262
265
 
263
266
 
264
267
  ```bash
265
268
  devlinker --url
266
269
  ```
267
270
 
268
- This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
271
+ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The output will show:
269
272
 
270
273
  ```text
271
274
  🌍 Enabling public tunnel...
@@ -276,6 +279,20 @@ This will start the proxy and open a public tunnel (Cloudflare or ngrok). The ou
276
279
  ℹ Share this link with collaborators.
277
280
  ```
278
281
 
282
+ If you already started DevLinker and want to turn on sharing without restarting, use:
283
+
284
+ ```bash
285
+ devlinker share
286
+ ```
287
+
288
+ If you use a custom proxy port, pass it explicitly:
289
+
290
+ ```bash
291
+ devlinker share --proxy-port 18000
292
+ ```
293
+
294
+ If your friend is on the same Wi-Fi/LAN, use the printed LAN URL like `http://192.168.x.x:<proxy-port>`. If they are outside your network, use the public tunnel URL instead.
295
+
279
296
  To force tunnel off (even if --url is passed):
280
297
 
281
298
  ```bash
@@ -15,7 +15,7 @@ DEFAULT_BACKEND_PROBE_PATHS = (
15
15
 
16
16
  def check_port(
17
17
  port: int,
18
- timeout: float = 1.0,
18
+ timeout: float = 0.4,
19
19
  probe_paths: Iterable[str] = DEFAULT_BACKEND_PROBE_PATHS,
20
20
  ) -> bool:
21
21
  """Return True when an HTTP service is reachable on localhost:port.
@@ -53,7 +53,7 @@ def check_port(
53
53
  return False
54
54
 
55
55
 
56
- def is_vite_port(port: int, timeout: float = 1.0) -> bool:
56
+ def is_vite_port(port: int, timeout: float = 0.4) -> bool:
57
57
  """Return True when port looks like a Vite dev server."""
58
58
  for host in ("localhost", "127.0.0.1"):
59
59
  try:
@@ -95,8 +95,8 @@ def _ordered_unique_ports(*port_groups: Iterable[int]) -> list[int]:
95
95
  def detect_ports(
96
96
  frontend: Optional[int] = None,
97
97
  backend: Optional[int] = None,
98
- retries: int = 12,
99
- delay_seconds: float = 1.0,
98
+ retries: int = 16,
99
+ delay_seconds: float = 0.5,
100
100
  ) -> Tuple[Optional[int], Optional[int]]:
101
101
  """Detect frontend and backend ports with retry support for slow startups."""
102
102
  frontend_ports = _ordered_unique_ports(
@@ -33,7 +33,7 @@ except ImportError: # pragma: no cover - fallback when rich is unavailable
33
33
 
34
34
  from . import __version__
35
35
  from .detector import check_port, detect_ports, is_vite_port
36
- from .proxy import start_proxy
36
+ from .proxy import start_proxy, wait_for_proxy_startup
37
37
  from .runner import detect_backend_port, start_servers
38
38
  from .tunnel import start_tunnel
39
39
  from .doctor import doctor
@@ -43,6 +43,7 @@ from .share import share, unshare
43
43
  from .config import load_config
44
44
  from .inspect import inspect
45
45
  from .monitor import monitor
46
+ from .global_state import STATE
46
47
 
47
48
  SUPPORT_UPI_ID = "devlinker@upi"
48
49
  SUPPORT_UPI_LINK = "upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀"
@@ -408,8 +409,8 @@ def _wait_for_readiness(
408
409
  label: str,
409
410
  port: int,
410
411
  checker,
411
- retries: int = 15,
412
- delay_seconds: float = 1.0,
412
+ retries: int = 20,
413
+ delay_seconds: float = 0.5,
413
414
  ) -> bool:
414
415
  _ui_status("⏳", f"Waiting for {label} ({port})...", style="cyan")
415
416
  for attempt in range(1, retries + 1):
@@ -427,8 +428,8 @@ def _wait_for_readiness_live(
427
428
  port: int,
428
429
  checker,
429
430
  live_status: _LiveStatus,
430
- retries: int = 15,
431
- delay_seconds: float = 1.0,
431
+ retries: int = 20,
432
+ delay_seconds: float = 0.5,
432
433
  ) -> bool:
433
434
  live_status.update(label, f"⏳ Waiting ({port})...", style="cyan")
434
435
  for attempt in range(1, retries + 1):
@@ -602,6 +603,7 @@ def _run_proxy(
602
603
  )
603
604
 
604
605
  proxy_port = _select_proxy_port(proxy_port)
606
+ STATE["proxy_port"] = proxy_port
605
607
  _write_frontend_api_env(proxy_port)
606
608
 
607
609
  if not live_status:
@@ -619,8 +621,10 @@ def _run_proxy(
619
621
  enable_debug_logs=debug,
620
622
  )
621
623
 
622
- # Allow proxy thread to bind before opening tunnel.
623
- time.sleep(1)
624
+ if not wait_for_proxy_startup(timeout=5.0):
625
+ raise click.ClickException(
626
+ f"Proxy failed to start on port {proxy_port}. Check whether the port is already in use."
627
+ )
624
628
 
625
629
  if live_status:
626
630
  live_status.update("Proxy", f"✔ Active ({proxy_port})", style="green")
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import os
5
+ import sys
5
6
  import time
6
7
  from typing import Dict, Optional
7
- from urllib.parse import urlencode
8
+ from urllib.parse import parse_qsl, urlencode, urlsplit
8
9
 
9
10
  import httpx
10
11
  import uvicorn
@@ -26,6 +27,7 @@ _printed_fixes = set()
26
27
  _printed_live_header = False
27
28
  LIVE_REQUEST_LOGGING_ENABLED = False
28
29
  MAX_RECENT_REQUESTS = 200
30
+ PROXY_READY_EVENT = threading.Event()
29
31
 
30
32
 
31
33
  def _format_request_context(path: str, method: str | None, status: int, target: str) -> str:
@@ -66,6 +68,19 @@ def _extract_presented_token(headers: Dict[str, str], query_params) -> str | Non
66
68
  query_token = query_params.get("dl_token")
67
69
  if query_token:
68
70
  return query_token
71
+
72
+ # WebSocket handshakes may not include dl_token in the WS URL, but the
73
+ # browser Referer usually contains the original page URL query string.
74
+ referer = headers.get("referer", "").strip()
75
+ if referer:
76
+ try:
77
+ referer_query = dict(parse_qsl(urlsplit(referer).query, keep_blank_values=True))
78
+ referer_token = (referer_query.get("dl_token") or "").strip()
79
+ if referer_token:
80
+ return referer_token
81
+ except ValueError:
82
+ pass
83
+
69
84
  direct_header = headers.get("x-devlinker-token", "").strip()
70
85
  if direct_header:
71
86
  return direct_header
@@ -175,6 +190,7 @@ def _apply_cors_headers(headers: Dict[str, str], request: Request) -> Dict[str,
175
190
  async def _on_startup() -> None:
176
191
  global HTTP_CLIENT
177
192
  HTTP_CLIENT = httpx.AsyncClient(timeout=15.0, follow_redirects=False)
193
+ PROXY_READY_EVENT.set()
178
194
 
179
195
 
180
196
  @app.on_event("shutdown")
@@ -183,6 +199,7 @@ async def _on_shutdown() -> None:
183
199
  if HTTP_CLIENT is not None:
184
200
  await HTTP_CLIENT.aclose()
185
201
  HTTP_CLIENT = None
202
+ PROXY_READY_EVENT.clear()
186
203
 
187
204
 
188
205
  def _connection_header_tokens(headers: Dict[str, str]) -> set[str]:
@@ -210,6 +227,7 @@ def _filter_websocket_headers(incoming: Dict[str, str]) -> Dict[str, str]:
210
227
  connection_tokens = _connection_header_tokens(incoming)
211
228
  excluded = HOP_BY_HOP_HEADERS | connection_tokens | {
212
229
  "host",
230
+ "origin",
213
231
  "sec-websocket-key",
214
232
  "sec-websocket-version",
215
233
  "sec-websocket-extensions",
@@ -720,9 +738,16 @@ def start_proxy(
720
738
  BACKEND = backend_port
721
739
  LIVE_REQUEST_LOGGING_ENABLED = enable_debug_logs
722
740
  _printed_live_header = False
741
+ PROXY_READY_EVENT.clear()
723
742
 
724
- thread = threading.Thread(
725
- target=lambda: uvicorn.run(app, host="0.0.0.0", port=proxy_port, log_level="warning"),
726
- daemon=True,
727
- )
743
+ def _run_server() -> None:
744
+ if sys.platform.startswith("win") and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
745
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
746
+ uvicorn.run(app, host="0.0.0.0", port=proxy_port, log_level="warning")
747
+
748
+ thread = threading.Thread(target=_run_server, daemon=True)
728
749
  thread.start()
750
+
751
+
752
+ def wait_for_proxy_startup(timeout: float = 5.0) -> bool:
753
+ return PROXY_READY_EVENT.wait(timeout)
@@ -0,0 +1,64 @@
1
+ import click
2
+ import requests
3
+
4
+ from devlinker.global_state import STATE
5
+ from devlinker.tunnel import start_tunnel
6
+
7
+
8
+ _COMMON_PROXY_PORTS = (8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 18000)
9
+
10
+
11
+ def _is_devlinker_proxy(port: int) -> bool:
12
+ try:
13
+ response = requests.get(f"http://127.0.0.1:{port}/__devlinker/dashboard", timeout=0.5)
14
+ except requests.RequestException:
15
+ return False
16
+ return response.status_code == 200 and "API Logs Dashboard" in response.text
17
+
18
+
19
+ def _resolve_proxy_port(requested_port: int | None) -> int:
20
+ if requested_port is not None:
21
+ if _is_devlinker_proxy(requested_port):
22
+ return requested_port
23
+ raise click.ClickException(
24
+ f"No DevLinker proxy is running on port {requested_port}. Start devlinker first, or pass the correct --proxy-port."
25
+ )
26
+
27
+ for candidate in _COMMON_PROXY_PORTS:
28
+ if _is_devlinker_proxy(candidate):
29
+ return candidate
30
+
31
+ raise click.ClickException(
32
+ "No running DevLinker proxy was found. Start devlinker first, or pass --proxy-port <port>."
33
+ )
34
+
35
+ @click.command()
36
+ @click.option("--proxy-port", type=int, default=None, help="Proxy port to tunnel. Auto-detect when omitted.")
37
+ def share(proxy_port: int | None):
38
+ """Enable public tunnel at runtime (no restart)."""
39
+ if STATE["tunnel"]:
40
+ click.secho("⚠️ Already shared", fg="yellow")
41
+ return
42
+ try:
43
+ resolved_proxy_port = _resolve_proxy_port(proxy_port)
44
+ STATE["proxy_port"] = resolved_proxy_port
45
+ provider, url = start_tunnel(resolved_proxy_port)
46
+ STATE["tunnel"] = url
47
+ click.secho("\n🌍 Public Sharing Enabled\n" + ("─" * 24), fg="green", bold=True)
48
+ click.secho("✔ Tunnel connected", fg="green")
49
+ click.secho(f"\nPublic URL:\n{url}\n", fg="cyan", bold=True)
50
+ click.secho("📤 Share this link with your team", fg="magenta")
51
+ except Exception as exc:
52
+ click.secho(f"[WARN] Tunnel failed: {exc}", fg="red")
53
+ click.secho("[INFO] Next step: install cloudflared or configure ngrok auth.", fg="yellow")
54
+
55
+ @click.command()
56
+ def unshare():
57
+ """Disable public tunnel at runtime (no restart)."""
58
+ if not STATE["tunnel"]:
59
+ click.secho("⚠️ No active tunnel", fg="yellow")
60
+ return
61
+ from devlinker.tunnel import stop_tunnel
62
+ stop_tunnel()
63
+ STATE["tunnel"] = None
64
+ click.secho("🛑 Sharing stopped", fg="red", bold=True)
@@ -20,8 +20,11 @@ def stop_tunnel():
20
20
  _CLOUDFLARED_PROCESSES.clear()
21
21
 
22
22
  import re
23
+ import queue
23
24
  import shutil
24
25
  import subprocess
26
+ import threading
27
+ import time
25
28
  from subprocess import TimeoutExpired
26
29
 
27
30
  from pyngrok import ngrok
@@ -56,33 +59,53 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
56
59
  text=True,
57
60
  )
58
61
 
59
- output = ""
60
- try:
61
- stdout, _ = process.communicate(timeout=startup_timeout)
62
- output = stdout or ""
63
- except TimeoutExpired as exc:
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)
62
+ # Read cloudflared logs incrementally so we can return as soon as the URL appears.
63
+ output_queue: queue.Queue[str | None] = queue.Queue()
64
+
65
+ def _reader() -> None:
66
+ stream = process.stdout
67
+ if stream is None:
68
+ output_queue.put(None)
69
+ return
70
+ try:
71
+ for line in stream:
72
+ output_queue.put(line)
73
+ finally:
74
+ output_queue.put(None)
79
75
 
76
+ threading.Thread(target=_reader, daemon=True).start()
77
+
78
+ output = ""
79
+ deadline = time.monotonic() + max(startup_timeout, 0.1)
80
+ stream_closed = False
81
+ while time.monotonic() < deadline:
82
+ try:
83
+ chunk = output_queue.get(timeout=0.1)
84
+ except queue.Empty:
85
+ chunk = ""
86
+
87
+ if chunk is None:
88
+ stream_closed = True
89
+ break
90
+ if chunk:
91
+ output += chunk
92
+ url = _extract_trycloudflare_url(output)
93
+ if url:
94
+ _CLOUDFLARED_PROCESSES.append(process)
95
+ return url
96
+
97
+ if process.poll() is not None and output_queue.empty():
98
+ break
99
+
100
+ # One final parse pass before treating startup as failed.
80
101
  url = _extract_trycloudflare_url(output)
81
102
  if url:
82
103
  _CLOUDFLARED_PROCESSES.append(process)
83
104
  return url
84
105
 
85
- process.terminate()
106
+ if not stream_closed and process.poll() is None:
107
+ process.terminate()
108
+
86
109
  return None
87
110
 
88
111
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.4.2
3
+ Version: 1.4.4
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
@@ -130,6 +130,7 @@ If DevLinker helps you ship faster, consider supporting the project:
130
130
  - `devlinker support` — Show UPI support QR code in terminal
131
131
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
132
132
  - `devlinker share` — Enable public tunnel at runtime (no restart)
133
+ - `devlinker share --proxy-port 18000` — Enable public tunnel for a custom proxy port
133
134
  - `devlinker unshare` — Disable public tunnel at runtime
134
135
  - `devlinker doctor` — Diagnose issues, see categorized problems and fixes
135
136
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
@@ -278,14 +279,16 @@ devlinker --docker
278
279
 
279
280
  ## Tunnel and Sharing Modes
280
281
 
281
- By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
282
+ By default, DevLinker starts **fast local proxy only** (no tunnel). It prints a LAN URL when it can detect a local network interface, and you can share that link with devices on the same Wi-Fi/LAN.
283
+
284
+ For access from another network, start with a public tunnel using the `--url` flag:
282
285
 
283
286
 
284
287
  ```bash
285
288
  devlinker --url
286
289
  ```
287
290
 
288
- This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
291
+ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The output will show:
289
292
 
290
293
  ```text
291
294
  🌍 Enabling public tunnel...
@@ -296,6 +299,20 @@ This will start the proxy and open a public tunnel (Cloudflare or ngrok). The ou
296
299
  ℹ Share this link with collaborators.
297
300
  ```
298
301
 
302
+ If you already started DevLinker and want to turn on sharing without restarting, use:
303
+
304
+ ```bash
305
+ devlinker share
306
+ ```
307
+
308
+ If you use a custom proxy port, pass it explicitly:
309
+
310
+ ```bash
311
+ devlinker share --proxy-port 18000
312
+ ```
313
+
314
+ If your friend is on the same Wi-Fi/LAN, use the printed LAN URL like `http://192.168.x.x:<proxy-port>`. If they are outside your network, use the public tunnel URL instead.
315
+
299
316
  To force tunnel off (even if --url is passed):
300
317
 
301
318
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlinker"
7
- version = "1.4.2"
7
+ version = "1.4.4"
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,32 +0,0 @@
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)
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