devlinker 1.3.8__tar.gz → 1.4.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.
Files changed (33) hide show
  1. {devlinker-1.3.8 → devlinker-1.4.0}/PKG-INFO +14 -1
  2. devlinker-1.3.8/devlinker.egg-info/PKG-INFO → devlinker-1.4.0/README.md +12 -18
  3. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/devlinker_loader_instant.html +20 -3
  4. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/devlinker_loader_snippet.html +42 -1
  5. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/main.py +101 -6
  6. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/proxy.py +66 -12
  7. devlinker-1.3.8/README.md → devlinker-1.4.0/devlinker.egg-info/PKG-INFO +31 -0
  8. devlinker-1.4.0/devlinker.egg-info/entry_points.txt +2 -0
  9. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/requires.txt +1 -0
  10. {devlinker-1.3.8 → devlinker-1.4.0}/pyproject.toml +3 -2
  11. devlinker-1.3.8/devlinker.egg-info/entry_points.txt +0 -2
  12. {devlinker-1.3.8 → devlinker-1.4.0}/LICENSE +0 -0
  13. {devlinker-1.3.8 → devlinker-1.4.0}/MANIFEST.in +0 -0
  14. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/__init__.py +0 -0
  15. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/config.py +0 -0
  16. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detection_state.py +0 -0
  17. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detector.py +0 -0
  18. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detector_ai.py +0 -0
  19. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/doctor.py +0 -0
  20. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/fix.py +0 -0
  21. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/fixer.py +0 -0
  22. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/global_state.py +0 -0
  23. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/inspect.py +0 -0
  24. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/logger.py +0 -0
  25. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/monitor.py +0 -0
  26. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/runner.py +0 -0
  27. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/share.py +0 -0
  28. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/tunnel.py +0 -0
  29. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/SOURCES.txt +0 -0
  30. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/dependency_links.txt +0 -0
  31. {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/top_level.txt +0 -0
  32. {devlinker-1.3.8 → devlinker-1.4.0}/setup.cfg +0 -0
  33. {devlinker-1.3.8 → devlinker-1.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.8
3
+ Version: 1.4.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
6
  Requires-Python: >=3.7
@@ -11,6 +11,7 @@ Requires-Dist: docker
11
11
  Requires-Dist: fastapi
12
12
  Requires-Dist: httpx
13
13
  Requires-Dist: pyngrok
14
+ Requires-Dist: qrcode[pil]
14
15
  Requires-Dist: requests
15
16
  Requires-Dist: uvicorn
16
17
  Requires-Dist: websockets
@@ -35,9 +36,21 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
35
36
  - 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
36
37
  - 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
37
38
 
39
+ ## Support DevLinker
40
+
41
+ If DevLinker helps you ship faster, consider supporting the project:
42
+
43
+ > 💖 Support DevLinker
44
+ > [![UPI](https://img.shields.io/badge/UPI-devlinker%40upi-22c55e?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
45
+ > [![Donate](https://img.shields.io/badge/Support-Scan%20or%20Pay-0ea5e9?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
46
+
47
+ - 💖 UPI: devlinker@upi
48
+ - 🔗 UPI link: upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀
49
+
38
50
  ## CLI Commands & Options
39
51
 
40
52
  - `devlinker` — Start proxy (local only, fast)
53
+ - `devlinker support` — Show UPI support QR code in terminal
41
54
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
42
55
  - `devlinker share` — Enable public tunnel at runtime (no restart)
43
56
  - `devlinker unshare` — Disable public tunnel at runtime
@@ -1,21 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: devlinker
3
- Version: 1.3.8
4
- Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
- Author-email: Mani <mani1028@users.noreply.github.com>
6
- Requires-Python: >=3.7
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: click
10
- Requires-Dist: docker
11
- Requires-Dist: fastapi
12
- Requires-Dist: httpx
13
- Requires-Dist: pyngrok
14
- Requires-Dist: requests
15
- Requires-Dist: uvicorn
16
- Requires-Dist: websockets
17
- Dynamic: license-file
18
-
19
1
  # Dev Linker
20
2
 
21
3
  Dev Linker runs frontend and backend dev servers, proxies both through a single local port (8000), and creates a single public URL via Cloudflare or ngrok.
@@ -35,9 +17,21 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
35
17
  - 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
36
18
  - 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
37
19
 
20
+ ## Support DevLinker
21
+
22
+ If DevLinker helps you ship faster, consider supporting the project:
23
+
24
+ > 💖 Support DevLinker
25
+ > [![UPI](https://img.shields.io/badge/UPI-devlinker%40upi-22c55e?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
26
+ > [![Donate](https://img.shields.io/badge/Support-Scan%20or%20Pay-0ea5e9?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
27
+
28
+ - 💖 UPI: devlinker@upi
29
+ - 🔗 UPI link: upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀
30
+
38
31
  ## CLI Commands & Options
39
32
 
40
33
  - `devlinker` — Start proxy (local only, fast)
34
+ - `devlinker support` — Show UPI support QR code in terminal
41
35
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
42
36
  - `devlinker share` — Enable public tunnel at runtime (no restart)
43
37
  - `devlinker unshare` — Disable public tunnel at runtime
@@ -63,8 +63,22 @@
63
63
  font-size: 0.92rem;
64
64
  font-weight: 500;
65
65
  opacity: 0.9;
66
- pointer-events: none;
67
- user-select: none;
66
+ pointer-events: auto;
67
+ user-select: text;
68
+ }
69
+ .devlinker-powered span {
70
+ display: block;
71
+ margin-top: 4px;
72
+ color: #cbd5e1;
73
+ }
74
+ .devlinker-powered a {
75
+ color: #7dd3fc;
76
+ text-decoration: none;
77
+ font-weight: 600;
78
+ }
79
+ .devlinker-powered a:hover {
80
+ color: #bae6fd;
81
+ text-decoration: underline;
68
82
  }
69
83
  @keyframes devlinker-spin {
70
84
  to { transform: rotate(360deg); }
@@ -80,7 +94,10 @@
80
94
  <div class="devlinker-health">Frontend connected • Backend connected</div>
81
95
  </div>
82
96
  </div>
83
- <div class="devlinker-powered">Powered by DevLinker</div>
97
+ <div class="devlinker-powered">
98
+ Powered by DevLinker 🚀
99
+ <span>Support the project ❤️ <a href="upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀">devlinker@upi</a></span>
100
+ </div>
84
101
  <script>
85
102
  // Fetch the real page and replace the loader with a short minimum display for smoothness.
86
103
  (async function() {
@@ -98,6 +98,33 @@
98
98
  font-weight: 500;
99
99
  opacity: 0.95;
100
100
  }
101
+ .devlinker-support {
102
+ color: #cbd5e1;
103
+ line-height: 1.45;
104
+ }
105
+ .devlinker-support a {
106
+ color: #7dd3fc;
107
+ text-decoration: none;
108
+ font-weight: 600;
109
+ }
110
+ .devlinker-support a:hover {
111
+ color: #bae6fd;
112
+ text-decoration: underline;
113
+ }
114
+ .devlinker-warning {
115
+ display: none;
116
+ margin-top: 12px;
117
+ border: 1px solid rgba(251, 191, 36, 0.45);
118
+ background: rgba(120, 53, 15, 0.26);
119
+ border-radius: 12px;
120
+ padding: 10px 12px;
121
+ color: #fde68a;
122
+ font-size: 0.9rem;
123
+ line-height: 1.4;
124
+ }
125
+ .devlinker-warning strong {
126
+ color: #fef3c7;
127
+ }
101
128
  @keyframes devlinker-spin {
102
129
  to { transform: rotate(360deg); }
103
130
  }
@@ -124,8 +151,12 @@
124
151
  <div class="devlinker-tip">Tip: Share your app instantly with one link. No CORS issues, no port confusion.</div>
125
152
  <div class="devlinker-tip">Go Pro: custom domains, faster sharing, and no branding.</div>
126
153
  <a class="devlinker-upgrade" href="https://devlinker.app" target="_blank" rel="noopener">Explore Pro at devlinker.app</a>
154
+ <div class="devlinker-support">Support the project ❤️ <a href="upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀">devlinker@upi</a></div>
155
+ <div class="devlinker-warning" id="devlinker-camera-warning">
156
+ <strong>Camera warning:</strong> camera and microphone are blocked on LAN HTTP URLs by browser policy. Use localhost or run <strong>devlinker --url</strong> for HTTPS sharing.
157
+ </div>
127
158
  </div>
128
- <div class="devlinker-powered">Powered by DevLinker</div>
159
+ <div class="devlinker-powered">Powered by DevLinker 🚀</div>
129
160
  </div>
130
161
  </div>
131
162
  <script>
@@ -149,6 +180,16 @@ const MIN_TIME = 800;
149
180
  const MAX_TIME = 5000;
150
181
  let hidden = false;
151
182
 
183
+ const cameraContext = window.__DEVLINKER_CAMERA_CONTEXT__ || {};
184
+ const warningElement = document.getElementById("devlinker-camera-warning");
185
+ if (warningElement) {
186
+ const isLan = cameraContext.mode === "lan";
187
+ const isSecure = Boolean(cameraContext.secure);
188
+ if (isLan && !isSecure) {
189
+ warningElement.style.display = "block";
190
+ }
191
+ }
192
+
152
193
  function hideLoader() {
153
194
  if (hidden) return;
154
195
  hidden = true;
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import socket
4
4
  import time
5
+ import webbrowser
5
6
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
6
7
 
7
8
  import click
@@ -19,6 +20,32 @@ from .config import load_config
19
20
  from .inspect import inspect
20
21
  from .monitor import monitor
21
22
 
23
+ SUPPORT_UPI_ID = "devlinker@upi"
24
+ SUPPORT_UPI_LINK = "upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀"
25
+ SUPPORT_QR_FALLBACK = [
26
+ "#######.......#######",
27
+ "#.....#.......#.....#",
28
+ "#.###.#.......#.###.#",
29
+ "#.###.#.......#.###.#",
30
+ "#.###.#.......#.###.#",
31
+ "#.....#.......#.....#",
32
+ "#######.......#######",
33
+ "##..##.##..##.##..##.",
34
+ ".##..##..##..##..##..",
35
+ "###...###...###...###",
36
+ "..###....###....###..",
37
+ "###..#.###..#.###..#.",
38
+ "#..###.#..###.#..###.",
39
+ "....#......#......#..",
40
+ "#######..............",
41
+ "#.....#..............",
42
+ "#.###.#..............",
43
+ "#.###.#..............",
44
+ "#.###.#..............",
45
+ "#.....#..............",
46
+ "#######..............",
47
+ ]
48
+
22
49
 
23
50
  def _is_port_in_use(port: int) -> bool:
24
51
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@@ -83,6 +110,35 @@ def _print_summary(
83
110
  click.secho("Tip: Press Ctrl+Click to open link", fg="magenta")
84
111
  else:
85
112
  click.secho(" Public → Disabled (use --url)", fg="yellow")
113
+ click.secho("\n💡 Enjoying DevLinker? Support the project ❤️", fg="magenta")
114
+ click.secho(f"UPI: {SUPPORT_UPI_ID}", fg="yellow")
115
+ click.secho("Run: devlinker support (shows QR)", fg="yellow")
116
+
117
+
118
+ def _print_support_qr(open_link: bool) -> None:
119
+ click.secho("\n💖 Support DevLinker 🚀", fg="magenta", bold=True)
120
+ click.secho("Help keep the tool free and improving!", fg="white")
121
+ click.secho(f"\nUPI: {SUPPORT_UPI_ID}", fg="yellow")
122
+ click.secho(f"Link: {SUPPORT_UPI_LINK}\n", fg="cyan")
123
+
124
+ try:
125
+ import qrcode
126
+ except ImportError:
127
+ click.secho("ASCII QR (fallback):", fg="yellow")
128
+ for row in SUPPORT_QR_FALLBACK:
129
+ click.echo(row.replace("#", "##").replace(".", " "))
130
+ click.secho("\nInstall full QR support: pip install qrcode[pil]", fg="yellow")
131
+ if open_link:
132
+ webbrowser.open(SUPPORT_UPI_LINK)
133
+ return
134
+
135
+ qr = qrcode.QRCode(border=1)
136
+ qr.add_data(SUPPORT_UPI_LINK)
137
+ qr.make(fit=True)
138
+ qr.print_ascii(invert=True)
139
+
140
+ if open_link:
141
+ webbrowser.open(SUPPORT_UPI_LINK)
86
142
 
87
143
 
88
144
  def _get_local_ip() -> str | None:
@@ -117,7 +173,7 @@ def _wait_for_readiness(
117
173
  return False
118
174
 
119
175
 
120
- @click.command()
176
+ @click.group(invoke_without_command=True)
121
177
  @click.version_option(version=__version__, prog_name="devlinker")
122
178
  @click.option("--frontend", type=int, default=None, help="Override detected frontend port.")
123
179
  @click.option(
@@ -151,8 +207,36 @@ def _wait_for_readiness(
151
207
  help="Show WLAN sharing URL for devices on the same network.",
152
208
  )
153
209
  @click.option("--debug", is_flag=True, hidden=True, help="Enable debug logging.")
210
+ @click.pass_context
211
+ def main(
212
+ ctx: click.Context,
213
+ frontend: int | None,
214
+ backend_port_override: int | None,
215
+ proxy_port: int,
216
+ auto_start_docker: bool,
217
+ url: bool,
218
+ no_tunnel: bool,
219
+ interactive_backend: bool,
220
+ lan_enabled: bool,
221
+ debug: bool,
222
+ ) -> None:
223
+ if ctx.invoked_subcommand is not None:
224
+ return
154
225
 
155
- def cli(
226
+ _run_proxy(
227
+ frontend,
228
+ backend_port_override,
229
+ proxy_port,
230
+ auto_start_docker,
231
+ url,
232
+ no_tunnel,
233
+ interactive_backend,
234
+ lan_enabled,
235
+ debug,
236
+ )
237
+
238
+
239
+ def _run_proxy(
156
240
  frontend: int | None,
157
241
  backend_port_override: int | None,
158
242
  proxy_port: int,
@@ -236,6 +320,11 @@ def cli(
236
320
  wlan_url = f"http://{local_ip}:{proxy_port}"
237
321
  click.secho(f"[OK] WLAN URL: {wlan_url}", fg="green")
238
322
  click.secho("[INFO] Share WLAN link with teammates on same WiFi/LAN.", fg="blue")
323
+ click.secho(
324
+ "[WARN] Camera/mic may be blocked on WLAN HTTP links by browser security."
325
+ " Use localhost or --url for HTTPS.",
326
+ fg="yellow",
327
+ )
239
328
  else:
240
329
  click.secho("[WARN] WLAN URL unavailable (no active LAN interface detected).", fg="yellow")
241
330
  click.secho("[INFO] If LAN sharing fails, allow proxy port in firewall and use same network.", fg="yellow")
@@ -286,19 +375,25 @@ def cli(
286
375
  click.secho("\n[INFO] Dev Linker stopped.", fg="yellow")
287
376
 
288
377
 
289
- @click.group()
290
- def main():
291
- pass
378
+ @click.command()
379
+ @click.option(
380
+ "--open",
381
+ "open_link",
382
+ is_flag=True,
383
+ help="Open the UPI link in your browser after rendering the QR.",
384
+ )
385
+ def support(open_link: bool) -> None:
386
+ _print_support_qr(open_link)
292
387
 
293
388
 
294
389
 
295
- main.add_command(cli)
296
390
  main.add_command(doctor)
297
391
  main.add_command(fix)
298
392
  main.add_command(share)
299
393
  main.add_command(unshare)
300
394
  main.add_command(inspect)
301
395
  main.add_command(monitor)
396
+ main.add_command(support)
302
397
 
303
398
  if __name__ == "__main__":
304
399
  main()
@@ -21,6 +21,21 @@ from devlinker.detection_state import state
21
21
  import threading
22
22
  _recent_requests = []
23
23
  _recent_lock = threading.Lock()
24
+ _printed_fixes = set()
25
+
26
+
27
+ def _format_request_context(path: str, method: str | None, status: int, target: str) -> str:
28
+ safe_method = method.upper() if method else "UNKNOWN"
29
+ return f"{safe_method} {path} -> {target} ({status})"
30
+
31
+
32
+ def _print_fix_once(message: str) -> None:
33
+ key = message.strip().lower()
34
+ if key in _printed_fixes:
35
+ return
36
+ _printed_fixes.add(key)
37
+ from devlinker.logger import print_fix
38
+ print_fix(message)
24
39
 
25
40
  class RequestInspector:
26
41
  def analyze(self, path, status, target, method=None, response_text=None):
@@ -73,6 +88,12 @@ HOP_BY_HOP_HEADERS = {
73
88
  }
74
89
 
75
90
 
91
+ def _apply_security_headers(headers: Dict[str, str]) -> Dict[str, str]:
92
+ # Explicitly allow camera/mic for the current origin.
93
+ headers["Permissions-Policy"] = "camera=(self), microphone=(self)"
94
+ return headers
95
+
96
+
76
97
  @app.on_event("startup")
77
98
  async def _on_startup() -> None:
78
99
  global HTTP_CLIENT
@@ -185,10 +206,20 @@ async def _forward_http(request: Request) -> Response:
185
206
  return "lan"
186
207
  return "public" if ip else "unknown"
187
208
 
209
+ def _is_secure_request(req: Request, host: str) -> bool:
210
+ if req.url.scheme == "https":
211
+ return True
212
+ forwarded_proto = req.headers.get("x-forwarded-proto", "").lower()
213
+ if "https" in forwarded_proto:
214
+ return True
215
+ host_only = host.split(":", 1)[0] if host else ""
216
+ return host_only in ("localhost", "127.0.0.1", "::1")
217
+
188
218
  mode = classify_mode(host_header, client_ip)
189
219
  is_localhost = mode == "localhost"
190
220
  is_lan = mode == "lan"
191
221
  is_public = mode == "public"
222
+ is_secure = _is_secure_request(request, host_header)
192
223
  is_instant = request.headers.get("x-devlinker-instant") == "1"
193
224
  accept_header = request.headers.get("accept", "")
194
225
  sec_fetch_dest = request.headers.get("sec-fetch-dest", "")
@@ -207,8 +238,13 @@ async def _forward_http(request: Request) -> Response:
207
238
  loader_path = os.path.join(os.path.dirname(__file__), "devlinker_loader_instant.html")
208
239
  with open(loader_path, encoding="utf-8") as f:
209
240
  loader_html = f.read()
210
- return Response(content=loader_html, status_code=200, media_type="text/html")
211
- from devlinker.logger import print_warning, print_fix
241
+ return Response(
242
+ content=loader_html,
243
+ status_code=200,
244
+ media_type="text/html",
245
+ headers=_apply_security_headers({}),
246
+ )
247
+ from devlinker.logger import print_warning
212
248
  from devlinker.detector_ai import DevLinkerAI
213
249
 
214
250
  inspector = RequestInspector()
@@ -219,16 +255,21 @@ async def _forward_http(request: Request) -> Response:
219
255
  if request.url.path.startswith("/api"):
220
256
  status = 503
221
257
  warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
258
+ context = _format_request_context(request.url.path, request.method, status, "backend")
222
259
  for w in warnings:
223
260
  if "/api" in w:
224
- print_warning(f"API routing issue detected\n👉 {request.url.path} returned 404\n👉 Try: /api{request.url.path}")
261
+ print_warning(
262
+ f"API routing issue detected | {context}\n"
263
+ f"Fix: Try /api{request.url.path}"
264
+ )
225
265
  else:
226
- print_warning(w)
266
+ print_warning(f"{w} | {context}")
227
267
  return PlainTextResponse("Backend is not configured.", status_code=status)
228
268
  status = 503
229
269
  warnings = inspector.analyze(request.url.path, status, "frontend", method=request.method)
270
+ context = _format_request_context(request.url.path, request.method, status, "frontend")
230
271
  for w in warnings:
231
- print_warning(w)
272
+ print_warning(f"{w} | {context}")
232
273
  return PlainTextResponse("Frontend is not configured.", status_code=status)
233
274
 
234
275
  if HTTP_CLIENT is None:
@@ -249,11 +290,12 @@ async def _forward_http(request: Request) -> Response:
249
290
  except httpx.RequestError as exc:
250
291
  status = 502
251
292
  warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
293
+ context = _format_request_context(request.url.path, request.method, status, "backend")
252
294
  for w in warnings:
253
- print_warning(w)
295
+ print_warning(f"{w} | {context}")
254
296
  ai_suggestions = ai.analyze_failure(str(exc))
255
297
  for s in ai_suggestions:
256
- print_fix(s)
298
+ _print_fix_once(f"{s} | {context}")
257
299
  return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=status)
258
300
 
259
301
  # Analyze response for warnings and fixes
@@ -264,17 +306,21 @@ async def _forward_http(request: Request) -> Response:
264
306
  method=request.method,
265
307
  response_text=upstream.text
266
308
  )
309
+ context = _format_request_context(request.url.path, request.method, upstream.status_code, "backend")
267
310
  for w in warnings:
268
311
  if "/api" in w:
269
- print_warning(f"API routing issue detected\n👉 {request.url.path} returned 404\n👉 Try: /api{request.url.path}")
312
+ print_warning(
313
+ f"API routing issue detected | {context}\n"
314
+ f"Fix: Try /api{request.url.path}"
315
+ )
270
316
  else:
271
- print_warning(w)
317
+ print_warning(f"{w} | {context}")
272
318
  ai_suggestions = ai.analyze_failure(str(upstream.text))
273
319
  for s in ai_suggestions:
274
- print_fix(s)
320
+ _print_fix_once(f"{s} | {context}")
275
321
 
276
322
  # Inject loader overlay only for LAN/WiFi/public HTML responses.
277
- headers = _filter_response_headers(dict(upstream.headers))
323
+ headers = _apply_security_headers(_filter_response_headers(dict(upstream.headers)))
278
324
  content_type = headers.get("content-type", "")
279
325
  is_html = "text/html" in content_type
280
326
  content = upstream.content
@@ -288,7 +334,15 @@ async def _forward_http(request: Request) -> Response:
288
334
  loader_file = "devlinker_loader_snippet.html"
289
335
  with open(os.path.join(os.path.dirname(__file__), loader_file), encoding="utf-8") as f:
290
336
  loader = f.read()
291
- html = html.replace("</body>", loader + "</body>")
337
+ context_script = (
338
+ "<script>window.__DEVLINKER_CAMERA_CONTEXT__="
339
+ + "{"
340
+ + f'"mode":"{mode}",'
341
+ + f'"secure":{str(is_secure).lower()}'
342
+ + "}"
343
+ + ";</script>"
344
+ )
345
+ html = html.replace("</body>", context_script + loader + "</body>")
292
346
  content = html.encode(upstream.encoding or "utf-8")
293
347
  except Exception:
294
348
  pass
@@ -1,3 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: devlinker
3
+ Version: 1.4.0
4
+ Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
+ Author-email: Mani <mani1028@users.noreply.github.com>
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: click
10
+ Requires-Dist: docker
11
+ Requires-Dist: fastapi
12
+ Requires-Dist: httpx
13
+ Requires-Dist: pyngrok
14
+ Requires-Dist: qrcode[pil]
15
+ Requires-Dist: requests
16
+ Requires-Dist: uvicorn
17
+ Requires-Dist: websockets
18
+ Dynamic: license-file
19
+
1
20
  # Dev Linker
2
21
 
3
22
  Dev Linker runs frontend and backend dev servers, proxies both through a single local port (8000), and creates a single public URL via Cloudflare or ngrok.
@@ -17,9 +36,21 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
17
36
  - 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
18
37
  - 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
19
38
 
39
+ ## Support DevLinker
40
+
41
+ If DevLinker helps you ship faster, consider supporting the project:
42
+
43
+ > 💖 Support DevLinker
44
+ > [![UPI](https://img.shields.io/badge/UPI-devlinker%40upi-22c55e?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
45
+ > [![Donate](https://img.shields.io/badge/Support-Scan%20or%20Pay-0ea5e9?style=for-the-badge)](upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
46
+
47
+ - 💖 UPI: devlinker@upi
48
+ - 🔗 UPI link: upi://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀
49
+
20
50
  ## CLI Commands & Options
21
51
 
22
52
  - `devlinker` — Start proxy (local only, fast)
53
+ - `devlinker support` — Show UPI support QR code in terminal
23
54
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
24
55
  - `devlinker share` — Enable public tunnel at runtime (no restart)
25
56
  - `devlinker unshare` — Disable public tunnel at runtime
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devlinker = devlinker.main:main
@@ -3,6 +3,7 @@ docker
3
3
  fastapi
4
4
  httpx
5
5
  pyngrok
6
+ qrcode[pil]
6
7
  requests
7
8
  uvicorn
8
9
  websockets
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlinker"
7
- version = "1.3.8"
7
+ version = "1.4.0"
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" }
@@ -17,13 +17,14 @@ dependencies = [
17
17
  "fastapi",
18
18
  "httpx",
19
19
  "pyngrok",
20
+ "qrcode[pil]",
20
21
  "requests",
21
22
  "uvicorn",
22
23
  "websockets",
23
24
  ]
24
25
 
25
26
  [project.scripts]
26
- devlinker = "devlinker.main:cli"
27
+ devlinker = "devlinker.main:main"
27
28
 
28
29
  [tool.setuptools.packages.find]
29
30
  include = ["devlinker*"]
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- devlinker = devlinker.main:cli
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