devlinker 1.3.0__tar.gz → 1.3.3__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.0 → devlinker-1.3.3}/PKG-INFO +85 -15
- {devlinker-1.3.0 → devlinker-1.3.3}/README.md +83 -13
- devlinker-1.3.3/devlinker/config.py +8 -0
- devlinker-1.3.3/devlinker/detection_state.py +58 -0
- devlinker-1.3.3/devlinker/detector_ai.py +18 -0
- devlinker-1.3.3/devlinker/doctor.py +27 -0
- devlinker-1.3.3/devlinker/fix.py +16 -0
- devlinker-1.3.3/devlinker/fixer.py +27 -0
- devlinker-1.3.3/devlinker/inspect.py +14 -0
- devlinker-1.3.3/devlinker/logger.py +5 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/main.py +89 -36
- devlinker-1.3.3/devlinker/monitor.py +19 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/proxy.py +65 -3
- devlinker-1.3.3/devlinker/share.py +54 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/PKG-INFO +85 -15
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/SOURCES.txt +10 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/pyproject.toml +2 -2
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/__init__.py +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/detector.py +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/runner.py +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/tunnel.py +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/setup.cfg +0 -0
- {devlinker-1.3.0 → devlinker-1.3.3}/setup.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.3.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.3.3
|
|
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
|
|
@@ -18,20 +18,36 @@ Requires-Dist: websockets
|
|
|
18
18
|
|
|
19
19
|
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.
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
-
- Auto
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
24
|
+
- 🚀 **Unified Dev Proxy:** Combines frontend (Vite/React) and backend (FastAPI/Flask/Node/Docker) into a single local and public URL.
|
|
25
|
+
- 🔍 **Auto Detection:** Detects frontend/backend ports, runtime, Docker containers, and Vite servers automatically.
|
|
26
|
+
- 🧠 **Smart Detection & Doctor:** Real-time request analysis, backend intelligence, log analyzer, and `devlinker doctor` for instant diagnostics.
|
|
27
|
+
- 🛡️ **Auto-Fix Engine:** `devlinker fix` applies safe fixes (like VITE_API_URL) and suggests code changes.
|
|
28
|
+
- 🌍 **Public Sharing:** Share your local dev environment instantly with `--url` (startup) or `devlinker share` (runtime, no restart).
|
|
29
|
+
- 🔄 **Dynamic Tunnel Control:** `devlinker unshare` disables public tunnel at runtime.
|
|
30
|
+
- 📡 **WLAN Sharing:** Prints LAN URL for same-network device access.
|
|
31
|
+
- 🧑💻 **Interactive CLI:** Modern, colorized, emoji-rich terminal UX for all commands.
|
|
32
|
+
- 🧩 **Zero Config:** Works out-of-the-box for most FastAPI, Flask, Vite, and Docker projects.
|
|
33
|
+
- 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
|
|
34
|
+
- 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
|
|
35
|
+
|
|
36
|
+
## CLI Commands & Options
|
|
37
|
+
|
|
38
|
+
- `devlinker` — Start proxy (local only, fast)
|
|
39
|
+
- `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
|
|
40
|
+
- `devlinker share` — Enable public tunnel at runtime (no restart)
|
|
41
|
+
- `devlinker unshare` — Disable public tunnel at runtime
|
|
42
|
+
- `devlinker doctor` — Diagnose issues, see categorized problems and fixes
|
|
43
|
+
- `devlinker fix` — Auto-fix common issues (env, API paths, config)
|
|
44
|
+
- `devlinker --frontend 5173 --backend 5000` — Override detected ports
|
|
45
|
+
- `devlinker --docker` — Auto-start Docker backend
|
|
46
|
+
- `devlinker --no-tunnel` — Force local-only mode
|
|
47
|
+
- `devlinker --no-lan` — Hide WLAN sharing URL
|
|
48
|
+
- `devlinker --interactive-backend` — Prompt to choose backend if multiple found
|
|
49
|
+
- `devlinker --proxy-port 18000` — Use custom proxy port
|
|
50
|
+
- `devlinker --version` — Show version
|
|
35
51
|
|
|
36
52
|
## Project Structure
|
|
37
53
|
|
|
@@ -123,17 +139,71 @@ Enable Docker auto-start explicitly:
|
|
|
123
139
|
devlinker --docker
|
|
124
140
|
```
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
|
|
143
|
+
## Tunnel and Sharing Modes
|
|
144
|
+
|
|
145
|
+
By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
devlinker --url
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In your terminal output, you'll see:
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
URL, run:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
devlinker --url
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
🌍 Enabling public tunnel...
|
|
165
|
+
[OK] Tunnel provider: Cloudflare
|
|
166
|
+
[OK] Public URL:
|
|
167
|
+
https://xxxx.trycloudflare.com
|
|
168
|
+
Tip: Press Ctrl+Click to open link
|
|
169
|
+
[INFO] Share this link with collaborators.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
To force tunnel off (even if --url is passed):
|
|
127
173
|
|
|
128
174
|
```bash
|
|
129
175
|
devlinker --no-tunnel
|
|
130
176
|
```
|
|
131
177
|
|
|
178
|
+
When running without `--url`, you’ll see:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
⚡ Skipping public tunnel (use --url to enable)
|
|
182
|
+
|
|
183
|
+
💡 Need to share outside network?
|
|
184
|
+
👉 Run: devlinker --url
|
|
185
|
+
```
|
|
186
|
+
|
|
132
187
|
Disable WLAN URL output:
|
|
133
188
|
|
|
134
189
|
```bash
|
|
135
190
|
devlinker --no-lan
|
|
136
191
|
```
|
|
192
|
+
## Smart Detection & Auto-Fix System
|
|
193
|
+
|
|
194
|
+
DevLinker now includes an AI-powered detection and auto-fix engine:
|
|
195
|
+
|
|
196
|
+
- **Request Inspector:** Real-time analysis of proxy traffic for common mistakes (missing `/api` prefix, 404s, CORS risks, upstream failures)
|
|
197
|
+
- **Backend Intelligence:** Probes backend endpoints and type at startup for smarter routing and hints
|
|
198
|
+
- **Log Analyzer:** Converts error messages (CORS, 404, connection refused) into human-readable explanations and actionable fixes
|
|
199
|
+
- **Smart Warning Engine:** Prints clean CLI warnings and suggestions, e.g.:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
⚠️ Detected direct backend call (localhost:5000)
|
|
203
|
+
👉 Use /api/* instead of direct URL
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
All detection and fixes are modular, async-compatible, and production-ready. See `devlinker/proxy.py`, `devlinker/detector_ai.py`, and `devlinker/logger.py` for implementation.
|
|
137
207
|
|
|
138
208
|
Interactive backend selection (when local and Docker are both detected):
|
|
139
209
|
|
|
@@ -2,20 +2,36 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
## Features
|
|
6
7
|
|
|
7
|
-
-
|
|
8
|
-
- Auto
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
8
|
+
- 🚀 **Unified Dev Proxy:** Combines frontend (Vite/React) and backend (FastAPI/Flask/Node/Docker) into a single local and public URL.
|
|
9
|
+
- 🔍 **Auto Detection:** Detects frontend/backend ports, runtime, Docker containers, and Vite servers automatically.
|
|
10
|
+
- 🧠 **Smart Detection & Doctor:** Real-time request analysis, backend intelligence, log analyzer, and `devlinker doctor` for instant diagnostics.
|
|
11
|
+
- 🛡️ **Auto-Fix Engine:** `devlinker fix` applies safe fixes (like VITE_API_URL) and suggests code changes.
|
|
12
|
+
- 🌍 **Public Sharing:** Share your local dev environment instantly with `--url` (startup) or `devlinker share` (runtime, no restart).
|
|
13
|
+
- 🔄 **Dynamic Tunnel Control:** `devlinker unshare` disables public tunnel at runtime.
|
|
14
|
+
- 📡 **WLAN Sharing:** Prints LAN URL for same-network device access.
|
|
15
|
+
- 🧑💻 **Interactive CLI:** Modern, colorized, emoji-rich terminal UX for all commands.
|
|
16
|
+
- 🧩 **Zero Config:** Works out-of-the-box for most FastAPI, Flask, Vite, and Docker projects.
|
|
17
|
+
- 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
|
|
18
|
+
- 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
|
|
19
|
+
|
|
20
|
+
## CLI Commands & Options
|
|
21
|
+
|
|
22
|
+
- `devlinker` — Start proxy (local only, fast)
|
|
23
|
+
- `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
|
|
24
|
+
- `devlinker share` — Enable public tunnel at runtime (no restart)
|
|
25
|
+
- `devlinker unshare` — Disable public tunnel at runtime
|
|
26
|
+
- `devlinker doctor` — Diagnose issues, see categorized problems and fixes
|
|
27
|
+
- `devlinker fix` — Auto-fix common issues (env, API paths, config)
|
|
28
|
+
- `devlinker --frontend 5173 --backend 5000` — Override detected ports
|
|
29
|
+
- `devlinker --docker` — Auto-start Docker backend
|
|
30
|
+
- `devlinker --no-tunnel` — Force local-only mode
|
|
31
|
+
- `devlinker --no-lan` — Hide WLAN sharing URL
|
|
32
|
+
- `devlinker --interactive-backend` — Prompt to choose backend if multiple found
|
|
33
|
+
- `devlinker --proxy-port 18000` — Use custom proxy port
|
|
34
|
+
- `devlinker --version` — Show version
|
|
19
35
|
|
|
20
36
|
## Project Structure
|
|
21
37
|
|
|
@@ -107,17 +123,71 @@ Enable Docker auto-start explicitly:
|
|
|
107
123
|
devlinker --docker
|
|
108
124
|
```
|
|
109
125
|
|
|
110
|
-
|
|
126
|
+
|
|
127
|
+
## Tunnel and Sharing Modes
|
|
128
|
+
|
|
129
|
+
By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
devlinker --url
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
In your terminal output, you'll see:
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
URL, run:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
devlinker --url
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
🌍 Enabling public tunnel...
|
|
149
|
+
[OK] Tunnel provider: Cloudflare
|
|
150
|
+
[OK] Public URL:
|
|
151
|
+
https://xxxx.trycloudflare.com
|
|
152
|
+
Tip: Press Ctrl+Click to open link
|
|
153
|
+
[INFO] Share this link with collaborators.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
To force tunnel off (even if --url is passed):
|
|
111
157
|
|
|
112
158
|
```bash
|
|
113
159
|
devlinker --no-tunnel
|
|
114
160
|
```
|
|
115
161
|
|
|
162
|
+
When running without `--url`, you’ll see:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
⚡ Skipping public tunnel (use --url to enable)
|
|
166
|
+
|
|
167
|
+
💡 Need to share outside network?
|
|
168
|
+
👉 Run: devlinker --url
|
|
169
|
+
```
|
|
170
|
+
|
|
116
171
|
Disable WLAN URL output:
|
|
117
172
|
|
|
118
173
|
```bash
|
|
119
174
|
devlinker --no-lan
|
|
120
175
|
```
|
|
176
|
+
## Smart Detection & Auto-Fix System
|
|
177
|
+
|
|
178
|
+
DevLinker now includes an AI-powered detection and auto-fix engine:
|
|
179
|
+
|
|
180
|
+
- **Request Inspector:** Real-time analysis of proxy traffic for common mistakes (missing `/api` prefix, 404s, CORS risks, upstream failures)
|
|
181
|
+
- **Backend Intelligence:** Probes backend endpoints and type at startup for smarter routing and hints
|
|
182
|
+
- **Log Analyzer:** Converts error messages (CORS, 404, connection refused) into human-readable explanations and actionable fixes
|
|
183
|
+
- **Smart Warning Engine:** Prints clean CLI warnings and suggestions, e.g.:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
⚠️ Detected direct backend call (localhost:5000)
|
|
187
|
+
👉 Use /api/* instead of direct URL
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
All detection and fixes are modular, async-compatible, and production-ready. See `devlinker/proxy.py`, `devlinker/detector_ai.py`, and `devlinker/logger.py` for implementation.
|
|
121
191
|
|
|
122
192
|
Interactive backend selection (when local and Docker are both detected):
|
|
123
193
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
class DetectionState:
|
|
3
|
+
def __init__(self):
|
|
4
|
+
self.issues = []
|
|
5
|
+
self.counts = {}
|
|
6
|
+
self.levels = {}
|
|
7
|
+
self.categories = {}
|
|
8
|
+
|
|
9
|
+
def add(self, issue, level="MEDIUM", category="general"):
|
|
10
|
+
self.issues.append(issue)
|
|
11
|
+
self.counts[issue] = self.counts.get(issue, 0) + 1
|
|
12
|
+
self.levels[issue] = level
|
|
13
|
+
self.categories.setdefault(category, []).append(issue)
|
|
14
|
+
|
|
15
|
+
def should_print(self, issue):
|
|
16
|
+
return self.counts.get(issue, 0) == 1
|
|
17
|
+
|
|
18
|
+
def get_issues(self):
|
|
19
|
+
return [(issue, self.levels.get(issue, "MEDIUM"), self.counts[issue], self._get_category(issue)) for issue in self.issues]
|
|
20
|
+
|
|
21
|
+
def summary(self):
|
|
22
|
+
summary = {}
|
|
23
|
+
for issue in self.issues:
|
|
24
|
+
level = self.levels.get(issue, "MEDIUM")
|
|
25
|
+
summary.setdefault(level, set()).add(issue)
|
|
26
|
+
return summary
|
|
27
|
+
|
|
28
|
+
def _get_category(self, issue):
|
|
29
|
+
for cat, issues in self.categories.items():
|
|
30
|
+
if issue in issues:
|
|
31
|
+
return cat
|
|
32
|
+
return "general"
|
|
33
|
+
|
|
34
|
+
def report(self):
|
|
35
|
+
print("\n🩺 DevLinker Doctor Report\n────────────────────────")
|
|
36
|
+
# Group by category
|
|
37
|
+
for category, issues in self.categories.items():
|
|
38
|
+
if not issues:
|
|
39
|
+
continue
|
|
40
|
+
if category == "network":
|
|
41
|
+
print("\n🌐 Network Issues")
|
|
42
|
+
elif category == "routing":
|
|
43
|
+
print("\n🔀 Routing Issues")
|
|
44
|
+
elif category == "cors":
|
|
45
|
+
print("\n🔐 CORS Issues")
|
|
46
|
+
else:
|
|
47
|
+
print(f"\n{category.title()} Issues")
|
|
48
|
+
for issue in set(issues):
|
|
49
|
+
level = self.levels.get(issue, "MEDIUM")
|
|
50
|
+
if level == "HIGH":
|
|
51
|
+
print(f"❌ {issue}")
|
|
52
|
+
elif level == "MEDIUM":
|
|
53
|
+
print(f"⚠️ {issue}")
|
|
54
|
+
else:
|
|
55
|
+
print(f"💡 {issue}")
|
|
56
|
+
|
|
57
|
+
# Singleton instance
|
|
58
|
+
state = DetectionState()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class DevLinkerAI:
|
|
2
|
+
def analyze_failure(self, error_text):
|
|
3
|
+
if "CORS" in error_text:
|
|
4
|
+
return [
|
|
5
|
+
"Frontend is calling backend directly",
|
|
6
|
+
"Use /api/* instead of localhost:PORT"
|
|
7
|
+
]
|
|
8
|
+
if "404" in error_text:
|
|
9
|
+
return [
|
|
10
|
+
"Route not found",
|
|
11
|
+
"Check if '/api' prefix is required"
|
|
12
|
+
]
|
|
13
|
+
if "connection refused" in error_text.lower():
|
|
14
|
+
return [
|
|
15
|
+
"Backend not reachable",
|
|
16
|
+
"Ensure backend is running"
|
|
17
|
+
]
|
|
18
|
+
return []
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.detection_state import state
|
|
3
|
+
from devlinker.detector_ai import DevLinkerAI
|
|
4
|
+
from devlinker.logger import print_fix
|
|
5
|
+
|
|
6
|
+
@click.command()
|
|
7
|
+
def doctor():
|
|
8
|
+
"""Run DevLinker diagnostics and print a health dashboard."""
|
|
9
|
+
ai = DevLinkerAI()
|
|
10
|
+
print("\n🩺 DevLinker Health Dashboard\n" + ("═" * 36))
|
|
11
|
+
# Grouped status summary
|
|
12
|
+
categories = state.categories
|
|
13
|
+
for category, issues in categories.items():
|
|
14
|
+
if not issues:
|
|
15
|
+
status = "✅"
|
|
16
|
+
else:
|
|
17
|
+
high = any(state.levels.get(issue, "MEDIUM") == "HIGH" for issue in issues)
|
|
18
|
+
warn = any(state.levels.get(issue, "MEDIUM") == "MEDIUM" for issue in issues)
|
|
19
|
+
status = "⚠️" if high or warn else "✅"
|
|
20
|
+
print(f"{category.title():<10}: {status}")
|
|
21
|
+
print("\nDetails:")
|
|
22
|
+
state.report()
|
|
23
|
+
print("\nFix Suggestions:")
|
|
24
|
+
for issue, level, count, category in state.get_issues():
|
|
25
|
+
suggestions = ai.analyze_failure(issue)
|
|
26
|
+
for s in suggestions:
|
|
27
|
+
print_fix(s)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.detection_state import state
|
|
3
|
+
from devlinker.fixer import DevLinkerFixer
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
def fix():
|
|
7
|
+
"""Apply auto-fixes for detected issues."""
|
|
8
|
+
issues = state.get_issues()
|
|
9
|
+
fixer = DevLinkerFixer()
|
|
10
|
+
print("\n🔧 Applying fixes...")
|
|
11
|
+
results = fixer.apply_fixes(issues)
|
|
12
|
+
print("\n🔧 Fix Results")
|
|
13
|
+
for r in results:
|
|
14
|
+
print(f"✔ {r}")
|
|
15
|
+
if not results:
|
|
16
|
+
print("No auto-fixes applied. All clear or manual review needed.")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class DevLinkerFixer:
|
|
4
|
+
def apply_fixes(self, issues):
|
|
5
|
+
fixes = []
|
|
6
|
+
for issue in issues:
|
|
7
|
+
desc = issue[0] if isinstance(issue, (list, tuple)) else issue.get("issue", "")
|
|
8
|
+
if "CORS" in desc:
|
|
9
|
+
fixes.append(self.fix_env())
|
|
10
|
+
if "Missing /api" in desc or "missing '/api'" in desc:
|
|
11
|
+
fixes.append(self.suggest_api_fix())
|
|
12
|
+
return fixes
|
|
13
|
+
|
|
14
|
+
def fix_env(self):
|
|
15
|
+
env_path = os.path.join("frontend", ".env")
|
|
16
|
+
line = "VITE_API_URL=http://localhost:8001"
|
|
17
|
+
# Only add if not already present
|
|
18
|
+
if os.path.exists(env_path):
|
|
19
|
+
with open(env_path, "r") as f:
|
|
20
|
+
if line in f.read():
|
|
21
|
+
return "VITE_API_URL already set in frontend/.env"
|
|
22
|
+
with open(env_path, "a") as f:
|
|
23
|
+
f.write(f"\n{line}\n")
|
|
24
|
+
return "Added VITE_API_URL to frontend/.env"
|
|
25
|
+
|
|
26
|
+
def suggest_api_fix(self):
|
|
27
|
+
return "Suggest: Replace hardcoded http://localhost:8000 with /api in frontend code (manual review)"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.proxy import _recent_requests
|
|
3
|
+
|
|
4
|
+
@click.command()
|
|
5
|
+
def inspect():
|
|
6
|
+
"""Show recent API calls and statuses."""
|
|
7
|
+
click.secho("\n🔍 Recent API Calls (last 50):\n" + ("═" * 36), fg="cyan", bold=True)
|
|
8
|
+
if not _recent_requests:
|
|
9
|
+
click.secho("No API calls recorded yet.", fg="yellow")
|
|
10
|
+
return
|
|
11
|
+
for req in _recent_requests[-50:]:
|
|
12
|
+
status = req["status"]
|
|
13
|
+
emoji = "✅" if status < 400 else ("⚠️" if status < 500 else "❌")
|
|
14
|
+
click.secho(f"{emoji} {req['target']:<8} {req['path']:<30} → {status}", fg="white")
|
|
@@ -11,6 +11,13 @@ from .detector import check_port, detect_ports, is_vite_port
|
|
|
11
11
|
from .proxy import start_proxy
|
|
12
12
|
from .runner import detect_backend_port, start_servers
|
|
13
13
|
from .tunnel import start_tunnel
|
|
14
|
+
from .doctor import doctor
|
|
15
|
+
from .fix import fix
|
|
16
|
+
|
|
17
|
+
from .share import share, unshare
|
|
18
|
+
from .config import load_config
|
|
19
|
+
from .inspect import inspect
|
|
20
|
+
from .monitor import monitor
|
|
14
21
|
|
|
15
22
|
|
|
16
23
|
def _is_port_in_use(port: int) -> bool:
|
|
@@ -50,6 +57,7 @@ def _with_ngrok_skip_warning(url: str) -> str:
|
|
|
50
57
|
return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
|
|
51
58
|
|
|
52
59
|
|
|
60
|
+
|
|
53
61
|
def _print_summary(
|
|
54
62
|
frontend_port: int,
|
|
55
63
|
backend_port: int,
|
|
@@ -58,20 +66,23 @@ def _print_summary(
|
|
|
58
66
|
wlan_url: str | None,
|
|
59
67
|
startup_seconds: float,
|
|
60
68
|
) -> None:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
import click
|
|
70
|
+
banner = "\n" + ("═" * 36) + "\n🚀 DevLinker Ready\n" + ("═" * 36)
|
|
71
|
+
click.secho(f"{banner}", fg="green", bold=True)
|
|
72
|
+
click.secho(f"⏱️ Startup time: {startup_seconds:.1f}s\n", fg="yellow")
|
|
73
|
+
click.secho(f"Frontend: ", nl=False, fg="blue"); click.secho(f"http://localhost:{frontend_port}", fg="cyan", bold=True)
|
|
74
|
+
click.secho(f"Backend: ", nl=False, fg="blue"); click.secho(f"http://localhost:{backend_port}", fg="cyan", bold=True)
|
|
75
|
+
click.secho("\nAccess Links:", fg="magenta", bold=True)
|
|
76
|
+
click.secho(f" Local → http://localhost:{proxy_port}", fg="white")
|
|
66
77
|
if wlan_url:
|
|
67
|
-
|
|
78
|
+
click.secho(f" WLAN → {wlan_url}", fg="white")
|
|
68
79
|
else:
|
|
69
|
-
|
|
80
|
+
click.secho(" WLAN → unavailable", fg="white")
|
|
70
81
|
if public_url:
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
click.secho(f" Public → {public_url}", fg="cyan", bold=True)
|
|
83
|
+
click.secho("Tip: Press Ctrl+Click to open link", fg="magenta")
|
|
73
84
|
else:
|
|
74
|
-
|
|
85
|
+
click.secho(" Public → Disabled (use --url)", fg="yellow")
|
|
75
86
|
|
|
76
87
|
|
|
77
88
|
def _get_local_ip() -> str | None:
|
|
@@ -124,6 +135,7 @@ def _wait_for_readiness(
|
|
|
124
135
|
is_flag=True,
|
|
125
136
|
help="Auto-start Docker backends (manual Docker is the default).",
|
|
126
137
|
)
|
|
138
|
+
@click.option("--url", is_flag=True, help="Enable public tunnel URL.")
|
|
127
139
|
@click.option("--no-tunnel", is_flag=True, help="Skip public tunnel and run local proxy only.")
|
|
128
140
|
@click.option(
|
|
129
141
|
"--interactive-backend/--no-interactive-backend",
|
|
@@ -139,20 +151,38 @@ def _wait_for_readiness(
|
|
|
139
151
|
help="Show WLAN sharing URL for devices on the same network.",
|
|
140
152
|
)
|
|
141
153
|
@click.option("--debug", is_flag=True, hidden=True, help="Enable debug logging.")
|
|
154
|
+
|
|
142
155
|
def cli(
|
|
143
156
|
frontend: int | None,
|
|
144
157
|
backend_port_override: int | None,
|
|
145
158
|
proxy_port: int,
|
|
146
159
|
auto_start_docker: bool,
|
|
160
|
+
url: bool,
|
|
147
161
|
no_tunnel: bool,
|
|
148
162
|
interactive_backend: bool,
|
|
149
163
|
lan_enabled: bool,
|
|
150
164
|
debug: bool,
|
|
151
165
|
) -> None:
|
|
166
|
+
# Load config file if present
|
|
167
|
+
config = load_config()
|
|
168
|
+
# Use config values as defaults if CLI args are not set
|
|
169
|
+
if frontend is None:
|
|
170
|
+
frontend = config.get("frontend")
|
|
171
|
+
if backend_port_override is None:
|
|
172
|
+
backend_port_override = config.get("backend")
|
|
173
|
+
if proxy_port == 8000 and config.get("proxy_port"):
|
|
174
|
+
proxy_port = config["proxy_port"]
|
|
175
|
+
if not url and config.get("tunnel") is True:
|
|
176
|
+
url = True
|
|
177
|
+
if config.get("api_prefix"):
|
|
178
|
+
# Optionally pass api_prefix to proxy if needed in future
|
|
179
|
+
pass
|
|
180
|
+
|
|
152
181
|
started = time.perf_counter()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
banner = "\n" + ("═" * 36) + f"\n⚡ Dev Linker v{__version__} ⚡\n" + ("═" * 36)
|
|
183
|
+
click.secho(banner, fg="green", bold=True)
|
|
184
|
+
click.secho("[INFO] Mode: Auto (FastAPI async proxy + Docker detection)", fg="blue")
|
|
185
|
+
click.secho("[INFO] Booting local services...", fg="blue")
|
|
156
186
|
|
|
157
187
|
start_servers(auto_start_docker=auto_start_docker)
|
|
158
188
|
|
|
@@ -165,7 +195,7 @@ def cli(
|
|
|
165
195
|
if backend_port is None:
|
|
166
196
|
raise SystemExit(1)
|
|
167
197
|
|
|
168
|
-
|
|
198
|
+
click.secho("[INFO] Detecting frontend/backend ports...", fg="blue")
|
|
169
199
|
frontend_port, backend_port = detect_ports(frontend=frontend, backend=backend_port)
|
|
170
200
|
|
|
171
201
|
if frontend_port is None:
|
|
@@ -190,10 +220,10 @@ def cli(
|
|
|
190
220
|
|
|
191
221
|
proxy_port = _select_proxy_port(proxy_port)
|
|
192
222
|
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
click.secho(f"[OK] Frontend → {frontend_port}", fg="green")
|
|
224
|
+
click.secho(f"[OK] Backend → {backend_port}\n", fg="green")
|
|
195
225
|
|
|
196
|
-
|
|
226
|
+
click.secho(f"[INFO] Starting proxy on :{proxy_port}...", fg="blue")
|
|
197
227
|
start_proxy(frontend_port, backend_port, proxy_port=proxy_port)
|
|
198
228
|
|
|
199
229
|
# Allow proxy thread to bind before opening tunnel.
|
|
@@ -204,32 +234,41 @@ def cli(
|
|
|
204
234
|
local_ip = _get_local_ip()
|
|
205
235
|
if local_ip:
|
|
206
236
|
wlan_url = f"http://{local_ip}:{proxy_port}"
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
click.secho(f"[OK] WLAN URL: {wlan_url}", fg="green")
|
|
238
|
+
click.secho("[INFO] Share WLAN link with teammates on same WiFi/LAN.", fg="blue")
|
|
209
239
|
else:
|
|
210
|
-
|
|
211
|
-
|
|
240
|
+
click.secho("[WARN] WLAN URL unavailable (no active LAN interface detected).", fg="yellow")
|
|
241
|
+
click.secho("[INFO] If LAN sharing fails, allow proxy port in firewall and use same network.", fg="yellow")
|
|
242
|
+
|
|
243
|
+
click.secho(f"\n[OK] Proxy ready at http://localhost:{proxy_port}\n", fg="green", bold=True)
|
|
212
244
|
|
|
213
|
-
print(f"\n[OK] Proxy ready at http://localhost:{proxy_port}\n")
|
|
214
245
|
warning_free_url: str | None = None
|
|
246
|
+
enable_tunnel = False
|
|
247
|
+
if url:
|
|
248
|
+
enable_tunnel = True
|
|
215
249
|
if no_tunnel:
|
|
216
|
-
|
|
217
|
-
|
|
250
|
+
enable_tunnel = False
|
|
251
|
+
|
|
252
|
+
if enable_tunnel:
|
|
218
253
|
try:
|
|
219
|
-
|
|
254
|
+
click.secho("\n🌍 Enabling public tunnel...", fg="green", bold=True)
|
|
220
255
|
provider, public_url = start_tunnel(proxy_port)
|
|
221
256
|
warning_free_url = _with_ngrok_skip_warning(public_url)
|
|
222
257
|
provider_label = "Cloudflare" if provider == "cloudflare" else "ngrok"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
click.secho(f"[OK] Tunnel provider: {provider_label}", fg="blue")
|
|
259
|
+
click.secho(f"[OK] Public URL:", fg="blue")
|
|
260
|
+
click.secho(f" {warning_free_url}\n", fg="cyan", bold=True)
|
|
261
|
+
click.secho("Tip: Press Ctrl+Click to open link", fg="magenta")
|
|
262
|
+
click.secho("[INFO] Share this link with collaborators.", fg="magenta")
|
|
228
263
|
except RuntimeError as exc:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
264
|
+
click.secho(f"[WARN] Tunnel failed: {exc}", fg="red")
|
|
265
|
+
click.secho("[INFO] Next step: install cloudflared or configure ngrok auth.", fg="yellow")
|
|
266
|
+
click.secho("[INFO] Tip: run 'ngrok config add-authtoken <token>' for ngrok fallback.", fg="yellow")
|
|
267
|
+
click.secho(f"[OK] Continuing with local proxy at http://localhost:{proxy_port}", fg="green")
|
|
268
|
+
else:
|
|
269
|
+
click.secho("\n⚡ Skipping public tunnel (use --url to enable)", fg="yellow", bold=True)
|
|
270
|
+
click.secho("\n💡 Need to share outside network?", fg="magenta")
|
|
271
|
+
click.secho("👉 Run: devlinker --url", fg="magenta", bold=True)
|
|
233
272
|
|
|
234
273
|
_print_summary(
|
|
235
274
|
frontend_port,
|
|
@@ -244,8 +283,22 @@ def cli(
|
|
|
244
283
|
while True:
|
|
245
284
|
time.sleep(1)
|
|
246
285
|
except KeyboardInterrupt:
|
|
247
|
-
|
|
286
|
+
click.secho("\n[INFO] Dev Linker stopped.", fg="yellow")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@click.group()
|
|
290
|
+
def main():
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
|
|
248
294
|
|
|
295
|
+
main.add_command(cli)
|
|
296
|
+
main.add_command(doctor)
|
|
297
|
+
main.add_command(fix)
|
|
298
|
+
main.add_command(share)
|
|
299
|
+
main.add_command(unshare)
|
|
300
|
+
main.add_command(inspect)
|
|
301
|
+
main.add_command(monitor)
|
|
249
302
|
|
|
250
303
|
if __name__ == "__main__":
|
|
251
|
-
|
|
304
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# API Monitor CLI command for health/status dashboard
|
|
2
|
+
import click
|
|
3
|
+
from devlinker.detection_state import state
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
def monitor():
|
|
7
|
+
"""Show API health/status dashboard."""
|
|
8
|
+
click.secho("\n📡 API Monitor Dashboard\n" + ("═" * 36), fg="cyan", bold=True)
|
|
9
|
+
categories = state.categories
|
|
10
|
+
for category, issues in categories.items():
|
|
11
|
+
if not issues:
|
|
12
|
+
status = "✅"
|
|
13
|
+
else:
|
|
14
|
+
high = any(state.levels.get(issue, "MEDIUM") == "HIGH" for issue in issues)
|
|
15
|
+
warn = any(state.levels.get(issue, "MEDIUM") == "MEDIUM" for issue in issues)
|
|
16
|
+
status = "⚠️" if high or warn else "✅"
|
|
17
|
+
click.secho(f"{category.title():<10}: {status}", fg="white")
|
|
18
|
+
click.secho("\nDetails:", fg="magenta")
|
|
19
|
+
state.report()
|
|
@@ -15,6 +15,38 @@ from websockets.exceptions import ConnectionClosed
|
|
|
15
15
|
|
|
16
16
|
app = FastAPI()
|
|
17
17
|
|
|
18
|
+
# --- RequestInspector: Real-time request analyzer ---
|
|
19
|
+
|
|
20
|
+
from devlinker.detection_state import state
|
|
21
|
+
import threading
|
|
22
|
+
_recent_requests = []
|
|
23
|
+
_recent_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
class RequestInspector:
|
|
26
|
+
def analyze(self, path, status, target):
|
|
27
|
+
warnings = []
|
|
28
|
+
# 1. Missing /api prefix
|
|
29
|
+
if not path.startswith("/api") and target == "backend":
|
|
30
|
+
issue = "Possible missing '/api' prefix"
|
|
31
|
+
state.add(issue, level="MEDIUM", category="routing")
|
|
32
|
+
warnings.append(issue)
|
|
33
|
+
# 2. 404 detection
|
|
34
|
+
if status == 404:
|
|
35
|
+
issue = "Route not found → check backend route"
|
|
36
|
+
state.add(issue, level="HIGH", category="routing")
|
|
37
|
+
warnings.append(issue)
|
|
38
|
+
# 3. Upstream failure
|
|
39
|
+
if status == 502:
|
|
40
|
+
issue = "Backend unreachable"
|
|
41
|
+
state.add(issue, level="HIGH", category="network")
|
|
42
|
+
warnings.append(issue)
|
|
43
|
+
# Log request for inspector
|
|
44
|
+
with _recent_lock:
|
|
45
|
+
_recent_requests.append({"path": path, "status": status, "target": target})
|
|
46
|
+
if len(_recent_requests) > 50:
|
|
47
|
+
_recent_requests.pop(0)
|
|
48
|
+
return warnings
|
|
49
|
+
|
|
18
50
|
FRONTEND: Optional[int] = None
|
|
19
51
|
BACKEND: Optional[int] = None
|
|
20
52
|
HTTP_CLIENT: Optional[httpx.AsyncClient] = None
|
|
@@ -100,13 +132,28 @@ def _build_target_ws_url(port: int, path: str, query: str) -> str:
|
|
|
100
132
|
|
|
101
133
|
|
|
102
134
|
async def _forward_http(request: Request) -> Response:
|
|
135
|
+
from devlinker.logger import print_warning, print_fix
|
|
136
|
+
from devlinker.detector_ai import DevLinkerAI
|
|
137
|
+
|
|
138
|
+
inspector = RequestInspector()
|
|
139
|
+
ai = DevLinkerAI()
|
|
140
|
+
|
|
103
141
|
target_port = _target_port(request.url.path)
|
|
104
142
|
if target_port is None:
|
|
105
143
|
if request.url.path.startswith("/api"):
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
status = 503
|
|
145
|
+
warnings = inspector.analyze(request.url.path, status, "backend")
|
|
146
|
+
for w in warnings:
|
|
147
|
+
print_warning(w)
|
|
148
|
+
return PlainTextResponse("Backend is not configured.", status_code=status)
|
|
149
|
+
status = 503
|
|
150
|
+
warnings = inspector.analyze(request.url.path, status, "frontend")
|
|
151
|
+
for w in warnings:
|
|
152
|
+
print_warning(w)
|
|
153
|
+
return PlainTextResponse("Frontend is not configured.", status_code=status)
|
|
108
154
|
|
|
109
155
|
if HTTP_CLIENT is None:
|
|
156
|
+
print_warning("Proxy HTTP client is not ready.")
|
|
110
157
|
return PlainTextResponse("Proxy HTTP client is not ready.", status_code=503)
|
|
111
158
|
|
|
112
159
|
payload = await request.body()
|
|
@@ -121,7 +168,22 @@ async def _forward_http(request: Request) -> Response:
|
|
|
121
168
|
headers=_filter_request_headers(dict(request.headers)),
|
|
122
169
|
)
|
|
123
170
|
except httpx.RequestError as exc:
|
|
124
|
-
|
|
171
|
+
status = 502
|
|
172
|
+
warnings = inspector.analyze(request.url.path, status, "backend")
|
|
173
|
+
for w in warnings:
|
|
174
|
+
print_warning(w)
|
|
175
|
+
ai_suggestions = ai.analyze_failure(str(exc))
|
|
176
|
+
for s in ai_suggestions:
|
|
177
|
+
print_fix(s)
|
|
178
|
+
return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=status)
|
|
179
|
+
|
|
180
|
+
# Analyze response for warnings and fixes
|
|
181
|
+
warnings = inspector.analyze(request.url.path, upstream.status_code, "backend")
|
|
182
|
+
for w in warnings:
|
|
183
|
+
print_warning(w)
|
|
184
|
+
ai_suggestions = ai.analyze_failure(str(upstream.text))
|
|
185
|
+
for s in ai_suggestions:
|
|
186
|
+
print_fix(s)
|
|
125
187
|
|
|
126
188
|
return Response(
|
|
127
189
|
content=upstream.content,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devlinker.tunnel import start_tunnel
|
|
3
|
+
|
|
4
|
+
# Global tunnel state
|
|
5
|
+
_tunnel_info = {
|
|
6
|
+
"provider": None,
|
|
7
|
+
"public_url": None,
|
|
8
|
+
"active": False,
|
|
9
|
+
"proxy_port": None,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
def share():
|
|
14
|
+
"""Enable public tunnel at runtime (no restart)."""
|
|
15
|
+
from devlinker.main import _select_proxy_port
|
|
16
|
+
import sys
|
|
17
|
+
proxy_port = 8000
|
|
18
|
+
banner = "\n" + ("═" * 36) + "\n🌍 DevLinker Share Mode\n" + ("═" * 36)
|
|
19
|
+
if _tunnel_info["active"]:
|
|
20
|
+
click.secho(f"{banner}\n\n🔗 Tunnel already active:", fg="yellow", bold=True)
|
|
21
|
+
click.secho(f" {_tunnel_info['public_url']}\n", fg="cyan", bold=True)
|
|
22
|
+
return
|
|
23
|
+
try:
|
|
24
|
+
click.secho(f"{banner}\n\n🌍 Enabling public tunnel...", fg="green", bold=True)
|
|
25
|
+
provider, public_url = start_tunnel(proxy_port)
|
|
26
|
+
_tunnel_info["provider"] = provider
|
|
27
|
+
_tunnel_info["public_url"] = public_url
|
|
28
|
+
_tunnel_info["active"] = True
|
|
29
|
+
_tunnel_info["proxy_port"] = proxy_port
|
|
30
|
+
click.secho(f"\n[OK] Tunnel provider: {provider}", fg="blue")
|
|
31
|
+
click.secho(f"[OK] Public URL:", fg="blue")
|
|
32
|
+
click.secho(f" {public_url}\n", fg="cyan", bold=True)
|
|
33
|
+
click.secho("Tip: Press Ctrl+Click to open link", fg="magenta")
|
|
34
|
+
click.secho("[INFO] Share this link with collaborators.\n", fg="magenta")
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
click.secho(f"[WARN] Tunnel failed: {exc}", fg="red")
|
|
37
|
+
click.secho("[INFO] Next step: install cloudflared or configure ngrok auth.", fg="yellow")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
@click.command()
|
|
41
|
+
def unshare():
|
|
42
|
+
"""Disable public tunnel at runtime (no restart)."""
|
|
43
|
+
banner = "\n" + ("═" * 36) + "\n🛑 DevLinker Unshare Mode\n" + ("═" * 36)
|
|
44
|
+
if not _tunnel_info["active"]:
|
|
45
|
+
click.secho(f"{banner}\n\nNo tunnel is currently active.\n", fg="yellow", bold=True)
|
|
46
|
+
return
|
|
47
|
+
# In a real implementation, stop the tunnel process here
|
|
48
|
+
click.secho(f"{banner}\n\n🛑 Disabling tunnel:", fg="red", bold=True)
|
|
49
|
+
click.secho(f" {_tunnel_info['public_url']}\n", fg="cyan", bold=True)
|
|
50
|
+
_tunnel_info["provider"] = None
|
|
51
|
+
_tunnel_info["public_url"] = None
|
|
52
|
+
_tunnel_info["active"] = False
|
|
53
|
+
_tunnel_info["proxy_port"] = None
|
|
54
|
+
click.secho("[OK] Tunnel disabled.\n", fg="green", bold=True)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.3.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.3.3
|
|
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
|
|
@@ -18,20 +18,36 @@ Requires-Dist: websockets
|
|
|
18
18
|
|
|
19
19
|
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.
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
-
- Auto
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
24
|
+
- 🚀 **Unified Dev Proxy:** Combines frontend (Vite/React) and backend (FastAPI/Flask/Node/Docker) into a single local and public URL.
|
|
25
|
+
- 🔍 **Auto Detection:** Detects frontend/backend ports, runtime, Docker containers, and Vite servers automatically.
|
|
26
|
+
- 🧠 **Smart Detection & Doctor:** Real-time request analysis, backend intelligence, log analyzer, and `devlinker doctor` for instant diagnostics.
|
|
27
|
+
- 🛡️ **Auto-Fix Engine:** `devlinker fix` applies safe fixes (like VITE_API_URL) and suggests code changes.
|
|
28
|
+
- 🌍 **Public Sharing:** Share your local dev environment instantly with `--url` (startup) or `devlinker share` (runtime, no restart).
|
|
29
|
+
- 🔄 **Dynamic Tunnel Control:** `devlinker unshare` disables public tunnel at runtime.
|
|
30
|
+
- 📡 **WLAN Sharing:** Prints LAN URL for same-network device access.
|
|
31
|
+
- 🧑💻 **Interactive CLI:** Modern, colorized, emoji-rich terminal UX for all commands.
|
|
32
|
+
- 🧩 **Zero Config:** Works out-of-the-box for most FastAPI, Flask, Vite, and Docker projects.
|
|
33
|
+
- 🧪 **Runtime Smoke Test:** Built-in test for end-to-end proxy validation.
|
|
34
|
+
- 🛠️ **Extensible:** Modular architecture for future SaaS, dashboard, and team features.
|
|
35
|
+
|
|
36
|
+
## CLI Commands & Options
|
|
37
|
+
|
|
38
|
+
- `devlinker` — Start proxy (local only, fast)
|
|
39
|
+
- `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
|
|
40
|
+
- `devlinker share` — Enable public tunnel at runtime (no restart)
|
|
41
|
+
- `devlinker unshare` — Disable public tunnel at runtime
|
|
42
|
+
- `devlinker doctor` — Diagnose issues, see categorized problems and fixes
|
|
43
|
+
- `devlinker fix` — Auto-fix common issues (env, API paths, config)
|
|
44
|
+
- `devlinker --frontend 5173 --backend 5000` — Override detected ports
|
|
45
|
+
- `devlinker --docker` — Auto-start Docker backend
|
|
46
|
+
- `devlinker --no-tunnel` — Force local-only mode
|
|
47
|
+
- `devlinker --no-lan` — Hide WLAN sharing URL
|
|
48
|
+
- `devlinker --interactive-backend` — Prompt to choose backend if multiple found
|
|
49
|
+
- `devlinker --proxy-port 18000` — Use custom proxy port
|
|
50
|
+
- `devlinker --version` — Show version
|
|
35
51
|
|
|
36
52
|
## Project Structure
|
|
37
53
|
|
|
@@ -123,17 +139,71 @@ Enable Docker auto-start explicitly:
|
|
|
123
139
|
devlinker --docker
|
|
124
140
|
```
|
|
125
141
|
|
|
126
|
-
|
|
142
|
+
|
|
143
|
+
## Tunnel and Sharing Modes
|
|
144
|
+
|
|
145
|
+
By default, DevLinker starts **fast local proxy only** (no tunnel). To enable a public tunnel, use the `--url` flag:
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
devlinker --url
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In your terminal output, you'll see:
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
URL, run:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
devlinker --url
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This will start the proxy and open a public tunnel (Cloudflare or ngrok). The output will show:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
🌍 Enabling public tunnel...
|
|
165
|
+
[OK] Tunnel provider: Cloudflare
|
|
166
|
+
[OK] Public URL:
|
|
167
|
+
https://xxxx.trycloudflare.com
|
|
168
|
+
Tip: Press Ctrl+Click to open link
|
|
169
|
+
[INFO] Share this link with collaborators.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
To force tunnel off (even if --url is passed):
|
|
127
173
|
|
|
128
174
|
```bash
|
|
129
175
|
devlinker --no-tunnel
|
|
130
176
|
```
|
|
131
177
|
|
|
178
|
+
When running without `--url`, you’ll see:
|
|
179
|
+
|
|
180
|
+
```text
|
|
181
|
+
⚡ Skipping public tunnel (use --url to enable)
|
|
182
|
+
|
|
183
|
+
💡 Need to share outside network?
|
|
184
|
+
👉 Run: devlinker --url
|
|
185
|
+
```
|
|
186
|
+
|
|
132
187
|
Disable WLAN URL output:
|
|
133
188
|
|
|
134
189
|
```bash
|
|
135
190
|
devlinker --no-lan
|
|
136
191
|
```
|
|
192
|
+
## Smart Detection & Auto-Fix System
|
|
193
|
+
|
|
194
|
+
DevLinker now includes an AI-powered detection and auto-fix engine:
|
|
195
|
+
|
|
196
|
+
- **Request Inspector:** Real-time analysis of proxy traffic for common mistakes (missing `/api` prefix, 404s, CORS risks, upstream failures)
|
|
197
|
+
- **Backend Intelligence:** Probes backend endpoints and type at startup for smarter routing and hints
|
|
198
|
+
- **Log Analyzer:** Converts error messages (CORS, 404, connection refused) into human-readable explanations and actionable fixes
|
|
199
|
+
- **Smart Warning Engine:** Prints clean CLI warnings and suggestions, e.g.:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
⚠️ Detected direct backend call (localhost:5000)
|
|
203
|
+
👉 Use /api/* instead of direct URL
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
All detection and fixes are modular, async-compatible, and production-ready. See `devlinker/proxy.py`, `devlinker/detector_ai.py`, and `devlinker/logger.py` for implementation.
|
|
137
207
|
|
|
138
208
|
Interactive backend selection (when local and Docker are both detected):
|
|
139
209
|
|
|
@@ -2,10 +2,20 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
setup.py
|
|
4
4
|
devlinker/__init__.py
|
|
5
|
+
devlinker/config.py
|
|
6
|
+
devlinker/detection_state.py
|
|
5
7
|
devlinker/detector.py
|
|
8
|
+
devlinker/detector_ai.py
|
|
9
|
+
devlinker/doctor.py
|
|
10
|
+
devlinker/fix.py
|
|
11
|
+
devlinker/fixer.py
|
|
12
|
+
devlinker/inspect.py
|
|
13
|
+
devlinker/logger.py
|
|
6
14
|
devlinker/main.py
|
|
15
|
+
devlinker/monitor.py
|
|
7
16
|
devlinker/proxy.py
|
|
8
17
|
devlinker/runner.py
|
|
18
|
+
devlinker/share.py
|
|
9
19
|
devlinker/tunnel.py
|
|
10
20
|
devlinker.egg-info/PKG-INFO
|
|
11
21
|
devlinker.egg-info/SOURCES.txt
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devlinker"
|
|
7
|
-
version = "1.3.
|
|
8
|
-
description = "
|
|
7
|
+
version = "1.3.3"
|
|
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" }
|
|
11
11
|
]
|
|
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
|