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.
- {devlinker-1.3.8 → devlinker-1.4.0}/PKG-INFO +14 -1
- devlinker-1.3.8/devlinker.egg-info/PKG-INFO → devlinker-1.4.0/README.md +12 -18
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/devlinker_loader_instant.html +20 -3
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/devlinker_loader_snippet.html +42 -1
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/main.py +101 -6
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/proxy.py +66 -12
- devlinker-1.3.8/README.md → devlinker-1.4.0/devlinker.egg-info/PKG-INFO +31 -0
- devlinker-1.4.0/devlinker.egg-info/entry_points.txt +2 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/requires.txt +1 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/pyproject.toml +3 -2
- devlinker-1.3.8/devlinker.egg-info/entry_points.txt +0 -2
- {devlinker-1.3.8 → devlinker-1.4.0}/LICENSE +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/MANIFEST.in +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/__init__.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/config.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detection_state.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detector.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/detector_ai.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/doctor.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/fix.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/fixer.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/global_state.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/inspect.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/logger.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/monitor.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/runner.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/share.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker/tunnel.py +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.3.8 → devlinker-1.4.0}/setup.cfg +0 -0
- {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
|
+
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://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
|
|
45
|
+
> [](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://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
|
|
26
|
+
> [](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:
|
|
67
|
-
user-select:
|
|
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">
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
211
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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://pay?pa=devlinker@upi&pn=DevLinker&cu=INR&tn=Support%20DevLinker%20Project%20🚀)
|
|
45
|
+
> [](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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devlinker"
|
|
7
|
-
version = "1.
|
|
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:
|
|
27
|
+
devlinker = "devlinker.main:main"
|
|
27
28
|
|
|
28
29
|
[tool.setuptools.packages.find]
|
|
29
30
|
include = ["devlinker*"]
|
|
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
|
|
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
|