devlinker 1.4.4__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.
- {devlinker-1.4.4/devlinker.egg-info → devlinker-1.4.5}/PKG-INFO +27 -32
- {devlinker-1.4.4 → devlinker-1.4.5}/README.md +26 -31
- devlinker-1.4.5/devlinker/config.py +55 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/detection_state.py +39 -0
- devlinker-1.4.5/devlinker/detector_ai.py +46 -0
- devlinker-1.4.5/devlinker/doctor.py +50 -0
- devlinker-1.4.5/devlinker/fix.py +26 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/fixer.py +4 -12
- devlinker-1.4.5/devlinker/inspect.py +25 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/main.py +28 -12
- devlinker-1.4.5/devlinker/monitor.py +40 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/proxy.py +486 -84
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/runner.py +106 -11
- devlinker-1.4.5/devlinker/runtime_api.py +69 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/tunnel.py +7 -2
- {devlinker-1.4.4 → devlinker-1.4.5/devlinker.egg-info}/PKG-INFO +27 -32
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker.egg-info/SOURCES.txt +1 -2
- {devlinker-1.4.4 → devlinker-1.4.5}/pyproject.toml +1 -1
- devlinker-1.4.5/setup.py +4 -0
- devlinker-1.4.4/devlinker/config.py +0 -17
- devlinker-1.4.4/devlinker/detector_ai.py +0 -23
- devlinker-1.4.4/devlinker/doctor.py +0 -27
- devlinker-1.4.4/devlinker/fix.py +0 -16
- devlinker-1.4.4/devlinker/global_state.py +0 -5
- devlinker-1.4.4/devlinker/inspect.py +0 -14
- devlinker-1.4.4/devlinker/monitor.py +0 -19
- devlinker-1.4.4/devlinker/share.py +0 -64
- devlinker-1.4.4/setup.py +0 -9
- {devlinker-1.4.4 → devlinker-1.4.5}/LICENSE +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/MANIFEST.in +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/__init__.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/detector.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/devlinker_loader_instant.html +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/devlinker_loader_snippet.html +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker/logger.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.5}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.4.4 → 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
|
+
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
|
-
- 🔁 **
|
|
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`
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
- 🔁 **
|
|
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`
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
|
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()
|
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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")
|