devlinker 1.3.7__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.7 → devlinker-1.4.0}/PKG-INFO +14 -1
- devlinker-1.3.7/devlinker.egg-info/PKG-INFO → devlinker-1.4.0/README.md +12 -18
- devlinker-1.4.0/devlinker/devlinker_loader_instant.html +126 -0
- devlinker-1.4.0/devlinker/devlinker_loader_snippet.html +209 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/main.py +101 -6
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/proxy.py +116 -20
- devlinker-1.3.7/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.7 → devlinker-1.4.0}/devlinker.egg-info/requires.txt +1 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/pyproject.toml +3 -2
- devlinker-1.3.7/devlinker/devlinker_loader_instant.html +0 -100
- devlinker-1.3.7/devlinker/devlinker_loader_snippet.html +0 -128
- devlinker-1.3.7/devlinker.egg-info/entry_points.txt +0 -2
- {devlinker-1.3.7 → devlinker-1.4.0}/LICENSE +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/MANIFEST.in +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/__init__.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/config.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/detection_state.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/detector.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/detector_ai.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/doctor.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/fix.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/fixer.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/global_state.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/inspect.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/logger.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/monitor.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/runner.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/share.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker/tunnel.py +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.3.7 → devlinker-1.4.0}/setup.cfg +0 -0
- {devlinker-1.3.7 → 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.7
|
|
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
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- DevLinker Instant Loader for Local/WLAN Users -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>DevLinker Loading</title>
|
|
8
|
+
<style>
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
background: #0b1220;
|
|
14
|
+
font-family: "Segoe UI", Arial, sans-serif;
|
|
15
|
+
color: #e2e8f0;
|
|
16
|
+
}
|
|
17
|
+
#devlinker-loader-min {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: 99999;
|
|
21
|
+
display: grid;
|
|
22
|
+
place-items: center;
|
|
23
|
+
padding: 24px;
|
|
24
|
+
}
|
|
25
|
+
.devlinker-center {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 10px;
|
|
30
|
+
}
|
|
31
|
+
.devlinker-logo {
|
|
32
|
+
font-size: 1.15rem;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
letter-spacing: 0.01em;
|
|
35
|
+
margin-bottom: 4px;
|
|
36
|
+
color: #f8fafc;
|
|
37
|
+
}
|
|
38
|
+
.devlinker-spinner {
|
|
39
|
+
width: 40px;
|
|
40
|
+
height: 40px;
|
|
41
|
+
border: 4px solid rgba(148, 163, 184, 0.25);
|
|
42
|
+
border-top: 4px solid #22d3ee;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
animation: devlinker-spin 1s linear infinite;
|
|
45
|
+
}
|
|
46
|
+
.devlinker-text {
|
|
47
|
+
font-size: 1rem;
|
|
48
|
+
color: #cbd5e1;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
.devlinker-health {
|
|
52
|
+
font-size: 0.84rem;
|
|
53
|
+
color: #94a3b8;
|
|
54
|
+
opacity: 0.9;
|
|
55
|
+
}
|
|
56
|
+
.devlinker-powered {
|
|
57
|
+
position: fixed;
|
|
58
|
+
bottom: 18px;
|
|
59
|
+
left: 0;
|
|
60
|
+
width: 100vw;
|
|
61
|
+
text-align: center;
|
|
62
|
+
color: #94a3b8;
|
|
63
|
+
font-size: 0.92rem;
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
opacity: 0.9;
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
@keyframes devlinker-spin {
|
|
84
|
+
to { transform: rotate(360deg); }
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<div id="devlinker-loader-min">
|
|
90
|
+
<div class="devlinker-center">
|
|
91
|
+
<div class="devlinker-logo">DevLinker</div>
|
|
92
|
+
<div class="devlinker-spinner"></div>
|
|
93
|
+
<div class="devlinker-text">Loading your app...</div>
|
|
94
|
+
<div class="devlinker-health">Frontend connected • Backend connected</div>
|
|
95
|
+
</div>
|
|
96
|
+
</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>
|
|
101
|
+
<script>
|
|
102
|
+
// Fetch the real page and replace the loader with a short minimum display for smoothness.
|
|
103
|
+
(async function() {
|
|
104
|
+
const minTime = 500;
|
|
105
|
+
const start = Date.now();
|
|
106
|
+
try {
|
|
107
|
+
const resp = await fetch(window.location.href, { headers: { "X-DevLinker-Instant": "1" } });
|
|
108
|
+
const html = await resp.text();
|
|
109
|
+
const elapsed = Date.now() - start;
|
|
110
|
+
const delay = Math.max(0, minTime - elapsed);
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
document.open();
|
|
113
|
+
document.write(html);
|
|
114
|
+
document.close();
|
|
115
|
+
}, delay);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Show error if fetch fails
|
|
118
|
+
var loader = document.getElementById("devlinker-loader-min");
|
|
119
|
+
if (loader) {
|
|
120
|
+
loader.innerHTML += '<div style="color:#b91c1c;font-size:1.05rem;margin-top:1.5rem;">Failed to load app. Please check your connection.</div>';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!-- DevLinker Loader Overlay -->
|
|
2
|
+
<style>
|
|
3
|
+
#devlinker-loader {
|
|
4
|
+
position: fixed;
|
|
5
|
+
z-index: 99999;
|
|
6
|
+
inset: 0;
|
|
7
|
+
display: grid;
|
|
8
|
+
place-items: center;
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
background:
|
|
11
|
+
radial-gradient(700px 360px at 18% 12%, rgba(34, 211, 238, 0.18), transparent 60%),
|
|
12
|
+
radial-gradient(520px 280px at 82% 18%, rgba(59, 130, 246, 0.16), transparent 58%),
|
|
13
|
+
#0f172a;
|
|
14
|
+
font-family: "Segoe UI", Arial, sans-serif;
|
|
15
|
+
transition: opacity 0.4s cubic-bezier(.4,0,.2,1);
|
|
16
|
+
color: #e2e8f0;
|
|
17
|
+
}
|
|
18
|
+
.devlinker-panel {
|
|
19
|
+
width: min(640px, 92vw);
|
|
20
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
21
|
+
border-radius: 18px;
|
|
22
|
+
background: rgba(15, 23, 42, 0.75);
|
|
23
|
+
box-shadow: 0 24px 70px rgba(2, 6, 23, 0.55);
|
|
24
|
+
backdrop-filter: blur(8px);
|
|
25
|
+
padding: 24px 22px;
|
|
26
|
+
}
|
|
27
|
+
.devlinker-top {
|
|
28
|
+
font-weight: 700;
|
|
29
|
+
font-size: 1.2rem;
|
|
30
|
+
color: #f8fafc;
|
|
31
|
+
margin-bottom: 12px;
|
|
32
|
+
}
|
|
33
|
+
.devlinker-title {
|
|
34
|
+
font-size: 1.15rem;
|
|
35
|
+
font-weight: 650;
|
|
36
|
+
margin-bottom: 8px;
|
|
37
|
+
color: #f8fafc;
|
|
38
|
+
}
|
|
39
|
+
.devlinker-stage {
|
|
40
|
+
min-height: 1.45rem;
|
|
41
|
+
font-size: 0.98rem;
|
|
42
|
+
color: #a5b4fc;
|
|
43
|
+
margin-bottom: 14px;
|
|
44
|
+
}
|
|
45
|
+
.devlinker-center {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 10px;
|
|
50
|
+
padding: 14px 0 16px;
|
|
51
|
+
}
|
|
52
|
+
.devlinker-spinner {
|
|
53
|
+
width: 52px;
|
|
54
|
+
height: 52px;
|
|
55
|
+
border: 4px solid rgba(148, 163, 184, 0.25);
|
|
56
|
+
border-top: 4px solid #22d3ee;
|
|
57
|
+
border-right: 4px solid #60a5fa;
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
animation: devlinker-spin 1s linear infinite;
|
|
60
|
+
}
|
|
61
|
+
.devlinker-text {
|
|
62
|
+
font-size: 1rem;
|
|
63
|
+
color: #cbd5e1;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
}
|
|
66
|
+
.devlinker-divider {
|
|
67
|
+
height: 1px;
|
|
68
|
+
width: 100%;
|
|
69
|
+
background: rgba(148, 163, 184, 0.22);
|
|
70
|
+
margin: 8px 0 14px;
|
|
71
|
+
}
|
|
72
|
+
.devlinker-info {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: 1fr;
|
|
75
|
+
gap: 10px;
|
|
76
|
+
color: #cbd5e1;
|
|
77
|
+
font-size: 0.92rem;
|
|
78
|
+
}
|
|
79
|
+
.devlinker-tip {
|
|
80
|
+
color: #cbd5e1;
|
|
81
|
+
line-height: 1.4;
|
|
82
|
+
}
|
|
83
|
+
.devlinker-upgrade {
|
|
84
|
+
color: #93c5fd;
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
display: inline-block;
|
|
88
|
+
margin-top: 2px;
|
|
89
|
+
}
|
|
90
|
+
.devlinker-upgrade:hover {
|
|
91
|
+
color: #bfdbfe;
|
|
92
|
+
}
|
|
93
|
+
.devlinker-powered {
|
|
94
|
+
margin-top: 14px;
|
|
95
|
+
text-align: center;
|
|
96
|
+
color: #94a3b8;
|
|
97
|
+
font-size: 0.9rem;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
opacity: 0.95;
|
|
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
|
+
}
|
|
128
|
+
@keyframes devlinker-spin {
|
|
129
|
+
to { transform: rotate(360deg); }
|
|
130
|
+
}
|
|
131
|
+
@media (max-width: 640px) {
|
|
132
|
+
.devlinker-panel {
|
|
133
|
+
padding: 18px 16px;
|
|
134
|
+
}
|
|
135
|
+
.devlinker-title {
|
|
136
|
+
font-size: 1.04rem;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
<div id="devlinker-loader">
|
|
141
|
+
<div class="devlinker-panel">
|
|
142
|
+
<div class="devlinker-top">DevLinker</div>
|
|
143
|
+
<div class="devlinker-title">Connecting your app...</div>
|
|
144
|
+
<div class="devlinker-stage" id="devlinker-stage">Establishing secure tunnel...</div>
|
|
145
|
+
<div class="devlinker-center">
|
|
146
|
+
<div class="devlinker-spinner"></div>
|
|
147
|
+
<div class="devlinker-text">Preparing full experience</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="devlinker-divider"></div>
|
|
150
|
+
<div class="devlinker-info">
|
|
151
|
+
<div class="devlinker-tip">Tip: Share your app instantly with one link. No CORS issues, no port confusion.</div>
|
|
152
|
+
<div class="devlinker-tip">Go Pro: custom domains, faster sharing, and no branding.</div>
|
|
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>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="devlinker-powered">Powered by DevLinker 🚀</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<script>
|
|
163
|
+
const stageMessages = [
|
|
164
|
+
"Establishing secure tunnel...",
|
|
165
|
+
"Routing frontend assets...",
|
|
166
|
+
"Connecting backend services...",
|
|
167
|
+
"Optimizing connection...",
|
|
168
|
+
"Almost ready..."
|
|
169
|
+
];
|
|
170
|
+
let stageIndex = 0;
|
|
171
|
+
const stageElement = document.getElementById("devlinker-stage");
|
|
172
|
+
const stageTimer = setInterval(() => {
|
|
173
|
+
if (!stageElement) return;
|
|
174
|
+
stageIndex = (stageIndex + 1) % stageMessages.length;
|
|
175
|
+
stageElement.textContent = stageMessages[stageIndex];
|
|
176
|
+
}, 1000);
|
|
177
|
+
|
|
178
|
+
const start = Date.now();
|
|
179
|
+
const MIN_TIME = 800;
|
|
180
|
+
const MAX_TIME = 5000;
|
|
181
|
+
let hidden = false;
|
|
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
|
+
|
|
193
|
+
function hideLoader() {
|
|
194
|
+
if (hidden) return;
|
|
195
|
+
hidden = true;
|
|
196
|
+
clearInterval(stageTimer);
|
|
197
|
+
const elapsed = Date.now() - start;
|
|
198
|
+
const delay = Math.max(0, MIN_TIME - elapsed);
|
|
199
|
+
const el = document.getElementById("devlinker-loader");
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
if (el) {
|
|
202
|
+
el.style.opacity = "0";
|
|
203
|
+
setTimeout(() => el.remove(), 400);
|
|
204
|
+
}
|
|
205
|
+
}, delay);
|
|
206
|
+
}
|
|
207
|
+
window.addEventListener("load", hideLoader);
|
|
208
|
+
setTimeout(hideLoader, MAX_TIME);
|
|
209
|
+
</script>
|
|
@@ -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
|
|
@@ -142,12 +163,17 @@ def _build_target_ws_url(port: int, path: str, query: str) -> str:
|
|
|
142
163
|
|
|
143
164
|
|
|
144
165
|
async def _forward_http(request: Request) -> Response:
|
|
145
|
-
# Serve instant loader for
|
|
166
|
+
# Serve instant loader only for localhost HTML document navigations.
|
|
146
167
|
client_ip = request.client.host if request.client else None
|
|
147
|
-
|
|
168
|
+
host_header = request.headers.get("host", "").lower()
|
|
169
|
+
|
|
170
|
+
def is_localhost_ip(ip):
|
|
148
171
|
if not ip:
|
|
149
172
|
return False
|
|
150
|
-
|
|
173
|
+
return ip.startswith("127.") or ip == "localhost" or ip == "::1"
|
|
174
|
+
|
|
175
|
+
def is_lan_ip(ip):
|
|
176
|
+
if not ip:
|
|
151
177
|
return False
|
|
152
178
|
if ip.startswith("192.168.") or ip.startswith("10."):
|
|
153
179
|
return True
|
|
@@ -158,15 +184,67 @@ async def _forward_http(request: Request) -> Response:
|
|
|
158
184
|
except Exception:
|
|
159
185
|
return False
|
|
160
186
|
return False
|
|
161
|
-
|
|
187
|
+
|
|
188
|
+
def classify_mode(host: str, ip: str | None) -> str:
|
|
189
|
+
host_only = host.split(":", 1)[0] if host else ""
|
|
190
|
+
if host_only in ("localhost", "127.0.0.1", "::1"):
|
|
191
|
+
return "localhost"
|
|
192
|
+
if host_only.startswith("192.168.") or host_only.startswith("10."):
|
|
193
|
+
return "lan"
|
|
194
|
+
if host_only.startswith("172."):
|
|
195
|
+
try:
|
|
196
|
+
second = int(host_only.split(".")[1])
|
|
197
|
+
if 16 <= second <= 31:
|
|
198
|
+
return "lan"
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
if host_only and host_only not in ("", "0.0.0.0"):
|
|
202
|
+
return "public"
|
|
203
|
+
if is_localhost_ip(ip):
|
|
204
|
+
return "localhost"
|
|
205
|
+
if is_lan_ip(ip):
|
|
206
|
+
return "lan"
|
|
207
|
+
return "public" if ip else "unknown"
|
|
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
|
+
|
|
218
|
+
mode = classify_mode(host_header, client_ip)
|
|
219
|
+
is_localhost = mode == "localhost"
|
|
220
|
+
is_lan = mode == "lan"
|
|
221
|
+
is_public = mode == "public"
|
|
222
|
+
is_secure = _is_secure_request(request, host_header)
|
|
162
223
|
is_instant = request.headers.get("x-devlinker-instant") == "1"
|
|
163
|
-
|
|
224
|
+
accept_header = request.headers.get("accept", "")
|
|
225
|
+
sec_fetch_dest = request.headers.get("sec-fetch-dest", "")
|
|
226
|
+
is_html_request = "text/html" in accept_header.lower()
|
|
227
|
+
is_document_navigation = sec_fetch_dest in ("", "document")
|
|
228
|
+
is_api_path = request.url.path == "/api" or request.url.path.startswith("/api/")
|
|
229
|
+
if (
|
|
230
|
+
is_localhost
|
|
231
|
+
and not is_instant
|
|
232
|
+
and request.method == "GET"
|
|
233
|
+
and is_html_request
|
|
234
|
+
and is_document_navigation
|
|
235
|
+
and not is_api_path
|
|
236
|
+
):
|
|
164
237
|
import os
|
|
165
238
|
loader_path = os.path.join(os.path.dirname(__file__), "devlinker_loader_instant.html")
|
|
166
239
|
with open(loader_path, encoding="utf-8") as f:
|
|
167
240
|
loader_html = f.read()
|
|
168
|
-
return Response(
|
|
169
|
-
|
|
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
|
|
170
248
|
from devlinker.detector_ai import DevLinkerAI
|
|
171
249
|
|
|
172
250
|
inspector = RequestInspector()
|
|
@@ -177,16 +255,21 @@ async def _forward_http(request: Request) -> Response:
|
|
|
177
255
|
if request.url.path.startswith("/api"):
|
|
178
256
|
status = 503
|
|
179
257
|
warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
|
|
258
|
+
context = _format_request_context(request.url.path, request.method, status, "backend")
|
|
180
259
|
for w in warnings:
|
|
181
260
|
if "/api" in w:
|
|
182
|
-
print_warning(
|
|
261
|
+
print_warning(
|
|
262
|
+
f"API routing issue detected | {context}\n"
|
|
263
|
+
f"Fix: Try /api{request.url.path}"
|
|
264
|
+
)
|
|
183
265
|
else:
|
|
184
|
-
print_warning(w)
|
|
266
|
+
print_warning(f"{w} | {context}")
|
|
185
267
|
return PlainTextResponse("Backend is not configured.", status_code=status)
|
|
186
268
|
status = 503
|
|
187
269
|
warnings = inspector.analyze(request.url.path, status, "frontend", method=request.method)
|
|
270
|
+
context = _format_request_context(request.url.path, request.method, status, "frontend")
|
|
188
271
|
for w in warnings:
|
|
189
|
-
print_warning(w)
|
|
272
|
+
print_warning(f"{w} | {context}")
|
|
190
273
|
return PlainTextResponse("Frontend is not configured.", status_code=status)
|
|
191
274
|
|
|
192
275
|
if HTTP_CLIENT is None:
|
|
@@ -207,11 +290,12 @@ async def _forward_http(request: Request) -> Response:
|
|
|
207
290
|
except httpx.RequestError as exc:
|
|
208
291
|
status = 502
|
|
209
292
|
warnings = inspector.analyze(request.url.path, status, "backend", method=request.method)
|
|
293
|
+
context = _format_request_context(request.url.path, request.method, status, "backend")
|
|
210
294
|
for w in warnings:
|
|
211
|
-
print_warning(w)
|
|
295
|
+
print_warning(f"{w} | {context}")
|
|
212
296
|
ai_suggestions = ai.analyze_failure(str(exc))
|
|
213
297
|
for s in ai_suggestions:
|
|
214
|
-
|
|
298
|
+
_print_fix_once(f"{s} | {context}")
|
|
215
299
|
return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=status)
|
|
216
300
|
|
|
217
301
|
# Analyze response for warnings and fixes
|
|
@@ -222,22 +306,26 @@ async def _forward_http(request: Request) -> Response:
|
|
|
222
306
|
method=request.method,
|
|
223
307
|
response_text=upstream.text
|
|
224
308
|
)
|
|
309
|
+
context = _format_request_context(request.url.path, request.method, upstream.status_code, "backend")
|
|
225
310
|
for w in warnings:
|
|
226
311
|
if "/api" in w:
|
|
227
|
-
print_warning(
|
|
312
|
+
print_warning(
|
|
313
|
+
f"API routing issue detected | {context}\n"
|
|
314
|
+
f"Fix: Try /api{request.url.path}"
|
|
315
|
+
)
|
|
228
316
|
else:
|
|
229
|
-
print_warning(w)
|
|
317
|
+
print_warning(f"{w} | {context}")
|
|
230
318
|
ai_suggestions = ai.analyze_failure(str(upstream.text))
|
|
231
319
|
for s in ai_suggestions:
|
|
232
|
-
|
|
320
|
+
_print_fix_once(f"{s} | {context}")
|
|
233
321
|
|
|
234
|
-
#
|
|
235
|
-
headers = _filter_response_headers(dict(upstream.headers))
|
|
322
|
+
# Inject loader overlay only for LAN/WiFi/public HTML responses.
|
|
323
|
+
headers = _apply_security_headers(_filter_response_headers(dict(upstream.headers)))
|
|
236
324
|
content_type = headers.get("content-type", "")
|
|
237
325
|
is_html = "text/html" in content_type
|
|
238
|
-
is_public = client_ip and not is_local and (not client_ip.startswith("127.") and client_ip != "localhost" and client_ip != "::1")
|
|
239
326
|
content = upstream.content
|
|
240
|
-
if
|
|
327
|
+
# Only inject loader if NOT an instant loader background fetch
|
|
328
|
+
if is_html and (is_lan or is_public) and not is_instant:
|
|
241
329
|
try:
|
|
242
330
|
html = content.decode(upstream.encoding or "utf-8", errors="replace")
|
|
243
331
|
# Only inject if </body> exists
|
|
@@ -246,7 +334,15 @@ async def _forward_http(request: Request) -> Response:
|
|
|
246
334
|
loader_file = "devlinker_loader_snippet.html"
|
|
247
335
|
with open(os.path.join(os.path.dirname(__file__), loader_file), encoding="utf-8") as f:
|
|
248
336
|
loader = f.read()
|
|
249
|
-
|
|
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>")
|
|
250
346
|
content = html.encode(upstream.encoding or "utf-8")
|
|
251
347
|
except Exception:
|
|
252
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*"]
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
<!-- DevLinker Instant Loader for Local/LAN Users -->
|
|
2
|
-
<!DOCTYPE html>
|
|
3
|
-
<html lang="en">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="UTF-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
-
<title>Loading...</title>
|
|
8
|
-
<style>
|
|
9
|
-
#devlinker-loader-min {
|
|
10
|
-
position: fixed;
|
|
11
|
-
inset: 0;
|
|
12
|
-
z-index: 99999;
|
|
13
|
-
display: flex;
|
|
14
|
-
flex-direction: column;
|
|
15
|
-
justify-content: center;
|
|
16
|
-
align-items: center;
|
|
17
|
-
background: rgba(255,255,255,0.92);
|
|
18
|
-
min-height: 100vh;
|
|
19
|
-
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
|
|
20
|
-
}
|
|
21
|
-
.devlinker-spinner-min {
|
|
22
|
-
width: 38px;
|
|
23
|
-
height: 38px;
|
|
24
|
-
border: 4px solid #e0e7ef;
|
|
25
|
-
border-top: 4px solid #2563eb;
|
|
26
|
-
border-radius: 50%;
|
|
27
|
-
animation: spin 1s linear infinite;
|
|
28
|
-
margin-bottom: 2.5rem;
|
|
29
|
-
}
|
|
30
|
-
@keyframes spin {
|
|
31
|
-
100% { transform: rotate(360deg); }
|
|
32
|
-
}
|
|
33
|
-
.devlinker-powered {
|
|
34
|
-
position: fixed;
|
|
35
|
-
bottom: 18px;
|
|
36
|
-
left: 0;
|
|
37
|
-
width: 100vw;
|
|
38
|
-
text-align: center;
|
|
39
|
-
color: #2563eb;
|
|
40
|
-
font-size: 1.08rem;
|
|
41
|
-
font-weight: 500;
|
|
42
|
-
opacity: 0.85;
|
|
43
|
-
letter-spacing: 0.01em;
|
|
44
|
-
pointer-events: none;
|
|
45
|
-
user-select: none;
|
|
46
|
-
z-index: 100000;
|
|
47
|
-
}
|
|
48
|
-
.devlinker-loading-msg {
|
|
49
|
-
font-size: 1.18rem;
|
|
50
|
-
color: #222;
|
|
51
|
-
font-weight: 500;
|
|
52
|
-
margin-top: -1.5rem;
|
|
53
|
-
margin-bottom: 2.2rem;
|
|
54
|
-
letter-spacing: 0.01em;
|
|
55
|
-
text-align: center;
|
|
56
|
-
opacity: 0.92;
|
|
57
|
-
}
|
|
58
|
-
.devlinker-3wev {
|
|
59
|
-
font-size: 1.05rem;
|
|
60
|
-
color: #2563eb;
|
|
61
|
-
font-weight: 600;
|
|
62
|
-
margin-top: 0.5rem;
|
|
63
|
-
letter-spacing: 0.04em;
|
|
64
|
-
text-align: center;
|
|
65
|
-
opacity: 0.93;
|
|
66
|
-
}
|
|
67
|
-
</style>
|
|
68
|
-
</head>
|
|
69
|
-
<body>
|
|
70
|
-
<div id="devlinker-loader-min">
|
|
71
|
-
<div class="devlinker-spinner-min"></div>
|
|
72
|
-
<div class="devlinker-loading-msg">Loading your app…</div>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="devlinker-powered">Powered by DevLinker</div>
|
|
75
|
-
<script>
|
|
76
|
-
// Fetch the real page and replace the loader, but show loader for at least 1s
|
|
77
|
-
(async function() {
|
|
78
|
-
const minTime = 1000;
|
|
79
|
-
const start = Date.now();
|
|
80
|
-
try {
|
|
81
|
-
const resp = await fetch(window.location.href, { headers: { 'X-DevLinker-Instant': '1' } });
|
|
82
|
-
const html = await resp.text();
|
|
83
|
-
const elapsed = Date.now() - start;
|
|
84
|
-
const delay = Math.max(0, minTime - elapsed);
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
document.open();
|
|
87
|
-
document.write(html);
|
|
88
|
-
document.close();
|
|
89
|
-
}, delay);
|
|
90
|
-
} catch (e) {
|
|
91
|
-
// Show error if fetch fails
|
|
92
|
-
var loader = document.getElementById('devlinker-loader-min');
|
|
93
|
-
if (loader) {
|
|
94
|
-
loader.innerHTML += '<div style="color:#b91c1c;font-size:1.1rem;margin-top:1.5rem;">Failed to load app. Please check your connection.</div>';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
98
|
-
</script>
|
|
99
|
-
</body>
|
|
100
|
-
</html>
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
<!-- DevLinker Next-Level Loader Overlay -->
|
|
2
|
-
<style>
|
|
3
|
-
#devlinker-loader {
|
|
4
|
-
position: fixed;
|
|
5
|
-
z-index: 99999;
|
|
6
|
-
inset: 0;
|
|
7
|
-
display: flex;
|
|
8
|
-
align-items: center;
|
|
9
|
-
justify-content: center;
|
|
10
|
-
min-height: 100vh;
|
|
11
|
-
background: linear-gradient(135deg, #f1f5f9 0%, #e0e7ff 100%);
|
|
12
|
-
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
|
|
13
|
-
transition: opacity 0.4s cubic-bezier(.4,0,.2,1);
|
|
14
|
-
}
|
|
15
|
-
.devlinker-loader-card {
|
|
16
|
-
background: #fff;
|
|
17
|
-
border-radius: 20px;
|
|
18
|
-
box-shadow: 0 15px 40px rgba(0,0,0,0.08);
|
|
19
|
-
padding: 40px 30px;
|
|
20
|
-
min-width: 260px;
|
|
21
|
-
max-width: 94vw;
|
|
22
|
-
text-align: center;
|
|
23
|
-
border: 1.5px solid #e0e7ef;
|
|
24
|
-
position: relative;
|
|
25
|
-
width: 100%;
|
|
26
|
-
max-width: 350px;
|
|
27
|
-
}
|
|
28
|
-
.devlinker-logo {
|
|
29
|
-
font-size: 2.1rem;
|
|
30
|
-
font-weight: 600;
|
|
31
|
-
color: #2563eb;
|
|
32
|
-
margin-bottom: 20px;
|
|
33
|
-
letter-spacing: -1.2px;
|
|
34
|
-
user-select: none;
|
|
35
|
-
}
|
|
36
|
-
.devlinker-spin {
|
|
37
|
-
margin: 20px auto;
|
|
38
|
-
width: 40px;
|
|
39
|
-
height: 40px;
|
|
40
|
-
display: flex;
|
|
41
|
-
align-items: center;
|
|
42
|
-
justify-content: center;
|
|
43
|
-
}
|
|
44
|
-
.devlinker-spinner {
|
|
45
|
-
width: 40px;
|
|
46
|
-
height: 40px;
|
|
47
|
-
border-radius: 50%;
|
|
48
|
-
border: 4px solid #e0e7ef;
|
|
49
|
-
border-top: 4px solid #3b82f6;
|
|
50
|
-
animation: devlinker-spin 1.1s linear infinite;
|
|
51
|
-
}
|
|
52
|
-
@keyframes devlinker-spin {
|
|
53
|
-
100% { transform: rotate(360deg); }
|
|
54
|
-
}
|
|
55
|
-
.devlinker-brand {
|
|
56
|
-
font-size: 1.13rem;
|
|
57
|
-
color: #222;
|
|
58
|
-
font-weight: 600;
|
|
59
|
-
margin-bottom: 0.2rem;
|
|
60
|
-
letter-spacing: -0.5px;
|
|
61
|
-
}
|
|
62
|
-
.devlinker-desc {
|
|
63
|
-
font-size: 1.01rem;
|
|
64
|
-
color: #555;
|
|
65
|
-
margin-bottom: 0.7rem;
|
|
66
|
-
}
|
|
67
|
-
.devlinker-upgrade-btn {
|
|
68
|
-
margin-top: 15px;
|
|
69
|
-
background: #2563eb;
|
|
70
|
-
color: #fff;
|
|
71
|
-
border: none;
|
|
72
|
-
padding: 8px 16px;
|
|
73
|
-
border-radius: 8px;
|
|
74
|
-
font-size: 14px;
|
|
75
|
-
font-weight: 500;
|
|
76
|
-
cursor: pointer;
|
|
77
|
-
box-shadow: 0 2px 8px rgba(37,99,235,0.08);
|
|
78
|
-
transition: background 0.2s, box-shadow 0.2s;
|
|
79
|
-
outline: none;
|
|
80
|
-
display: inline-block;
|
|
81
|
-
}
|
|
82
|
-
.devlinker-upgrade-btn:hover {
|
|
83
|
-
background: #1d4ed8;
|
|
84
|
-
box-shadow: 0 4px 16px rgba(37,99,235,0.13);
|
|
85
|
-
}
|
|
86
|
-
@media (max-width: 600px) {
|
|
87
|
-
.devlinker-loader-card {
|
|
88
|
-
min-width: 0;
|
|
89
|
-
padding: 1.2rem 0.5rem 1.1rem 0.5rem;
|
|
90
|
-
border-radius: 12px;
|
|
91
|
-
}
|
|
92
|
-
.devlinker-logo {
|
|
93
|
-
font-size: 1.3rem;
|
|
94
|
-
}
|
|
95
|
-
.devlinker-upgrade-btn {
|
|
96
|
-
font-size: 0.97rem;
|
|
97
|
-
padding: 8px 12px;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
</style>
|
|
101
|
-
<div id="devlinker-loader">
|
|
102
|
-
<div class="devlinker-loader-card">
|
|
103
|
-
<div class="devlinker-logo">∞ DevLinker</div>
|
|
104
|
-
<div class="devlinker-spin"><div class="devlinker-spinner"></div></div>
|
|
105
|
-
<div class="devlinker-brand">Loading your app...</div>
|
|
106
|
-
<div class="devlinker-desc">Powered by <span style="color:#2563eb;font-weight:700;">DevLinker</span> 🚀</div>
|
|
107
|
-
<a href="https://devlinker.app" target="_blank" rel="noopener">
|
|
108
|
-
<button class="devlinker-upgrade-btn">Upgrade for custom domain</button>
|
|
109
|
-
</a>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
<script>
|
|
113
|
-
const start = Date.now();
|
|
114
|
-
function hideLoader() {
|
|
115
|
-
const minTime = 1000; // 1 sec branding (increased from 0.8s)
|
|
116
|
-
const elapsed = Date.now() - start;
|
|
117
|
-
const delay = Math.max(0, minTime - elapsed);
|
|
118
|
-
setTimeout(() => {
|
|
119
|
-
const el = document.getElementById("devlinker-loader");
|
|
120
|
-
if (el) {
|
|
121
|
-
el.style.opacity = "0";
|
|
122
|
-
setTimeout(() => el.remove(), 400);
|
|
123
|
-
}
|
|
124
|
-
}, delay);
|
|
125
|
-
}
|
|
126
|
-
window.addEventListener("load", hideLoader);
|
|
127
|
-
setTimeout(hideLoader, 7000); // max 7s (increased from 5s)
|
|
128
|
-
</script>
|
|
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
|