devlinker 1.4.3__tar.gz → 1.4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {devlinker-1.4.3/devlinker.egg-info → devlinker-1.4.5}/PKG-INFO +27 -32
  2. {devlinker-1.4.3 → devlinker-1.4.5}/README.md +26 -31
  3. devlinker-1.4.5/devlinker/config.py +55 -0
  4. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/detection_state.py +39 -0
  5. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/detector.py +4 -4
  6. devlinker-1.4.5/devlinker/detector_ai.py +46 -0
  7. devlinker-1.4.5/devlinker/doctor.py +50 -0
  8. devlinker-1.4.5/devlinker/fix.py +26 -0
  9. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/fixer.py +4 -12
  10. devlinker-1.4.5/devlinker/inspect.py +25 -0
  11. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/main.py +32 -16
  12. devlinker-1.4.5/devlinker/monitor.py +40 -0
  13. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/proxy.py +507 -88
  14. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/runner.py +106 -11
  15. devlinker-1.4.5/devlinker/runtime_api.py +69 -0
  16. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/tunnel.py +51 -23
  17. {devlinker-1.4.3 → devlinker-1.4.5/devlinker.egg-info}/PKG-INFO +27 -32
  18. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker.egg-info/SOURCES.txt +1 -2
  19. {devlinker-1.4.3 → devlinker-1.4.5}/pyproject.toml +1 -1
  20. devlinker-1.4.5/setup.py +4 -0
  21. devlinker-1.4.3/devlinker/config.py +0 -17
  22. devlinker-1.4.3/devlinker/detector_ai.py +0 -23
  23. devlinker-1.4.3/devlinker/doctor.py +0 -27
  24. devlinker-1.4.3/devlinker/fix.py +0 -16
  25. devlinker-1.4.3/devlinker/global_state.py +0 -5
  26. devlinker-1.4.3/devlinker/inspect.py +0 -14
  27. devlinker-1.4.3/devlinker/monitor.py +0 -19
  28. devlinker-1.4.3/devlinker/share.py +0 -64
  29. devlinker-1.4.3/setup.py +0 -9
  30. {devlinker-1.4.3 → devlinker-1.4.5}/LICENSE +0 -0
  31. {devlinker-1.4.3 → devlinker-1.4.5}/MANIFEST.in +0 -0
  32. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/__init__.py +0 -0
  33. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/devlinker_loader_instant.html +0 -0
  34. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/devlinker_loader_snippet.html +0 -0
  35. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker/logger.py +0 -0
  36. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker.egg-info/dependency_links.txt +0 -0
  37. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker.egg-info/entry_points.txt +0 -0
  38. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker.egg-info/requires.txt +0 -0
  39. {devlinker-1.4.3 → devlinker-1.4.5}/devlinker.egg-info/top_level.txt +0 -0
  40. {devlinker-1.4.3 → devlinker-1.4.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.4.3
3
+ Version: 1.4.5
4
4
  Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
5
  Author-email: Mani <mani1028@users.noreply.github.com>
6
6
  Requires-Python: >=3.7
@@ -99,12 +99,11 @@ flowchart LR
99
99
  - 🔍 **Auto Detection:** Detects frontend/backend ports, runtime, Docker containers, and Vite servers automatically.
100
100
  - 📡 **Debug Request Logger:** Live API traffic lines (method, path, status, latency) only in debug mode.
101
101
  - 🧩 **Backend-Only Mode:** If no frontend is detected, DevLinker still runs and forwards all traffic to backend.
102
- - 🔁 **Auto API URL Sync:** Updates `frontend/.env.local` with `VITE_API_URL=http://localhost:<proxy-port>` using a managed block.
102
+ - 🔁 **Runtime API URL Injection:** Injects runtime browser patching so hardcoded localhost API calls are rewritten to the active proxy/tunnel origin.
103
103
  - 🛡️ **Proxy CORS + Preflight:** Handles common CORS/preflight behavior at the proxy layer, including credential-safe Origin handling.
104
104
  - 🧠 **Smart Detection & Doctor:** Real-time request analysis, backend intelligence, log analyzer, and `devlinker doctor` for instant diagnostics.
105
105
  - 🛡️ **Auto-Fix Engine:** `devlinker fix` applies safe fixes (like VITE_API_URL) and suggests code changes.
106
- - 🌍 **Public Sharing:** Share your local dev environment instantly with `--url` (startup) or `devlinker share` (runtime, no restart).
107
- - 🔄 **Dynamic Tunnel Control:** `devlinker unshare` disables public tunnel at runtime.
106
+ - 🌍 **Public Sharing:** Share your local dev environment instantly with the `--url` flag at startup.
108
107
  - 📡 **WLAN Sharing:** Prints LAN URL for same-network device access.
109
108
  - 🔒 **Secure Token Linking:** Optional token gate for LAN/public access with `DEVLINKER_LINK_TOKEN`.
110
109
  - 📊 **Browser API Logs Dashboard:** Open `/__devlinker/dashboard` for lightweight live API visibility.
@@ -129,9 +128,6 @@ If DevLinker helps you ship faster, consider supporting the project:
129
128
  - `devlinker` — Start proxy (local only, fast)
130
129
  - `devlinker support` — Show UPI support QR code in terminal
131
130
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
132
- - `devlinker share` — Enable public tunnel at runtime (no restart)
133
- - `devlinker share --proxy-port 18000` — Enable public tunnel for a custom proxy port
134
- - `devlinker unshare` — Disable public tunnel at runtime
135
131
  - `devlinker doctor` — Diagnose issues, see categorized problems and fixes
136
132
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
137
133
  - `devlinker --frontend 5173 --backend 5000` — Override detected ports
@@ -219,7 +215,7 @@ Typical startup output (TTY with Rich available):
219
215
 
220
216
  ```text
221
217
  ╭─────────────────────────────╮
222
- │ ♾️ DevLinker v1.4.1
218
+ │ ♾️ DevLinker v1.4.5
223
219
  │ Smart Local Dev Environment │
224
220
  ╰─────────────────────────────╯
225
221
 
@@ -299,18 +295,6 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
299
295
  ℹ Share this link with collaborators.
300
296
  ```
301
297
 
302
- If you already started DevLinker and want to turn on sharing without restarting, use:
303
-
304
- ```bash
305
- devlinker share
306
- ```
307
-
308
- If you use a custom proxy port, pass it explicitly:
309
-
310
- ```bash
311
- devlinker share --proxy-port 18000
312
- ```
313
-
314
298
  If your friend is on the same Wi-Fi/LAN, use the printed LAN URL like `http://192.168.x.x:<proxy-port>`. If they are outside your network, use the public tunnel URL instead.
315
299
 
316
300
  To force tunnel off (even if --url is passed):
@@ -393,15 +377,7 @@ fetch("/api/endpoint")
393
377
 
394
378
  Do not hardcode backend host URLs in frontend code.
395
379
 
396
- DevLinker also writes/updates a managed block in `frontend/.env.local`:
397
-
398
- ```env
399
- # devlinker-managed:start
400
- VITE_API_URL=http://localhost:8001
401
- # devlinker-managed:end
402
- ```
403
-
404
- This keeps frontend API calls consistently routed through the proxy.
380
+ DevLinker injects runtime config and request patching in proxied HTML so frontend API calls are routed through the active proxy/tunnel origin without requiring source edits or frontend rebuilds.
405
381
 
406
382
  Use the proxy URL as your single entry point during development:
407
383
 
@@ -426,8 +402,17 @@ frontend: 5173
426
402
  backend: 5000
427
403
  proxy_port: 8001
428
404
  tunnel: false
405
+ backend_entry: main.py
406
+ api_prefix: /api
407
+ strip_prefix: true
429
408
  ```
430
409
 
410
+ Config key notes:
411
+
412
+ - `backend_entry`: Optional Python backend startup file override (for example `main.py`)
413
+ - `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
414
+ - `strip_prefix`: When `true`, strips configured `api_prefix` before forwarding to backend
415
+
431
416
  ## Backend Auto-Detection
432
417
 
433
418
  Backend port detection runs in this order:
@@ -439,6 +424,16 @@ Backend port detection runs in this order:
439
424
  5. Use the best mapped host port automatically, even when internal port is not 5000
440
425
  6. If nothing is found, print next-step guidance and exit
441
426
 
427
+ Python backend entry detection supports automatic discovery of:
428
+
429
+ - `app.py`
430
+ - `main.py`
431
+ - `server.py`
432
+ - `run.py`
433
+ - `manage.py`
434
+
435
+ If `backend_entry` is set in config, DevLinker uses it first and falls back to the discovery list.
436
+
442
437
  If Docker SDK is unavailable, Dev Linker falls back to Docker CLI parsing as a compatibility path.
443
438
 
444
439
  When both Local and Docker backends are available, Dev Linker prompts you to choose one (TTY mode) unless `--no-interactive-backend` is used.
@@ -464,7 +459,7 @@ Dev Linker checks backend runtime in this order:
464
459
  1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
465
460
  2. Docker (`backend/Dockerfile`)
466
461
  3. Node (`backend/package.json`)
467
- 4. Python (`backend/requirements.txt` or `backend/app.py`)
462
+ 4. Python (`backend/requirements.txt` or discovered entry file such as `app.py` / `main.py`)
468
463
 
469
464
  Backend startup commands:
470
465
 
@@ -472,7 +467,7 @@ Backend startup commands:
472
467
  - Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
473
468
  - Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
474
469
  - Node: `npm run dev` (or `npm start` when `dev` is missing)
475
- - Python: `python app.py`
470
+ - Python: `python <discovered-entry>` (auto: `app.py`, `main.py`, `server.py`, `run.py`, `manage.py`, or configured `backend_entry`)
476
471
 
477
472
  For containerized Flask backends, ensure:
478
473
 
@@ -481,7 +476,7 @@ For containerized Flask backends, ensure:
481
476
 
482
477
  ## Notes
483
478
 
484
- - runner.py expects frontend project in frontend and Python app in backend/app.py.
479
+ - runner.py expects frontend project in `frontend/` and backend project in `backend/`, with automatic Python entry discovery.
485
480
  - If those paths do not exist, Dev Linker skips launch and only tries to detect already-running services.
486
481
  - If frontend is missing but backend is available, DevLinker continues in backend-only mode.
487
482
  - Tunnel selection order is: cloudflared (TryCloudflare), then ngrok.
@@ -79,12 +79,11 @@ flowchart LR
79
79
  - 🔍 **Auto Detection:** Detects frontend/backend ports, runtime, Docker containers, and Vite servers automatically.
80
80
  - 📡 **Debug Request Logger:** Live API traffic lines (method, path, status, latency) only in debug mode.
81
81
  - 🧩 **Backend-Only Mode:** If no frontend is detected, DevLinker still runs and forwards all traffic to backend.
82
- - 🔁 **Auto API URL Sync:** Updates `frontend/.env.local` with `VITE_API_URL=http://localhost:<proxy-port>` using a managed block.
82
+ - 🔁 **Runtime API URL Injection:** Injects runtime browser patching so hardcoded localhost API calls are rewritten to the active proxy/tunnel origin.
83
83
  - 🛡️ **Proxy CORS + Preflight:** Handles common CORS/preflight behavior at the proxy layer, including credential-safe Origin handling.
84
84
  - 🧠 **Smart Detection & Doctor:** Real-time request analysis, backend intelligence, log analyzer, and `devlinker doctor` for instant diagnostics.
85
85
  - 🛡️ **Auto-Fix Engine:** `devlinker fix` applies safe fixes (like VITE_API_URL) and suggests code changes.
86
- - 🌍 **Public Sharing:** Share your local dev environment instantly with `--url` (startup) or `devlinker share` (runtime, no restart).
87
- - 🔄 **Dynamic Tunnel Control:** `devlinker unshare` disables public tunnel at runtime.
86
+ - 🌍 **Public Sharing:** Share your local dev environment instantly with the `--url` flag at startup.
88
87
  - 📡 **WLAN Sharing:** Prints LAN URL for same-network device access.
89
88
  - 🔒 **Secure Token Linking:** Optional token gate for LAN/public access with `DEVLINKER_LINK_TOKEN`.
90
89
  - 📊 **Browser API Logs Dashboard:** Open `/__devlinker/dashboard` for lightweight live API visibility.
@@ -109,9 +108,6 @@ If DevLinker helps you ship faster, consider supporting the project:
109
108
  - `devlinker` — Start proxy (local only, fast)
110
109
  - `devlinker support` — Show UPI support QR code in terminal
111
110
  - `devlinker --url` — Start with public tunnel (Cloudflare/ngrok)
112
- - `devlinker share` — Enable public tunnel at runtime (no restart)
113
- - `devlinker share --proxy-port 18000` — Enable public tunnel for a custom proxy port
114
- - `devlinker unshare` — Disable public tunnel at runtime
115
111
  - `devlinker doctor` — Diagnose issues, see categorized problems and fixes
116
112
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
117
113
  - `devlinker --frontend 5173 --backend 5000` — Override detected ports
@@ -199,7 +195,7 @@ Typical startup output (TTY with Rich available):
199
195
 
200
196
  ```text
201
197
  ╭─────────────────────────────╮
202
- │ ♾️ DevLinker v1.4.1
198
+ │ ♾️ DevLinker v1.4.5
203
199
  │ Smart Local Dev Environment │
204
200
  ╰─────────────────────────────╯
205
201
 
@@ -279,18 +275,6 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
279
275
  ℹ Share this link with collaborators.
280
276
  ```
281
277
 
282
- If you already started DevLinker and want to turn on sharing without restarting, use:
283
-
284
- ```bash
285
- devlinker share
286
- ```
287
-
288
- If you use a custom proxy port, pass it explicitly:
289
-
290
- ```bash
291
- devlinker share --proxy-port 18000
292
- ```
293
-
294
278
  If your friend is on the same Wi-Fi/LAN, use the printed LAN URL like `http://192.168.x.x:<proxy-port>`. If they are outside your network, use the public tunnel URL instead.
295
279
 
296
280
  To force tunnel off (even if --url is passed):
@@ -373,15 +357,7 @@ fetch("/api/endpoint")
373
357
 
374
358
  Do not hardcode backend host URLs in frontend code.
375
359
 
376
- DevLinker also writes/updates a managed block in `frontend/.env.local`:
377
-
378
- ```env
379
- # devlinker-managed:start
380
- VITE_API_URL=http://localhost:8001
381
- # devlinker-managed:end
382
- ```
383
-
384
- This keeps frontend API calls consistently routed through the proxy.
360
+ DevLinker injects runtime config and request patching in proxied HTML so frontend API calls are routed through the active proxy/tunnel origin without requiring source edits or frontend rebuilds.
385
361
 
386
362
  Use the proxy URL as your single entry point during development:
387
363
 
@@ -406,8 +382,17 @@ frontend: 5173
406
382
  backend: 5000
407
383
  proxy_port: 8001
408
384
  tunnel: false
385
+ backend_entry: main.py
386
+ api_prefix: /api
387
+ strip_prefix: true
409
388
  ```
410
389
 
390
+ Config key notes:
391
+
392
+ - `backend_entry`: Optional Python backend startup file override (for example `main.py`)
393
+ - `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
394
+ - `strip_prefix`: When `true`, strips configured `api_prefix` before forwarding to backend
395
+
411
396
  ## Backend Auto-Detection
412
397
 
413
398
  Backend port detection runs in this order:
@@ -419,6 +404,16 @@ Backend port detection runs in this order:
419
404
  5. Use the best mapped host port automatically, even when internal port is not 5000
420
405
  6. If nothing is found, print next-step guidance and exit
421
406
 
407
+ Python backend entry detection supports automatic discovery of:
408
+
409
+ - `app.py`
410
+ - `main.py`
411
+ - `server.py`
412
+ - `run.py`
413
+ - `manage.py`
414
+
415
+ If `backend_entry` is set in config, DevLinker uses it first and falls back to the discovery list.
416
+
422
417
  If Docker SDK is unavailable, Dev Linker falls back to Docker CLI parsing as a compatibility path.
423
418
 
424
419
  When both Local and Docker backends are available, Dev Linker prompts you to choose one (TTY mode) unless `--no-interactive-backend` is used.
@@ -444,7 +439,7 @@ Dev Linker checks backend runtime in this order:
444
439
  1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
445
440
  2. Docker (`backend/Dockerfile`)
446
441
  3. Node (`backend/package.json`)
447
- 4. Python (`backend/requirements.txt` or `backend/app.py`)
442
+ 4. Python (`backend/requirements.txt` or discovered entry file such as `app.py` / `main.py`)
448
443
 
449
444
  Backend startup commands:
450
445
 
@@ -452,7 +447,7 @@ Backend startup commands:
452
447
  - Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
453
448
  - Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
454
449
  - Node: `npm run dev` (or `npm start` when `dev` is missing)
455
- - Python: `python app.py`
450
+ - Python: `python <discovered-entry>` (auto: `app.py`, `main.py`, `server.py`, `run.py`, `manage.py`, or configured `backend_entry`)
456
451
 
457
452
  For containerized Flask backends, ensure:
458
453
 
@@ -461,7 +456,7 @@ For containerized Flask backends, ensure:
461
456
 
462
457
  ## Notes
463
458
 
464
- - runner.py expects frontend project in frontend and Python app in backend/app.py.
459
+ - runner.py expects frontend project in `frontend/` and backend project in `backend/`, with automatic Python entry discovery.
465
460
  - If those paths do not exist, Dev Linker skips launch and only tries to detect already-running services.
466
461
  - If frontend is missing but backend is available, DevLinker continues in backend-only mode.
467
462
  - Tunnel selection order is: cloudflared (TryCloudflare), then ngrok.
@@ -0,0 +1,55 @@
1
+ import os
2
+ import json
3
+
4
+ try:
5
+ import yaml
6
+ except ImportError: # pragma: no cover - optional dependency fallback
7
+ yaml = None
8
+
9
+
10
+ def _normalize_api_prefix(value: object) -> str:
11
+ if not isinstance(value, str):
12
+ return "/api"
13
+ prefix = value.strip()
14
+ if not prefix:
15
+ return "/api"
16
+ if not prefix.startswith("/"):
17
+ prefix = f"/{prefix}"
18
+ if len(prefix) > 1 and prefix.endswith("/"):
19
+ prefix = prefix.rstrip("/")
20
+ return prefix or "/api"
21
+
22
+
23
+ def _normalize_config(data: dict) -> dict:
24
+ normalized = dict(data)
25
+ backend_entry = normalized.get("backend_entry")
26
+ if not backend_entry:
27
+ backend_entry = normalized.get("entry_point")
28
+ if isinstance(backend_entry, str) and backend_entry.strip():
29
+ normalized["backend_entry"] = backend_entry.strip()
30
+
31
+ if "api_prefix" in normalized:
32
+ normalized["api_prefix"] = _normalize_api_prefix(normalized.get("api_prefix"))
33
+
34
+ if "strip_prefix" in normalized:
35
+ normalized["strip_prefix"] = bool(normalized.get("strip_prefix"))
36
+
37
+ return normalized
38
+
39
+
40
+ def load_config(config_path: str = "devlinker.yaml") -> dict:
41
+ candidates = [config_path, "devlinker.yml", "devlinker.json"]
42
+ if yaml is None:
43
+ candidates = [path for path in candidates if path.endswith(".json")]
44
+ selected = next((path for path in candidates if os.path.exists(path)), None)
45
+ if not selected:
46
+ return {}
47
+
48
+ with open(selected, "r", encoding="utf-8") as handle:
49
+ if selected.endswith(".json"):
50
+ data = json.load(handle)
51
+ else:
52
+ if yaml is None:
53
+ return {}
54
+ data = yaml.safe_load(handle)
55
+ return _normalize_config(data or {})
@@ -13,6 +13,8 @@ class DetectionState:
13
13
  return False # already shown
14
14
  else:
15
15
  self.counts[key] = 1
16
+ self.levels[issue] = level
17
+ self.categories.setdefault(category, []).append(issue)
16
18
  self.issues.append({
17
19
  "issue": issue,
18
20
  "level": level,
@@ -62,5 +64,42 @@ class DetectionState:
62
64
  else:
63
65
  print(f"💡 {issue} (x{count})")
64
66
 
67
+ def get_issue_records(self):
68
+ records = []
69
+ for issue in self.issues:
70
+ issue_text = issue["issue"]
71
+ records.append(
72
+ {
73
+ "issue": issue_text,
74
+ "level": issue["level"],
75
+ "category": issue["category"],
76
+ "count": self.get_count(issue_text),
77
+ }
78
+ )
79
+ return records
80
+
81
+ def get_category_statuses(self):
82
+ statuses = {}
83
+ for category, issues in self.categories.items():
84
+ if not issues:
85
+ statuses[category] = "OK"
86
+ continue
87
+ has_high = any(self.levels.get(issue, "MEDIUM") == "HIGH" for issue in issues)
88
+ has_medium = any(self.levels.get(issue, "MEDIUM") == "MEDIUM" for issue in issues)
89
+ if has_high:
90
+ statuses[category] = "HIGH"
91
+ elif has_medium:
92
+ statuses[category] = "MEDIUM"
93
+ else:
94
+ statuses[category] = "LOW"
95
+ return statuses
96
+
97
+ def snapshot(self):
98
+ return {
99
+ "total_issues": len(self.issues),
100
+ "items": self.get_issue_records(),
101
+ "categories": self.get_category_statuses(),
102
+ }
103
+
65
104
  # Singleton instance
66
105
  state = DetectionState()
@@ -15,7 +15,7 @@ DEFAULT_BACKEND_PROBE_PATHS = (
15
15
 
16
16
  def check_port(
17
17
  port: int,
18
- timeout: float = 1.0,
18
+ timeout: float = 0.4,
19
19
  probe_paths: Iterable[str] = DEFAULT_BACKEND_PROBE_PATHS,
20
20
  ) -> bool:
21
21
  """Return True when an HTTP service is reachable on localhost:port.
@@ -53,7 +53,7 @@ def check_port(
53
53
  return False
54
54
 
55
55
 
56
- def is_vite_port(port: int, timeout: float = 1.0) -> bool:
56
+ def is_vite_port(port: int, timeout: float = 0.4) -> bool:
57
57
  """Return True when port looks like a Vite dev server."""
58
58
  for host in ("localhost", "127.0.0.1"):
59
59
  try:
@@ -95,8 +95,8 @@ def _ordered_unique_ports(*port_groups: Iterable[int]) -> list[int]:
95
95
  def detect_ports(
96
96
  frontend: Optional[int] = None,
97
97
  backend: Optional[int] = None,
98
- retries: int = 12,
99
- delay_seconds: float = 1.0,
98
+ retries: int = 16,
99
+ delay_seconds: float = 0.5,
100
100
  ) -> Tuple[Optional[int], Optional[int]]:
101
101
  """Detect frontend and backend ports with retry support for slow startups."""
102
102
  frontend_ports = _ordered_unique_ports(
@@ -0,0 +1,46 @@
1
+ class DevLinkerAI:
2
+ def analyze_prefix_mismatch(
3
+ self,
4
+ api_path: str,
5
+ prefixed_status: int,
6
+ unprefixed_status: int,
7
+ api_prefix: str = "/api",
8
+ ):
9
+ if prefixed_status != 404:
10
+ return []
11
+ if unprefixed_status >= 500 or unprefixed_status == 404:
12
+ return []
13
+ return [
14
+ f"Detected API prefix mismatch for {api_path}",
15
+ f"Enable strip_prefix=true so requests to {api_prefix}/* are forwarded without the prefix",
16
+ ]
17
+
18
+ def analyze_failure(self, error_text):
19
+ lowered = error_text.lower()
20
+ if "cors" in lowered:
21
+ return [
22
+ "Frontend is calling backend directly",
23
+ "Use /api/* instead of localhost:PORT"
24
+ ]
25
+ if "404" in error_text:
26
+ if " get / " in lowered or lowered.strip().startswith("get /"):
27
+ return []
28
+ if "/api" not in lowered:
29
+ return ["Route not found"]
30
+ return [
31
+ "Route not found",
32
+ "Check if '/api' prefix is required",
33
+ "If backend routes are root-based, enable strip_prefix=true",
34
+ ]
35
+ if "connection refused" in lowered:
36
+ return [
37
+ "Backend not reachable",
38
+ "Ensure backend is running"
39
+ ]
40
+ if "502" in lowered or "unreachable" in lowered:
41
+ return [
42
+ "Proxy cannot reach backend",
43
+ "If using Docker, ensure backend binds to 0.0.0.0 (not 127.0.0.1)",
44
+ "Check mapped backend port and local firewall rules",
45
+ ]
46
+ return []
@@ -0,0 +1,50 @@
1
+ import click
2
+ from devlinker.detector_ai import DevLinkerAI
3
+ from devlinker.logger import print_fix
4
+ from devlinker.runtime_api import fetch_issues, proxy_base_url
5
+
6
+ @click.command()
7
+ def doctor():
8
+ """Run DevLinker diagnostics and print a health dashboard."""
9
+ try:
10
+ payload = fetch_issues()
11
+ except Exception as exc:
12
+ click.secho(
13
+ f"Could not reach running DevLinker proxy at {proxy_base_url()} ({exc})",
14
+ fg="red",
15
+ )
16
+ click.secho("Start DevLinker first, then run this command from another terminal.", fg="yellow")
17
+ return
18
+
19
+ issues = payload.get("items", [])
20
+ categories = payload.get("categories", {})
21
+ ai = DevLinkerAI()
22
+ print("\n🩺 DevLinker Health Dashboard\n" + ("═" * 36))
23
+ # Grouped status summary
24
+ if not categories:
25
+ categories = {"general": "OK"}
26
+ for category, level in categories.items():
27
+ status = "✅" if level == "OK" else "⚠️"
28
+ print(f"{category.title():<10}: {status}")
29
+ print("\nDetails:")
30
+ if not issues:
31
+ print("✅ No issues detected yet.")
32
+ else:
33
+ for issue in issues:
34
+ issue_text = issue.get("issue", "Unknown issue")
35
+ level = str(issue.get("level", "MEDIUM")).upper()
36
+ count = int(issue.get("count", 1))
37
+ if level == "HIGH":
38
+ print(f"❌ {issue_text} (x{count})")
39
+ elif level == "MEDIUM":
40
+ print(f"⚠️ {issue_text} (x{count})")
41
+ else:
42
+ print(f"💡 {issue_text} (x{count})")
43
+ print("\nFix Suggestions:")
44
+ for issue in issues:
45
+ issue_text = issue.get("issue", "")
46
+ if not issue_text:
47
+ continue
48
+ suggestions = ai.analyze_failure(issue_text)
49
+ for s in suggestions:
50
+ print_fix(s)
@@ -0,0 +1,26 @@
1
+ import click
2
+ from devlinker.fixer import DevLinkerFixer
3
+ from devlinker.runtime_api import fetch_issues, proxy_base_url
4
+
5
+ @click.command()
6
+ def fix():
7
+ """Apply auto-fixes for detected issues."""
8
+ try:
9
+ payload = fetch_issues()
10
+ except Exception as exc:
11
+ click.secho(
12
+ f"Could not reach running DevLinker proxy at {proxy_base_url()} ({exc})",
13
+ fg="red",
14
+ )
15
+ click.secho("Start DevLinker first, then run this command from another terminal.", fg="yellow")
16
+ return
17
+
18
+ issues = payload.get("items", [])
19
+ fixer = DevLinkerFixer()
20
+ print("\n🔧 Applying fixes...")
21
+ results = fixer.apply_fixes(issues)
22
+ print("\n🔧 Fix Results")
23
+ for r in results:
24
+ print(f"✔ {r}")
25
+ if not results:
26
+ print("No auto-fixes applied. All clear or manual review needed.")
@@ -1,5 +1,3 @@
1
- import os
2
-
3
1
  class DevLinkerFixer:
4
2
  def apply_fixes(self, issues):
5
3
  fixes = []
@@ -12,16 +10,10 @@ class DevLinkerFixer:
12
10
  return fixes
13
11
 
14
12
  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"
13
+ return (
14
+ "Runtime injection is active: no .env update needed. "
15
+ "Use the DevLinker proxy URL and verify API calls go through /api."
16
+ )
25
17
 
26
18
  def suggest_api_fix(self):
27
19
  return "Suggest: Replace hardcoded http://localhost:8000 with /api in frontend code (manual review)"
@@ -0,0 +1,25 @@
1
+ import click
2
+ from devlinker.runtime_api import fetch_logs, proxy_base_url
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
+ try:
9
+ payload = fetch_logs(limit=50)
10
+ except Exception as exc:
11
+ click.secho(
12
+ f"Could not reach running DevLinker proxy at {proxy_base_url()} ({exc})",
13
+ fg="red",
14
+ )
15
+ click.secho("Start DevLinker first, then run this command from another terminal.", fg="yellow")
16
+ return
17
+
18
+ recent_requests = payload.get("items", [])
19
+ if not recent_requests:
20
+ click.secho("No API calls recorded yet.", fg="yellow")
21
+ return
22
+ for req in recent_requests[-50:]:
23
+ status = req["status"]
24
+ emoji = "✅" if status < 400 else ("⚠️" if status < 500 else "❌")
25
+ click.secho(f"{emoji} {req['target']:<8} {req['path']:<30} → {status}", fg="white")