devlinker 1.4.4__tar.gz → 1.4.6__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.6}/PKG-INFO +59 -40
- {devlinker-1.4.4 → devlinker-1.4.6}/README.md +58 -39
- devlinker-1.4.6/devlinker/config.py +55 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/detection_state.py +39 -0
- devlinker-1.4.6/devlinker/detector_ai.py +46 -0
- devlinker-1.4.6/devlinker/doctor.py +50 -0
- devlinker-1.4.6/devlinker/fix.py +26 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/fixer.py +4 -12
- devlinker-1.4.6/devlinker/inspect.py +25 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/main.py +36 -19
- devlinker-1.4.6/devlinker/monitor.py +40 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/proxy.py +486 -84
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/runner.py +106 -11
- devlinker-1.4.6/devlinker/runtime_api.py +69 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/tunnel.py +7 -2
- {devlinker-1.4.4 → devlinker-1.4.6/devlinker.egg-info}/PKG-INFO +59 -40
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/SOURCES.txt +1 -2
- {devlinker-1.4.4 → devlinker-1.4.6}/pyproject.toml +1 -1
- devlinker-1.4.6/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.6}/LICENSE +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/MANIFEST.in +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/__init__.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/detector.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/devlinker_loader_instant.html +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/devlinker_loader_snippet.html +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/logger.py +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.4.4 → devlinker-1.4.6}/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.6
|
|
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,14 +128,11 @@ 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
|
|
138
134
|
- `devlinker --docker` — Auto-start Docker backend
|
|
139
|
-
- `devlinker --no-tunnel` —
|
|
135
|
+
- `devlinker --no-tunnel` — Explicitly force local/WLAN-only mode (no public tunnel)
|
|
140
136
|
- `devlinker --no-lan` — Hide WLAN sharing URL
|
|
141
137
|
- `devlinker --interactive-backend` — Prompt to choose backend if multiple found
|
|
142
138
|
- `devlinker --proxy-port 18000` — Use custom proxy port
|
|
@@ -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
|
|
|
@@ -281,6 +277,20 @@ devlinker --docker
|
|
|
281
277
|
|
|
282
278
|
By default, DevLinker starts **fast local proxy only** (no tunnel). It prints a LAN URL when it can detect a local network interface, and you can share that link with devices on the same Wi-Fi/LAN.
|
|
283
279
|
|
|
280
|
+
LAN-only quick start:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
devlinker
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
You can also pass `--no-tunnel` to explicitly enforce no public tunnel startup:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
devlinker --no-tunnel
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`--no-tunnel` does **not** disable frontend/backend linking. DevLinker still starts the local proxy and routes frontend + backend through the same local entry URL.
|
|
293
|
+
|
|
284
294
|
For access from another network, start with a public tunnel using the `--url` flag:
|
|
285
295
|
|
|
286
296
|
|
|
@@ -299,35 +309,33 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
|
|
|
299
309
|
ℹ Share this link with collaborators.
|
|
300
310
|
```
|
|
301
311
|
|
|
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
312
|
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
313
|
|
|
316
|
-
To force tunnel off (even if
|
|
314
|
+
To force tunnel off (even if `--url` is passed):
|
|
317
315
|
|
|
318
316
|
```bash
|
|
319
317
|
devlinker --no-tunnel
|
|
320
318
|
```
|
|
321
319
|
|
|
322
|
-
When running without `--url`, you’ll see:
|
|
320
|
+
When running without `--url`, you’ll see local/WLAN output with public disabled:
|
|
323
321
|
|
|
324
322
|
```text
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
👉 Run: devlinker --url
|
|
323
|
+
Proxy: http://localhost:8000
|
|
324
|
+
WLAN: http://192.168.1.5:8000
|
|
325
|
+
Public: disabled (use --url)
|
|
329
326
|
```
|
|
330
327
|
|
|
328
|
+
WLAN usage notes:
|
|
329
|
+
|
|
330
|
+
- `localhost` works only on the machine running DevLinker.
|
|
331
|
+
- Use the printed WLAN URL on phones/laptops connected to the same Wi-Fi/LAN.
|
|
332
|
+
|
|
333
|
+
WLAN troubleshooting checklist:
|
|
334
|
+
|
|
335
|
+
1. Ensure both devices are on the same Wi-Fi/subnet.
|
|
336
|
+
2. Allow Python/DevLinker through firewall prompts (Private networks).
|
|
337
|
+
3. Keep Universal Mode enabled (default) so localhost API calls are rewritten safely for LAN/public links.
|
|
338
|
+
|
|
331
339
|
Disable WLAN URL output:
|
|
332
340
|
|
|
333
341
|
```bash
|
|
@@ -393,15 +401,7 @@ fetch("/api/endpoint")
|
|
|
393
401
|
|
|
394
402
|
Do not hardcode backend host URLs in frontend code.
|
|
395
403
|
|
|
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.
|
|
404
|
+
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
405
|
|
|
406
406
|
Use the proxy URL as your single entry point during development:
|
|
407
407
|
|
|
@@ -425,9 +425,18 @@ Example:
|
|
|
425
425
|
frontend: 5173
|
|
426
426
|
backend: 5000
|
|
427
427
|
proxy_port: 8001
|
|
428
|
-
|
|
428
|
+
backend_entry: main.py
|
|
429
|
+
api_prefix: /api
|
|
430
|
+
strip_prefix: true
|
|
429
431
|
```
|
|
430
432
|
|
|
433
|
+
Config key notes:
|
|
434
|
+
|
|
435
|
+
- `backend_entry`: Optional Python backend startup file override (for example `main.py`)
|
|
436
|
+
- `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
|
|
437
|
+
- `strip_prefix`: When `true`, strips configured `api_prefix` before forwarding to backend
|
|
438
|
+
- Public tunnel is opt-in at runtime using `--url`; local/WLAN mode works without ngrok/cloudflared.
|
|
439
|
+
|
|
431
440
|
## Backend Auto-Detection
|
|
432
441
|
|
|
433
442
|
Backend port detection runs in this order:
|
|
@@ -439,6 +448,16 @@ Backend port detection runs in this order:
|
|
|
439
448
|
5. Use the best mapped host port automatically, even when internal port is not 5000
|
|
440
449
|
6. If nothing is found, print next-step guidance and exit
|
|
441
450
|
|
|
451
|
+
Python backend entry detection supports automatic discovery of:
|
|
452
|
+
|
|
453
|
+
- `app.py`
|
|
454
|
+
- `main.py`
|
|
455
|
+
- `server.py`
|
|
456
|
+
- `run.py`
|
|
457
|
+
- `manage.py`
|
|
458
|
+
|
|
459
|
+
If `backend_entry` is set in config, DevLinker uses it first and falls back to the discovery list.
|
|
460
|
+
|
|
442
461
|
If Docker SDK is unavailable, Dev Linker falls back to Docker CLI parsing as a compatibility path.
|
|
443
462
|
|
|
444
463
|
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 +483,7 @@ Dev Linker checks backend runtime in this order:
|
|
|
464
483
|
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
465
484
|
2. Docker (`backend/Dockerfile`)
|
|
466
485
|
3. Node (`backend/package.json`)
|
|
467
|
-
4. Python (`backend/requirements.txt` or `
|
|
486
|
+
4. Python (`backend/requirements.txt` or discovered entry file such as `app.py` / `main.py`)
|
|
468
487
|
|
|
469
488
|
Backend startup commands:
|
|
470
489
|
|
|
@@ -472,7 +491,7 @@ Backend startup commands:
|
|
|
472
491
|
- Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
|
|
473
492
|
- Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
|
|
474
493
|
- Node: `npm run dev` (or `npm start` when `dev` is missing)
|
|
475
|
-
- Python: `python app.py`
|
|
494
|
+
- Python: `python <discovered-entry>` (auto: `app.py`, `main.py`, `server.py`, `run.py`, `manage.py`, or configured `backend_entry`)
|
|
476
495
|
|
|
477
496
|
For containerized Flask backends, ensure:
|
|
478
497
|
|
|
@@ -481,7 +500,7 @@ For containerized Flask backends, ensure:
|
|
|
481
500
|
|
|
482
501
|
## Notes
|
|
483
502
|
|
|
484
|
-
- runner.py expects frontend project in frontend and
|
|
503
|
+
- runner.py expects frontend project in `frontend/` and backend project in `backend/`, with automatic Python entry discovery.
|
|
485
504
|
- If those paths do not exist, Dev Linker skips launch and only tries to detect already-running services.
|
|
486
505
|
- If frontend is missing but backend is available, DevLinker continues in backend-only mode.
|
|
487
506
|
- 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,14 +108,11 @@ 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
|
|
118
114
|
- `devlinker --docker` — Auto-start Docker backend
|
|
119
|
-
- `devlinker --no-tunnel` —
|
|
115
|
+
- `devlinker --no-tunnel` — Explicitly force local/WLAN-only mode (no public tunnel)
|
|
120
116
|
- `devlinker --no-lan` — Hide WLAN sharing URL
|
|
121
117
|
- `devlinker --interactive-backend` — Prompt to choose backend if multiple found
|
|
122
118
|
- `devlinker --proxy-port 18000` — Use custom proxy port
|
|
@@ -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
|
|
|
@@ -261,6 +257,20 @@ devlinker --docker
|
|
|
261
257
|
|
|
262
258
|
By default, DevLinker starts **fast local proxy only** (no tunnel). It prints a LAN URL when it can detect a local network interface, and you can share that link with devices on the same Wi-Fi/LAN.
|
|
263
259
|
|
|
260
|
+
LAN-only quick start:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
devlinker
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
You can also pass `--no-tunnel` to explicitly enforce no public tunnel startup:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
devlinker --no-tunnel
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`--no-tunnel` does **not** disable frontend/backend linking. DevLinker still starts the local proxy and routes frontend + backend through the same local entry URL.
|
|
273
|
+
|
|
264
274
|
For access from another network, start with a public tunnel using the `--url` flag:
|
|
265
275
|
|
|
266
276
|
|
|
@@ -279,35 +289,33 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
|
|
|
279
289
|
ℹ Share this link with collaborators.
|
|
280
290
|
```
|
|
281
291
|
|
|
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
292
|
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
293
|
|
|
296
|
-
To force tunnel off (even if
|
|
294
|
+
To force tunnel off (even if `--url` is passed):
|
|
297
295
|
|
|
298
296
|
```bash
|
|
299
297
|
devlinker --no-tunnel
|
|
300
298
|
```
|
|
301
299
|
|
|
302
|
-
When running without `--url`, you’ll see:
|
|
300
|
+
When running without `--url`, you’ll see local/WLAN output with public disabled:
|
|
303
301
|
|
|
304
302
|
```text
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
👉 Run: devlinker --url
|
|
303
|
+
Proxy: http://localhost:8000
|
|
304
|
+
WLAN: http://192.168.1.5:8000
|
|
305
|
+
Public: disabled (use --url)
|
|
309
306
|
```
|
|
310
307
|
|
|
308
|
+
WLAN usage notes:
|
|
309
|
+
|
|
310
|
+
- `localhost` works only on the machine running DevLinker.
|
|
311
|
+
- Use the printed WLAN URL on phones/laptops connected to the same Wi-Fi/LAN.
|
|
312
|
+
|
|
313
|
+
WLAN troubleshooting checklist:
|
|
314
|
+
|
|
315
|
+
1. Ensure both devices are on the same Wi-Fi/subnet.
|
|
316
|
+
2. Allow Python/DevLinker through firewall prompts (Private networks).
|
|
317
|
+
3. Keep Universal Mode enabled (default) so localhost API calls are rewritten safely for LAN/public links.
|
|
318
|
+
|
|
311
319
|
Disable WLAN URL output:
|
|
312
320
|
|
|
313
321
|
```bash
|
|
@@ -373,15 +381,7 @@ fetch("/api/endpoint")
|
|
|
373
381
|
|
|
374
382
|
Do not hardcode backend host URLs in frontend code.
|
|
375
383
|
|
|
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.
|
|
384
|
+
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
385
|
|
|
386
386
|
Use the proxy URL as your single entry point during development:
|
|
387
387
|
|
|
@@ -405,9 +405,18 @@ Example:
|
|
|
405
405
|
frontend: 5173
|
|
406
406
|
backend: 5000
|
|
407
407
|
proxy_port: 8001
|
|
408
|
-
|
|
408
|
+
backend_entry: main.py
|
|
409
|
+
api_prefix: /api
|
|
410
|
+
strip_prefix: true
|
|
409
411
|
```
|
|
410
412
|
|
|
413
|
+
Config key notes:
|
|
414
|
+
|
|
415
|
+
- `backend_entry`: Optional Python backend startup file override (for example `main.py`)
|
|
416
|
+
- `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
|
|
417
|
+
- `strip_prefix`: When `true`, strips configured `api_prefix` before forwarding to backend
|
|
418
|
+
- Public tunnel is opt-in at runtime using `--url`; local/WLAN mode works without ngrok/cloudflared.
|
|
419
|
+
|
|
411
420
|
## Backend Auto-Detection
|
|
412
421
|
|
|
413
422
|
Backend port detection runs in this order:
|
|
@@ -419,6 +428,16 @@ Backend port detection runs in this order:
|
|
|
419
428
|
5. Use the best mapped host port automatically, even when internal port is not 5000
|
|
420
429
|
6. If nothing is found, print next-step guidance and exit
|
|
421
430
|
|
|
431
|
+
Python backend entry detection supports automatic discovery of:
|
|
432
|
+
|
|
433
|
+
- `app.py`
|
|
434
|
+
- `main.py`
|
|
435
|
+
- `server.py`
|
|
436
|
+
- `run.py`
|
|
437
|
+
- `manage.py`
|
|
438
|
+
|
|
439
|
+
If `backend_entry` is set in config, DevLinker uses it first and falls back to the discovery list.
|
|
440
|
+
|
|
422
441
|
If Docker SDK is unavailable, Dev Linker falls back to Docker CLI parsing as a compatibility path.
|
|
423
442
|
|
|
424
443
|
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 +463,7 @@ Dev Linker checks backend runtime in this order:
|
|
|
444
463
|
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
445
464
|
2. Docker (`backend/Dockerfile`)
|
|
446
465
|
3. Node (`backend/package.json`)
|
|
447
|
-
4. Python (`backend/requirements.txt` or `
|
|
466
|
+
4. Python (`backend/requirements.txt` or discovered entry file such as `app.py` / `main.py`)
|
|
448
467
|
|
|
449
468
|
Backend startup commands:
|
|
450
469
|
|
|
@@ -452,7 +471,7 @@ Backend startup commands:
|
|
|
452
471
|
- Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
|
|
453
472
|
- Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
|
|
454
473
|
- Node: `npm run dev` (or `npm start` when `dev` is missing)
|
|
455
|
-
- Python: `python app.py`
|
|
474
|
+
- Python: `python <discovered-entry>` (auto: `app.py`, `main.py`, `server.py`, `run.py`, `manage.py`, or configured `backend_entry`)
|
|
456
475
|
|
|
457
476
|
For containerized Flask backends, ensure:
|
|
458
477
|
|
|
@@ -461,7 +480,7 @@ For containerized Flask backends, ensure:
|
|
|
461
480
|
|
|
462
481
|
## Notes
|
|
463
482
|
|
|
464
|
-
- runner.py expects frontend project in frontend and
|
|
483
|
+
- runner.py expects frontend project in `frontend/` and backend project in `backend/`, with automatic Python entry discovery.
|
|
465
484
|
- If those paths do not exist, Dev Linker skips launch and only tries to detect already-running services.
|
|
466
485
|
- If frontend is missing but backend is available, DevLinker continues in backend-only mode.
|
|
467
486
|
- 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")
|