devlinker 1.3.4__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 (29) hide show
  1. devlinker-1.3.5/LICENSE +21 -0
  2. {devlinker-1.3.4 → devlinker-1.3.5}/PKG-INFO +3 -1
  3. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/proxy.py +19 -2
  4. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/tunnel.py +38 -11
  5. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/PKG-INFO +3 -1
  6. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/SOURCES.txt +1 -0
  7. {devlinker-1.3.4 → devlinker-1.3.5}/pyproject.toml +1 -1
  8. {devlinker-1.3.4 → devlinker-1.3.5}/README.md +0 -0
  9. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/__init__.py +0 -0
  10. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/config.py +0 -0
  11. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/detection_state.py +0 -0
  12. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/detector.py +0 -0
  13. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/detector_ai.py +0 -0
  14. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/doctor.py +0 -0
  15. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/fix.py +0 -0
  16. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/fixer.py +0 -0
  17. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/global_state.py +0 -0
  18. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/inspect.py +0 -0
  19. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/logger.py +0 -0
  20. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/main.py +0 -0
  21. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/monitor.py +0 -0
  22. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/runner.py +0 -0
  23. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker/share.py +0 -0
  24. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/dependency_links.txt +0 -0
  25. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/entry_points.txt +0 -0
  26. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/requires.txt +0 -0
  27. {devlinker-1.3.4 → devlinker-1.3.5}/devlinker.egg-info/top_level.txt +0 -0
  28. {devlinker-1.3.4 → devlinker-1.3.5}/setup.cfg +0 -0
  29. {devlinker-1.3.4 → 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.4
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
 
@@ -207,10 +207,27 @@ async def _forward_http(request: Request) -> Response:
207
207
  for s in ai_suggestions:
208
208
  print_fix(s)
209
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
210
227
  return Response(
211
- content=upstream.content,
228
+ content=content,
212
229
  status_code=upstream.status_code,
213
- headers=_filter_response_headers(dict(upstream.headers)),
230
+ headers=headers,
214
231
  )
215
232
 
216
233
 
@@ -1,9 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  def stop_tunnel():
2
4
  """Stop all active tunnels (Cloudflare/ngrok)."""
3
5
  # Stop ngrok tunnels
4
6
  try:
5
7
  for tunnel in ngrok.get_tunnels():
6
- ngrok.disconnect(tunnel.public_url)
8
+ public_url = getattr(tunnel, "public_url", None)
9
+ if public_url is not None:
10
+ ngrok.disconnect(public_url)
7
11
  except Exception:
8
12
  pass
9
13
  # Stop cloudflared processes
@@ -14,7 +18,6 @@ def stop_tunnel():
14
18
  except Exception:
15
19
  pass
16
20
  _CLOUDFLARED_PROCESSES.clear()
17
- from __future__ import annotations
18
21
 
19
22
  import re
20
23
  import shutil
@@ -58,7 +61,21 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
58
61
  stdout, _ = process.communicate(timeout=startup_timeout)
59
62
  output = stdout or ""
60
63
  except TimeoutExpired as exc:
61
- 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)
62
79
 
63
80
  url = _extract_trycloudflare_url(output)
64
81
  if url:
@@ -71,22 +88,30 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
71
88
 
72
89
  def _disconnect_existing_tunnels() -> None:
73
90
  for tunnel in ngrok.get_tunnels():
74
- 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)
75
94
 
76
95
 
77
- def _start_ngrok_tunnel(proxy_port: int) -> str:
96
+ def _start_ngrok_tunnel(proxy_port: int) -> str | None:
78
97
  try:
79
- tunnel = ngrok.connect(proxy_port)
80
- 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
81
103
  except PyngrokError as exc:
82
104
  message = str(exc).lower()
83
105
 
84
106
  # If a prior endpoint is still active, disconnect and retry once.
85
- 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):
86
108
  try:
87
109
  _disconnect_existing_tunnels()
88
- tunnel = ngrok.connect(proxy_port)
89
- 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
90
115
  except PyngrokError as retry_exc:
91
116
  raise RuntimeError(
92
117
  f"Failed to start ngrok tunnel after disconnect retry: {retry_exc}"
@@ -113,7 +138,9 @@ def start_tunnel(proxy_port: int = 8000) -> tuple[str, str]:
113
138
 
114
139
  try:
115
140
  ngrok_url = _start_ngrok_tunnel(proxy_port)
116
- 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.")
117
144
  except RuntimeError as ngrok_error:
118
145
  raise RuntimeError(
119
146
  "No tunnel available.\n"
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.3.4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlinker"
7
- version = "1.3.4"
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" }
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