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.
Files changed (27) hide show
  1. {devlinker-1.3.0 → devlinker-1.3.3}/PKG-INFO +85 -15
  2. {devlinker-1.3.0 → devlinker-1.3.3}/README.md +83 -13
  3. devlinker-1.3.3/devlinker/config.py +8 -0
  4. devlinker-1.3.3/devlinker/detection_state.py +58 -0
  5. devlinker-1.3.3/devlinker/detector_ai.py +18 -0
  6. devlinker-1.3.3/devlinker/doctor.py +27 -0
  7. devlinker-1.3.3/devlinker/fix.py +16 -0
  8. devlinker-1.3.3/devlinker/fixer.py +27 -0
  9. devlinker-1.3.3/devlinker/inspect.py +14 -0
  10. devlinker-1.3.3/devlinker/logger.py +5 -0
  11. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/main.py +89 -36
  12. devlinker-1.3.3/devlinker/monitor.py +19 -0
  13. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/proxy.py +65 -3
  14. devlinker-1.3.3/devlinker/share.py +54 -0
  15. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/PKG-INFO +85 -15
  16. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/SOURCES.txt +10 -0
  17. {devlinker-1.3.0 → devlinker-1.3.3}/pyproject.toml +2 -2
  18. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/__init__.py +0 -0
  19. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/detector.py +0 -0
  20. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/runner.py +0 -0
  21. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker/tunnel.py +0 -0
  22. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/dependency_links.txt +0 -0
  23. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/entry_points.txt +0 -0
  24. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/requires.txt +0 -0
  25. {devlinker-1.3.0 → devlinker-1.3.3}/devlinker.egg-info/top_level.txt +0 -0
  26. {devlinker-1.3.0 → devlinker-1.3.3}/setup.cfg +0 -0
  27. {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.0
4
- Summary: AI-powered linking and automation tool
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
- - Launches frontend automatically (when frontend exists)
24
- - Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
25
- - Auto-starts Python/Node backends; Docker is manual by default for reliability
26
- - Detects common frontend/backend ports
27
- - Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
28
- - Supports Docker backend port auto-detection
29
- - Works with dynamic container host ports
30
- - No config needed for standard FastAPI or Flask plus Docker flows
31
- - Serves both through one proxy at http://localhost:8000
32
- - Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
33
- - Terminal-first workflow
34
- - Supports CLI version output with --version
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
- Run local-only mode without tunnel:
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
- - Launches frontend automatically (when frontend exists)
8
- - Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
9
- - Auto-starts Python/Node backends; Docker is manual by default for reliability
10
- - Detects common frontend/backend ports
11
- - Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
12
- - Supports Docker backend port auto-detection
13
- - Works with dynamic container host ports
14
- - No config needed for standard FastAPI or Flask plus Docker flows
15
- - Serves both through one proxy at http://localhost:8000
16
- - Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
17
- - Terminal-first workflow
18
- - Supports CLI version output with --version
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
- Run local-only mode without tunnel:
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,8 @@
1
+ import yaml
2
+ import os
3
+
4
+ def load_config(config_path="devlinker.yaml"):
5
+ if not os.path.exists(config_path):
6
+ return {}
7
+ with open(config_path, "r") as f:
8
+ return yaml.safe_load(f) or {}
@@ -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")
@@ -0,0 +1,5 @@
1
+ def print_warning(msg):
2
+ print(f"\n⚠️ {msg}")
3
+
4
+ def print_fix(msg):
5
+ print(f"👉 {msg}")
@@ -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
- print(f"\nDevLinker Ready (in {startup_seconds:.1f}s)")
62
- print(f"Frontend: http://localhost:{frontend_port}")
63
- print(f"Backend: http://localhost:{backend_port}")
64
- print("Access Links:")
65
- print(f"Local: http://localhost:{proxy_port}")
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
- print(f"WLAN: {wlan_url}")
78
+ click.secho(f" WLAN {wlan_url}", fg="white")
68
79
  else:
69
- print("WLAN: unavailable")
80
+ click.secho(" WLAN unavailable", fg="white")
70
81
  if public_url:
71
- print(f"Public: {public_url}")
72
- print("Tip: Press Ctrl+Click to open link")
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
- print("Public: unavailable (local proxy still active)")
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
- print(f"\nDev Linker v{__version__}")
154
- print("[INFO] Mode: Auto (FastAPI async proxy + Docker detection)")
155
- print("[INFO] Booting local services...")
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
- print("[INFO] Detecting frontend/backend ports...")
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
- print(f"[OK] Frontend -> {frontend_port}")
194
- print(f"[OK] Backend -> {backend_port}\n")
223
+ click.secho(f"[OK] Frontend {frontend_port}", fg="green")
224
+ click.secho(f"[OK] Backend {backend_port}\n", fg="green")
195
225
 
196
- print(f"[INFO] Starting proxy on :{proxy_port}...")
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
- print(f"[OK] WLAN URL: {wlan_url}")
208
- print("[INFO] Share WLAN link with teammates on same WiFi/LAN.")
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
- print("[WARN] WLAN URL unavailable (no active LAN interface detected).")
211
- print("[INFO] If LAN sharing fails, allow proxy port in firewall and use same network.")
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
- print("[INFO] Tunnel disabled by --no-tunnel; local proxy only.")
217
- else:
250
+ enable_tunnel = False
251
+
252
+ if enable_tunnel:
218
253
  try:
219
- print("[INFO] Opening public tunnel...")
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
- print(f"[OK] Tunnel provider: {provider_label}")
224
- print("[OK] Public URL:")
225
- print(f" {warning_free_url}\n")
226
- print("Tip: Press Ctrl+Click to open link")
227
- print("[INFO] Share this link with collaborators.")
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
- print(f"[WARN] Tunnel failed: {exc}")
230
- print("[INFO] Next step: install cloudflared or configure ngrok auth.")
231
- print("[INFO] Tip: run 'ngrok config add-authtoken <token>' for ngrok fallback.")
232
- print(f"[OK] Continuing with local proxy at http://localhost:{proxy_port}")
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
- print("\n[INFO] Dev Linker stopped.")
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
- cli()
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
- return PlainTextResponse("Backend is not configured.", status_code=503)
107
- return PlainTextResponse("Frontend is not configured.", status_code=503)
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
- return PlainTextResponse(f"Upstream unavailable: {exc}", status_code=502)
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.0
4
- Summary: AI-powered linking and automation tool
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
- - Launches frontend automatically (when frontend exists)
24
- - Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
25
- - Auto-starts Python/Node backends; Docker is manual by default for reliability
26
- - Detects common frontend/backend ports
27
- - Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
28
- - Supports Docker backend port auto-detection
29
- - Works with dynamic container host ports
30
- - No config needed for standard FastAPI or Flask plus Docker flows
31
- - Serves both through one proxy at http://localhost:8000
32
- - Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
33
- - Terminal-first workflow
34
- - Supports CLI version output with --version
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
- Run local-only mode without tunnel:
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.0"
8
- description = "AI-powered linking and automation tool"
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