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.
- {devlinker-1.4.5 → devlinker-1.5.0}/PKG-INFO +47 -14
- devlinker-1.4.5/devlinker.egg-info/PKG-INFO → devlinker-1.5.0/README.md +32 -28
- devlinker-1.5.0/devlinker/dashboard_html.py +84 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detector_ai.py +1 -1
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/devlinker_loader_instant.html +4 -1
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/doctor.py +3 -3
- devlinker-1.5.0/devlinker/env_utils.py +36 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/fix.py +8 -2
- devlinker-1.5.0/devlinker/fixer.py +27 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/main.py +48 -217
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/proxy.py +33 -121
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/tunnel.py +31 -25
- devlinker-1.4.5/README.md → devlinker-1.5.0/devlinker.egg-info/PKG-INFO +61 -8
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/SOURCES.txt +4 -1
- devlinker-1.5.0/devlinker.egg-info/requires.txt +25 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/pyproject.toml +14 -6
- devlinker-1.5.0/tests/test_proxy_runtime.py +105 -0
- devlinker-1.4.5/devlinker/fixer.py +0 -19
- devlinker-1.4.5/devlinker.egg-info/requires.txt +0 -10
- {devlinker-1.4.5 → devlinker-1.5.0}/LICENSE +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/MANIFEST.in +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/__init__.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/config.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detection_state.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/detector.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/devlinker_loader_snippet.html +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/inspect.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/logger.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/monitor.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/runner.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker/runtime_api.py +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.4.5 → devlinker-1.5.0}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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:
|
|
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` —
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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` —
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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>"""
|
|
@@ -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, {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|