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.
Files changed (40) hide show
  1. {devlinker-1.4.4/devlinker.egg-info → devlinker-1.4.6}/PKG-INFO +59 -40
  2. {devlinker-1.4.4 → devlinker-1.4.6}/README.md +58 -39
  3. devlinker-1.4.6/devlinker/config.py +55 -0
  4. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/detection_state.py +39 -0
  5. devlinker-1.4.6/devlinker/detector_ai.py +46 -0
  6. devlinker-1.4.6/devlinker/doctor.py +50 -0
  7. devlinker-1.4.6/devlinker/fix.py +26 -0
  8. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/fixer.py +4 -12
  9. devlinker-1.4.6/devlinker/inspect.py +25 -0
  10. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/main.py +36 -19
  11. devlinker-1.4.6/devlinker/monitor.py +40 -0
  12. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/proxy.py +486 -84
  13. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/runner.py +106 -11
  14. devlinker-1.4.6/devlinker/runtime_api.py +69 -0
  15. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/tunnel.py +7 -2
  16. {devlinker-1.4.4 → devlinker-1.4.6/devlinker.egg-info}/PKG-INFO +59 -40
  17. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/SOURCES.txt +1 -2
  18. {devlinker-1.4.4 → devlinker-1.4.6}/pyproject.toml +1 -1
  19. devlinker-1.4.6/setup.py +4 -0
  20. devlinker-1.4.4/devlinker/config.py +0 -17
  21. devlinker-1.4.4/devlinker/detector_ai.py +0 -23
  22. devlinker-1.4.4/devlinker/doctor.py +0 -27
  23. devlinker-1.4.4/devlinker/fix.py +0 -16
  24. devlinker-1.4.4/devlinker/global_state.py +0 -5
  25. devlinker-1.4.4/devlinker/inspect.py +0 -14
  26. devlinker-1.4.4/devlinker/monitor.py +0 -19
  27. devlinker-1.4.4/devlinker/share.py +0 -64
  28. devlinker-1.4.4/setup.py +0 -9
  29. {devlinker-1.4.4 → devlinker-1.4.6}/LICENSE +0 -0
  30. {devlinker-1.4.4 → devlinker-1.4.6}/MANIFEST.in +0 -0
  31. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/__init__.py +0 -0
  32. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/detector.py +0 -0
  33. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/devlinker_loader_instant.html +0 -0
  34. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/devlinker_loader_snippet.html +0 -0
  35. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker/logger.py +0 -0
  36. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/dependency_links.txt +0 -0
  37. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/entry_points.txt +0 -0
  38. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/requires.txt +0 -0
  39. {devlinker-1.4.4 → devlinker-1.4.6}/devlinker.egg-info/top_level.txt +0 -0
  40. {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.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
- - 🔁 **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,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` — Force local-only mode
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.1
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 --url is passed):
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
- ⚡ Skipping public tunnel (use --url to enable)
326
-
327
- 💡 Need to share outside network?
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 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.
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
- tunnel: false
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 `backend/app.py`)
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 Python app in backend/app.py.
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
- - 🔁 **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,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` — Force local-only mode
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.1
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 --url is passed):
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
- ⚡ Skipping public tunnel (use --url to enable)
306
-
307
- 💡 Need to share outside network?
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 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.
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
- tunnel: false
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 `backend/app.py`)
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 Python app in backend/app.py.
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
- 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")