devlinker 1.4.5__tar.gz → 1.5.0__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 (36) hide show
  1. {devlinker-1.4.5 → devlinker-1.5.0}/PKG-INFO +47 -14
  2. devlinker-1.4.5/devlinker.egg-info/PKG-INFO → devlinker-1.5.0/README.md +32 -28
  3. devlinker-1.5.0/devlinker/dashboard_html.py +84 -0
  4. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detector_ai.py +1 -1
  5. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/devlinker_loader_instant.html +4 -1
  6. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/doctor.py +3 -3
  7. devlinker-1.5.0/devlinker/env_utils.py +36 -0
  8. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/fix.py +8 -2
  9. devlinker-1.5.0/devlinker/fixer.py +27 -0
  10. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/main.py +48 -217
  11. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/proxy.py +33 -121
  12. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/tunnel.py +31 -25
  13. devlinker-1.4.5/README.md → devlinker-1.5.0/devlinker.egg-info/PKG-INFO +61 -8
  14. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/SOURCES.txt +4 -1
  15. devlinker-1.5.0/devlinker.egg-info/requires.txt +25 -0
  16. {devlinker-1.4.5 → devlinker-1.5.0}/pyproject.toml +14 -6
  17. devlinker-1.5.0/tests/test_proxy_runtime.py +105 -0
  18. devlinker-1.4.5/devlinker/fixer.py +0 -19
  19. devlinker-1.4.5/devlinker.egg-info/requires.txt +0 -10
  20. {devlinker-1.4.5 → devlinker-1.5.0}/LICENSE +0 -0
  21. {devlinker-1.4.5 → devlinker-1.5.0}/MANIFEST.in +0 -0
  22. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/__init__.py +0 -0
  23. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/config.py +0 -0
  24. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detection_state.py +0 -0
  25. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detector.py +0 -0
  26. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/devlinker_loader_snippet.html +0 -0
  27. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/inspect.py +0 -0
  28. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/logger.py +0 -0
  29. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/monitor.py +0 -0
  30. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/runner.py +0 -0
  31. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/runtime_api.py +0 -0
  32. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/dependency_links.txt +0 -0
  33. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/entry_points.txt +0 -0
  34. {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/top_level.txt +0 -0
  35. {devlinker-1.4.5 → devlinker-1.5.0}/setup.cfg +0 -0
  36. {devlinker-1.4.5 → devlinker-1.5.0}/setup.py +0 -0
@@ -1,21 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 1.4.5
3
+ Version: 1.5.0
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
- Requires-Python: >=3.7
6
+ Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: click
10
- Requires-Dist: docker
11
10
  Requires-Dist: fastapi
12
11
  Requires-Dist: httpx
13
- Requires-Dist: pyngrok
14
- Requires-Dist: qrcode[pil]
12
+ Requires-Dist: PyYAML
15
13
  Requires-Dist: requests
16
- Requires-Dist: rich
17
14
  Requires-Dist: uvicorn
18
15
  Requires-Dist: websockets
16
+ Provides-Extra: docker
17
+ Requires-Dist: docker; extra == "docker"
18
+ Provides-Extra: tunnel
19
+ Requires-Dist: pyngrok; extra == "tunnel"
20
+ Provides-Extra: ui
21
+ Requires-Dist: rich; extra == "ui"
22
+ Provides-Extra: support
23
+ Requires-Dist: qrcode[pil]; extra == "support"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Provides-Extra: all
27
+ Requires-Dist: devlinker[docker,support,tunnel,ui]; extra == "all"
19
28
  Dynamic: license-file
20
29
 
21
30
  # Dev Linker
@@ -132,7 +141,7 @@ If DevLinker helps you ship faster, consider supporting the project:
132
141
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
133
142
  - `devlinker --frontend 5173 --backend 5000` — Override detected ports
134
143
  - `devlinker --docker` — Auto-start Docker backend
135
- - `devlinker --no-tunnel` — Force local-only mode
144
+ - `devlinker --no-tunnel` — Explicitly force local/WLAN-only mode (no public tunnel)
136
145
  - `devlinker --no-lan` — Hide WLAN sharing URL
137
146
  - `devlinker --interactive-backend` — Prompt to choose backend if multiple found
138
147
  - `devlinker --proxy-port 18000` — Use custom proxy port
@@ -277,6 +286,20 @@ devlinker --docker
277
286
 
278
287
  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.
279
288
 
289
+ LAN-only quick start:
290
+
291
+ ```bash
292
+ devlinker
293
+ ```
294
+
295
+ You can also pass `--no-tunnel` to explicitly enforce no public tunnel startup:
296
+
297
+ ```bash
298
+ devlinker --no-tunnel
299
+ ```
300
+
301
+ `--no-tunnel` does **not** disable frontend/backend linking. DevLinker still starts the local proxy and routes frontend + backend through the same local entry URL.
302
+
280
303
  For access from another network, start with a public tunnel using the `--url` flag:
281
304
 
282
305
 
@@ -297,21 +320,31 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
297
320
 
298
321
  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.
299
322
 
300
- To force tunnel off (even if --url is passed):
323
+ To force tunnel off (even if `--url` is passed):
301
324
 
302
325
  ```bash
303
326
  devlinker --no-tunnel
304
327
  ```
305
328
 
306
- When running without `--url`, you’ll see:
329
+ When running without `--url`, you’ll see local/WLAN output with public disabled:
307
330
 
308
331
  ```text
309
- ⚡ Skipping public tunnel (use --url to enable)
310
-
311
- 💡 Need to share outside network?
312
- 👉 Run: devlinker --url
332
+ Proxy: http://localhost:8000
333
+ WLAN: http://192.168.1.5:8000
334
+ Public: disabled (use --url)
313
335
  ```
314
336
 
337
+ WLAN usage notes:
338
+
339
+ - `localhost` works only on the machine running DevLinker.
340
+ - Use the printed WLAN URL on phones/laptops connected to the same Wi-Fi/LAN.
341
+
342
+ WLAN troubleshooting checklist:
343
+
344
+ 1. Ensure both devices are on the same Wi-Fi/subnet.
345
+ 2. Allow Python/DevLinker through firewall prompts (Private networks).
346
+ 3. Keep Universal Mode enabled (default) so localhost API calls are rewritten safely for LAN/public links.
347
+
315
348
  Disable WLAN URL output:
316
349
 
317
350
  ```bash
@@ -401,7 +434,6 @@ Example:
401
434
  frontend: 5173
402
435
  backend: 5000
403
436
  proxy_port: 8001
404
- tunnel: false
405
437
  backend_entry: main.py
406
438
  api_prefix: /api
407
439
  strip_prefix: true
@@ -412,6 +444,7 @@ Config key notes:
412
444
  - `backend_entry`: Optional Python backend startup file override (for example `main.py`)
413
445
  - `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
414
446
  - `strip_prefix`: When `true`, strips configured `api_prefix` before forwarding to backend
447
+ - Public tunnel is opt-in at runtime using `--url`; local/WLAN mode works without ngrok/cloudflared.
415
448
 
416
449
  ## Backend Auto-Detection
417
450
 
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: devlinker
3
- Version: 1.4.5
4
- Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
5
- Author-email: Mani <mani1028@users.noreply.github.com>
6
- Requires-Python: >=3.7
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: click
10
- Requires-Dist: docker
11
- Requires-Dist: fastapi
12
- Requires-Dist: httpx
13
- Requires-Dist: pyngrok
14
- Requires-Dist: qrcode[pil]
15
- Requires-Dist: requests
16
- Requires-Dist: rich
17
- Requires-Dist: uvicorn
18
- Requires-Dist: websockets
19
- Dynamic: license-file
20
-
21
1
  # Dev Linker
22
2
 
23
3
  Dev Linker starts your local development stack and routes frontend and backend traffic through one proxy URL, with optional LAN and public sharing.
@@ -132,7 +112,7 @@ If DevLinker helps you ship faster, consider supporting the project:
132
112
  - `devlinker fix` — Auto-fix common issues (env, API paths, config)
133
113
  - `devlinker --frontend 5173 --backend 5000` — Override detected ports
134
114
  - `devlinker --docker` — Auto-start Docker backend
135
- - `devlinker --no-tunnel` — Force local-only mode
115
+ - `devlinker --no-tunnel` — Explicitly force local/WLAN-only mode (no public tunnel)
136
116
  - `devlinker --no-lan` — Hide WLAN sharing URL
137
117
  - `devlinker --interactive-backend` — Prompt to choose backend if multiple found
138
118
  - `devlinker --proxy-port 18000` — Use custom proxy port
@@ -277,6 +257,20 @@ devlinker --docker
277
257
 
278
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.
279
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
+
280
274
  For access from another network, start with a public tunnel using the `--url` flag:
281
275
 
282
276
 
@@ -297,21 +291,31 @@ This starts the proxy and opens a public tunnel (Cloudflare or ngrok). The outpu
297
291
 
298
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.
299
293
 
300
- To force tunnel off (even if --url is passed):
294
+ To force tunnel off (even if `--url` is passed):
301
295
 
302
296
  ```bash
303
297
  devlinker --no-tunnel
304
298
  ```
305
299
 
306
- When running without `--url`, you’ll see:
300
+ When running without `--url`, you’ll see local/WLAN output with public disabled:
307
301
 
308
302
  ```text
309
- ⚡ Skipping public tunnel (use --url to enable)
310
-
311
- 💡 Need to share outside network?
312
- 👉 Run: devlinker --url
303
+ Proxy: http://localhost:8000
304
+ WLAN: http://192.168.1.5:8000
305
+ Public: disabled (use --url)
313
306
  ```
314
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
+
315
319
  Disable WLAN URL output:
316
320
 
317
321
  ```bash
@@ -401,7 +405,6 @@ Example:
401
405
  frontend: 5173
402
406
  backend: 5000
403
407
  proxy_port: 8001
404
- tunnel: false
405
408
  backend_entry: main.py
406
409
  api_prefix: /api
407
410
  strip_prefix: true
@@ -412,6 +415,7 @@ Config key notes:
412
415
  - `backend_entry`: Optional Python backend startup file override (for example `main.py`)
413
416
  - `api_prefix`: Prefix DevLinker treats as API traffic (default: `/api`)
414
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.
415
419
 
416
420
  ## Backend Auto-Detection
417
421
 
@@ -0,0 +1,84 @@
1
+ LOGS_DASHBOARD_HTML = """<!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>DevLinker API Logs</title>
7
+ <style>
8
+ :root { --bg:#f4f7fb; --card:#ffffff; --ink:#0f172a; --muted:#64748b; --ok:#065f46; --warn:#92400e; --err:#991b1b; --line:#dbe3ee; }
9
+ body { margin:0; font-family:"Segoe UI","Trebuchet MS",sans-serif; background: radial-gradient(circle at top left,#e7f1ff,transparent 45%), var(--bg); color:var(--ink); }
10
+ .wrap { max-width: 1100px; margin: 28px auto; padding: 0 16px; }
11
+ .card { background: var(--card); border:1px solid var(--line); border-radius:14px; box-shadow: 0 10px 25px rgba(15,23,42,.06); overflow:hidden; }
12
+ h1 { margin:0; font-size: 1.4rem; }
13
+ .head { padding: 14px 16px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--line); }
14
+ .meta { color:var(--muted); font-size:.9rem; }
15
+ table { width:100%; border-collapse: collapse; }
16
+ th,td { padding:10px 12px; border-bottom:1px solid var(--line); text-align:left; font-size:.9rem; }
17
+ th { color:var(--muted); font-weight:600; }
18
+ .s2 { color: var(--ok); font-weight: 700; }
19
+ .s4 { color: var(--warn); font-weight: 700; }
20
+ .s5 { color: var(--err); font-weight: 700; }
21
+ .path { font-family:Consolas, monospace; }
22
+ .empty { padding: 20px; color: var(--muted); }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div class="wrap">
27
+ <div class="card">
28
+ <div class="head">
29
+ <h1>API Logs Dashboard</h1>
30
+ <div class="meta" id="meta">Waiting for traffic...</div>
31
+ </div>
32
+ <div id="content" class="empty">No requests yet.</div>
33
+ </div>
34
+ </div>
35
+ <script>
36
+ function statusClass(code){
37
+ if(code >= 500) return 's5';
38
+ if(code >= 400) return 's4';
39
+ return 's2';
40
+ }
41
+ function ago(ms){
42
+ const d = Date.now() - ms;
43
+ if (d < 1000) return 'now';
44
+ if (d < 60000) return Math.floor(d/1000) + 's ago';
45
+ return Math.floor(d/60000) + 'm ago';
46
+ }
47
+ function render(items){
48
+ const content = document.getElementById('content');
49
+ const meta = document.getElementById('meta');
50
+ if(!items.length){
51
+ content.className = 'empty';
52
+ content.textContent = 'No requests yet.';
53
+ meta.textContent = 'Waiting for traffic...';
54
+ return;
55
+ }
56
+ const rows = items.slice().reverse().map(item => {
57
+ const status = Number(item.status || 0);
58
+ const lat = item.latency_ms == null ? '-' : item.latency_ms + 'ms';
59
+ return '<tr>' +
60
+ '<td>' + (item.method || '-') + '</td>' +
61
+ '<td class="path">' + (item.path || '-') + '</td>' +
62
+ '<td><span class="' + statusClass(status) + '">' + status + '</span></td>' +
63
+ '<td>' + (item.target || '-') + '</td>' +
64
+ '<td>' + lat + '</td>' +
65
+ '<td>' + (item.ts ? ago(item.ts) : '-') + '</td>' +
66
+ '</tr>';
67
+ }).join('');
68
+ content.className = '';
69
+ content.innerHTML = '<table><thead><tr><th>Method</th><th>Path</th><th>Status</th><th>Target</th><th>Latency</th><th>When</th></tr></thead><tbody>' + rows + '</tbody></table>';
70
+ meta.textContent = items.length + ' requests captured';
71
+ }
72
+ async function tick(){
73
+ try{
74
+ const resp = await fetch('/__devlinker/logs', {cache:'no-store'});
75
+ const data = await resp.json();
76
+ render(Array.isArray(data.items) ? data.items : []);
77
+ }catch(_){
78
+ }
79
+ }
80
+ tick();
81
+ setInterval(tick, 1500);
82
+ </script>
83
+ </body>
84
+ </html>"""
@@ -1,4 +1,4 @@
1
- class DevLinkerAI:
1
+ class IssueHints:
2
2
  def analyze_prefix_mismatch(
3
3
  self,
4
4
  api_path: str,
@@ -104,7 +104,10 @@
104
104
  const minTime = 500;
105
105
  const start = Date.now();
106
106
  try {
107
- const resp = await fetch(window.location.href, { headers: { "X-DevLinker-Instant": "1" } });
107
+ const resp = await fetch(window.location.href, {
108
+ headers: { "X-DevLinker-Instant": "1" },
109
+ cache: "no-store",
110
+ });
108
111
  const html = await resp.text();
109
112
  const elapsed = Date.now() - start;
110
113
  const delay = Math.max(0, minTime - elapsed);
@@ -1,5 +1,5 @@
1
1
  import click
2
- from devlinker.detector_ai import DevLinkerAI
2
+ from devlinker.detector_ai import IssueHints
3
3
  from devlinker.logger import print_fix
4
4
  from devlinker.runtime_api import fetch_issues, proxy_base_url
5
5
 
@@ -18,7 +18,7 @@ def doctor():
18
18
 
19
19
  issues = payload.get("items", [])
20
20
  categories = payload.get("categories", {})
21
- ai = DevLinkerAI()
21
+ hints = IssueHints()
22
22
  print("\n🩺 DevLinker Health Dashboard\n" + ("═" * 36))
23
23
  # Grouped status summary
24
24
  if not categories:
@@ -45,6 +45,6 @@ def doctor():
45
45
  issue_text = issue.get("issue", "")
46
46
  if not issue_text:
47
47
  continue
48
- suggestions = ai.analyze_failure(issue_text)
48
+ suggestions = hints.analyze_failure(issue_text)
49
49
  for s in suggestions:
50
50
  print_fix(s)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ _START_MARKER = "# devlinker-managed:start"
6
+ _END_MARKER = "# devlinker-managed:end"
7
+
8
+
9
+ def write_frontend_api_env(proxy_port: int, frontend_dir: str = "frontend") -> bool:
10
+ """Write VITE_API_URL into frontend/.env.local. Returns True when updated."""
11
+ frontend_path = Path(frontend_dir)
12
+ if not frontend_path.is_dir():
13
+ return False
14
+
15
+ env_path = frontend_path / ".env.local"
16
+ managed_block = (
17
+ f"{_START_MARKER}\n"
18
+ f"VITE_API_URL=http://localhost:{proxy_port}\n"
19
+ f"{_END_MARKER}"
20
+ )
21
+
22
+ try:
23
+ existing = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
24
+ if _START_MARKER in existing and _END_MARKER in existing:
25
+ before, _, tail = existing.partition(_START_MARKER)
26
+ _, _, after = tail.partition(_END_MARKER)
27
+ updated = f"{before}{managed_block}{after}"
28
+ elif existing.strip():
29
+ updated = f"{existing.rstrip()}\n\n{managed_block}\n"
30
+ else:
31
+ updated = f"{managed_block}\n"
32
+
33
+ env_path.write_text(updated, encoding="utf-8")
34
+ return True
35
+ except OSError:
36
+ return False
@@ -1,6 +1,6 @@
1
1
  import click
2
2
  from devlinker.fixer import DevLinkerFixer
3
- from devlinker.runtime_api import fetch_issues, proxy_base_url
3
+ from devlinker.runtime_api import fetch_issues, fetch_status, proxy_base_url
4
4
 
5
5
  @click.command()
6
6
  def fix():
@@ -16,9 +16,15 @@ def fix():
16
16
  return
17
17
 
18
18
  issues = payload.get("items", [])
19
+ try:
20
+ status = fetch_status()
21
+ proxy_port = int(status.get("proxy_port") or 8000)
22
+ except Exception:
23
+ proxy_port = 8000
24
+
19
25
  fixer = DevLinkerFixer()
20
26
  print("\n🔧 Applying fixes...")
21
- results = fixer.apply_fixes(issues)
27
+ results = fixer.apply_fixes(issues, proxy_port=proxy_port)
22
28
  print("\n🔧 Fix Results")
23
29
  for r in results:
24
30
  print(f"✔ {r}")
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from devlinker.env_utils import write_frontend_api_env
4
+
5
+
6
+ class DevLinkerFixer:
7
+ def apply_fixes(self, issues: list, proxy_port: int = 8000) -> list[str]:
8
+ fixes: list[str] = []
9
+ wrote_env = False
10
+
11
+ for issue in issues:
12
+ desc = issue[0] if isinstance(issue, (list, tuple)) else issue.get("issue", "")
13
+ lowered = desc.lower()
14
+
15
+ needs_env = "cors" in lowered or "missing '/api'" in lowered or "missing /api" in lowered
16
+ if not needs_env or wrote_env:
17
+ continue
18
+
19
+ if write_frontend_api_env(proxy_port):
20
+ fixes.append(f"Updated frontend/.env.local with VITE_API_URL=http://localhost:{proxy_port}")
21
+ wrote_env = True
22
+ elif "cors" in lowered:
23
+ fixes.append("Route API calls through /api/* using the DevLinker proxy URL")
24
+ else:
25
+ fixes.append("Replace hardcoded localhost URLs with relative /api paths in frontend code")
26
+
27
+ return fixes