devlinker 0.1.0__tar.gz → 0.2.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-0.1.0 → devlinker-0.2.0}/PKG-INFO +95 -15
- devlinker-0.2.0/README.md +206 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker/__init__.py +1 -1
- devlinker-0.2.0/devlinker/main.py +185 -0
- devlinker-0.2.0/devlinker/runner.py +306 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/PKG-INFO +95 -15
- {devlinker-0.1.0 → devlinker-0.2.0}/pyproject.toml +1 -1
- devlinker-0.1.0/README.md +0 -126
- devlinker-0.1.0/devlinker/main.py +0 -119
- devlinker-0.1.0/devlinker/runner.py +0 -68
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker/detector.py +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker/proxy.py +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker/tunnel.py +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/setup.cfg +0 -0
- {devlinker-0.1.0 → devlinker-0.2.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AI-powered linking and automation tool
|
|
5
5
|
Author-email: Mani <mani1028@users.noreply.github.com>
|
|
6
6
|
Requires-Python: >=3.7
|
|
@@ -16,8 +16,13 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- Launches frontend
|
|
19
|
+
- Launches frontend automatically (when frontend exists)
|
|
20
|
+
- Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
|
|
21
|
+
- Auto-starts Python/Node backends; Docker is manual by default for reliability
|
|
20
22
|
- Detects common frontend/backend ports
|
|
23
|
+
- Supports Docker backend port auto-detection
|
|
24
|
+
- Works with dynamic container host ports
|
|
25
|
+
- No config needed for standard Flask/Docker flows
|
|
21
26
|
- Serves both through one proxy at http://localhost:8000
|
|
22
27
|
- Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
|
|
23
28
|
- Terminal-first workflow
|
|
@@ -26,8 +31,8 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
26
31
|
## Project Structure
|
|
27
32
|
|
|
28
33
|
```text
|
|
29
|
-
|
|
30
|
-
├──
|
|
34
|
+
devlinker/
|
|
35
|
+
├── devlinker/
|
|
31
36
|
│ ├── __init__.py
|
|
32
37
|
│ ├── main.py
|
|
33
38
|
│ ├── runner.py
|
|
@@ -50,7 +55,7 @@ pip install .
|
|
|
50
55
|
After publishing to PyPI:
|
|
51
56
|
|
|
52
57
|
```bash
|
|
53
|
-
pip install
|
|
58
|
+
pip install devlinker
|
|
54
59
|
```
|
|
55
60
|
|
|
56
61
|
## Run
|
|
@@ -62,20 +67,29 @@ devlinker
|
|
|
62
67
|
Typical startup output:
|
|
63
68
|
|
|
64
69
|
```text
|
|
65
|
-
|
|
70
|
+
Dev Linker v0.2.0
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
[INFO] Mode: Auto (Flask + Docker detection)
|
|
73
|
+
[INFO] Booting local services...
|
|
74
|
+
[INFO] Detecting frontend/backend ports...
|
|
75
|
+
[OK] Frontend -> 5173
|
|
76
|
+
[OK] Backend -> 5000
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
[OK] Proxy ready at http://localhost:8000
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
[OK] Tunnel provider: Cloudflare
|
|
81
|
+
[OK] Public URL:
|
|
82
|
+
https://xxxx.trycloudflare.com
|
|
83
|
+
Tip: Press Ctrl+Click to open link
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
[INFO] Share this link with collaborators.
|
|
86
|
+
|
|
87
|
+
DevLinker Ready (in 2.4s)
|
|
88
|
+
Frontend: http://localhost:5173
|
|
89
|
+
Backend: http://localhost:5000
|
|
90
|
+
Proxy: http://localhost:8000
|
|
91
|
+
PUBLIC URL: https://xxxx.trycloudflare.com
|
|
92
|
+
Tip: Press Ctrl+Click to open link
|
|
79
93
|
```
|
|
80
94
|
|
|
81
95
|
Version check:
|
|
@@ -90,6 +104,24 @@ Optional overrides:
|
|
|
90
104
|
devlinker --frontend 5173 --backend 5000
|
|
91
105
|
```
|
|
92
106
|
|
|
107
|
+
Backend override alias:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
devlinker --backend-port 3001
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Enable Docker auto-start explicitly:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
devlinker --docker
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Run local-only mode without tunnel:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
devlinker --no-tunnel
|
|
123
|
+
```
|
|
124
|
+
|
|
93
125
|
If port 8000 is already in use:
|
|
94
126
|
|
|
95
127
|
```bash
|
|
@@ -98,6 +130,11 @@ devlinker --frontend 5173 --backend 5000 --proxy-port 18000
|
|
|
98
130
|
|
|
99
131
|
Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
100
132
|
|
|
133
|
+
```text
|
|
134
|
+
[WARN] Port 8000 in use
|
|
135
|
+
[INFO] Using proxy port: 8001
|
|
136
|
+
```
|
|
137
|
+
|
|
101
138
|
- 8001
|
|
102
139
|
- 8002
|
|
103
140
|
- 18000
|
|
@@ -112,6 +149,49 @@ fetch("/api/endpoint")
|
|
|
112
149
|
|
|
113
150
|
Do not hardcode backend host URLs in frontend code.
|
|
114
151
|
|
|
152
|
+
## Backend Auto-Detection
|
|
153
|
+
|
|
154
|
+
Backend port detection runs in this order:
|
|
155
|
+
|
|
156
|
+
1. Check localhost port 5000
|
|
157
|
+
2. If not found, check Docker port mappings for `->5000/tcp`
|
|
158
|
+
3. Use the mapped host port automatically
|
|
159
|
+
4. If nothing is found, print next-step guidance and exit
|
|
160
|
+
|
|
161
|
+
Detection messages include source labels, for example:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
[OK] Backend detected (Local) -> port 5000
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Example Docker dynamic-port message:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
[WARN] Backend not found on port 5000
|
|
171
|
+
[INFO] Checking Docker containers...
|
|
172
|
+
[OK] Backend detected (Docker) -> port 32768
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Dev Linker checks backend runtime in this order:
|
|
176
|
+
|
|
177
|
+
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
178
|
+
2. Docker (`backend/Dockerfile`)
|
|
179
|
+
3. Node (`backend/package.json`)
|
|
180
|
+
4. Python (`backend/requirements.txt` or `backend/app.py`)
|
|
181
|
+
|
|
182
|
+
Backend startup commands:
|
|
183
|
+
|
|
184
|
+
- Docker Compose (default): manual run `docker compose up --build` in `backend/`
|
|
185
|
+
- Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
|
|
186
|
+
- Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
|
|
187
|
+
- Node: `npm run dev` (or `npm start` when `dev` is missing)
|
|
188
|
+
- Python: `python app.py`
|
|
189
|
+
|
|
190
|
+
For containerized Flask backends, ensure:
|
|
191
|
+
|
|
192
|
+
- App binds to all interfaces: `app.run(host="0.0.0.0", port=5000)`
|
|
193
|
+
- Port mapping is present: `-p 5000:5000`
|
|
194
|
+
|
|
115
195
|
## Notes
|
|
116
196
|
|
|
117
197
|
- runner.py expects frontend project in frontend and Flask app in backend/app.py.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Dev Linker
|
|
2
|
+
|
|
3
|
+
Dev Linker runs frontend and backend dev servers, proxies both through a single local port (8000), and creates a single public URL via Cloudflare or ngrok.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Launches frontend automatically (when frontend exists)
|
|
8
|
+
- Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
|
|
9
|
+
- Auto-starts Python/Node backends; Docker is manual by default for reliability
|
|
10
|
+
- Detects common frontend/backend ports
|
|
11
|
+
- Supports Docker backend port auto-detection
|
|
12
|
+
- Works with dynamic container host ports
|
|
13
|
+
- No config needed for standard Flask/Docker flows
|
|
14
|
+
- Serves both through one proxy at http://localhost:8000
|
|
15
|
+
- Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
|
|
16
|
+
- Terminal-first workflow
|
|
17
|
+
- Supports CLI version output with --version
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
devlinker/
|
|
23
|
+
├── devlinker/
|
|
24
|
+
│ ├── __init__.py
|
|
25
|
+
│ ├── main.py
|
|
26
|
+
│ ├── runner.py
|
|
27
|
+
│ ├── detector.py
|
|
28
|
+
│ ├── proxy.py
|
|
29
|
+
│ └── tunnel.py
|
|
30
|
+
├── setup.py
|
|
31
|
+
├── README.md
|
|
32
|
+
└── requirements.txt
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
For local development:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
After publishing to PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install devlinker
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Run
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
devlinker
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Typical startup output:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Dev Linker v0.2.0
|
|
59
|
+
|
|
60
|
+
[INFO] Mode: Auto (Flask + Docker detection)
|
|
61
|
+
[INFO] Booting local services...
|
|
62
|
+
[INFO] Detecting frontend/backend ports...
|
|
63
|
+
[OK] Frontend -> 5173
|
|
64
|
+
[OK] Backend -> 5000
|
|
65
|
+
|
|
66
|
+
[OK] Proxy ready at http://localhost:8000
|
|
67
|
+
|
|
68
|
+
[OK] Tunnel provider: Cloudflare
|
|
69
|
+
[OK] Public URL:
|
|
70
|
+
https://xxxx.trycloudflare.com
|
|
71
|
+
Tip: Press Ctrl+Click to open link
|
|
72
|
+
|
|
73
|
+
[INFO] Share this link with collaborators.
|
|
74
|
+
|
|
75
|
+
DevLinker Ready (in 2.4s)
|
|
76
|
+
Frontend: http://localhost:5173
|
|
77
|
+
Backend: http://localhost:5000
|
|
78
|
+
Proxy: http://localhost:8000
|
|
79
|
+
PUBLIC URL: https://xxxx.trycloudflare.com
|
|
80
|
+
Tip: Press Ctrl+Click to open link
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Version check:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
devlinker --version
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Optional overrides:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
devlinker --frontend 5173 --backend 5000
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Backend override alias:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
devlinker --backend-port 3001
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Enable Docker auto-start explicitly:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
devlinker --docker
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run local-only mode without tunnel:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
devlinker --no-tunnel
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If port 8000 is already in use:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
devlinker --frontend 5173 --backend 5000 --proxy-port 18000
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
[WARN] Port 8000 in use
|
|
123
|
+
[INFO] Using proxy port: 8001
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- 8001
|
|
127
|
+
- 8002
|
|
128
|
+
- 18000
|
|
129
|
+
|
|
130
|
+
## Important Frontend Rule
|
|
131
|
+
|
|
132
|
+
Frontend requests must use relative API paths:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
fetch("/api/endpoint")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Do not hardcode backend host URLs in frontend code.
|
|
139
|
+
|
|
140
|
+
## Backend Auto-Detection
|
|
141
|
+
|
|
142
|
+
Backend port detection runs in this order:
|
|
143
|
+
|
|
144
|
+
1. Check localhost port 5000
|
|
145
|
+
2. If not found, check Docker port mappings for `->5000/tcp`
|
|
146
|
+
3. Use the mapped host port automatically
|
|
147
|
+
4. If nothing is found, print next-step guidance and exit
|
|
148
|
+
|
|
149
|
+
Detection messages include source labels, for example:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
[OK] Backend detected (Local) -> port 5000
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example Docker dynamic-port message:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
[WARN] Backend not found on port 5000
|
|
159
|
+
[INFO] Checking Docker containers...
|
|
160
|
+
[OK] Backend detected (Docker) -> port 32768
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Dev Linker checks backend runtime in this order:
|
|
164
|
+
|
|
165
|
+
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
166
|
+
2. Docker (`backend/Dockerfile`)
|
|
167
|
+
3. Node (`backend/package.json`)
|
|
168
|
+
4. Python (`backend/requirements.txt` or `backend/app.py`)
|
|
169
|
+
|
|
170
|
+
Backend startup commands:
|
|
171
|
+
|
|
172
|
+
- Docker Compose (default): manual run `docker compose up --build` in `backend/`
|
|
173
|
+
- Dockerfile (default): manual run `docker build -t devlinker-backend .` then `docker run --rm -p 5000:5000 devlinker-backend`
|
|
174
|
+
- Docker Compose/Dockerfile with `--docker`: Dev Linker runs those Docker commands for you
|
|
175
|
+
- Node: `npm run dev` (or `npm start` when `dev` is missing)
|
|
176
|
+
- Python: `python app.py`
|
|
177
|
+
|
|
178
|
+
For containerized Flask backends, ensure:
|
|
179
|
+
|
|
180
|
+
- App binds to all interfaces: `app.run(host="0.0.0.0", port=5000)`
|
|
181
|
+
- Port mapping is present: `-p 5000:5000`
|
|
182
|
+
|
|
183
|
+
## Notes
|
|
184
|
+
|
|
185
|
+
- runner.py expects frontend project in frontend and Flask app in backend/app.py.
|
|
186
|
+
- If those paths do not exist, Dev Linker skips launch and only tries to detect already-running services.
|
|
187
|
+
- Tunnel selection order is: cloudflared (TryCloudflare), then ngrok.
|
|
188
|
+
- If cloudflared is unavailable and ngrok is not configured, Dev Linker prints one-time setup guidance.
|
|
189
|
+
- You may need to set ngrok auth once on your machine using ngrok config add-authtoken <token>.
|
|
190
|
+
- Dev Linker prints a public URL with `ngrok-skip-browser-warning=true` only when ngrok is used.
|
|
191
|
+
- Startup output includes selected tunnel provider (`cloudflare` or `ngrok`).
|
|
192
|
+
- When Dev Linker launches a Vite frontend, it sets `ONELINK=1` to disable Vite HMR WebSockets for stable tunnel behavior.
|
|
193
|
+
|
|
194
|
+
## Real-Time Development Modes
|
|
195
|
+
|
|
196
|
+
### Option 1: Dev Linker sharing mode (recommended)
|
|
197
|
+
|
|
198
|
+
- Run `devlinker` to share one combined frontend/backend URL.
|
|
199
|
+
- Open local Vite URL yourself for instant HMR updates.
|
|
200
|
+
- Share Dev Linker/ngrok URL with others; they can use normal page refresh to see changes.
|
|
201
|
+
|
|
202
|
+
### Option 2: Full remote HMR mode (bypass Dev Linker)
|
|
203
|
+
|
|
204
|
+
- Start frontend and backend manually.
|
|
205
|
+
- Configure Vite `server.proxy` for `/api` to backend.
|
|
206
|
+
- Run `ngrok http <vite-port>` directly so Vite handles WebSocket HMR traffic.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import time
|
|
5
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .detector import check_port, detect_ports, is_vite_port
|
|
11
|
+
from .proxy import start_proxy
|
|
12
|
+
from .runner import detect_backend_port, start_servers
|
|
13
|
+
from .tunnel import start_tunnel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_port_in_use(port: int) -> bool:
|
|
17
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
18
|
+
sock.settimeout(1)
|
|
19
|
+
return sock.connect_ex(("127.0.0.1", port)) == 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _select_proxy_port(requested_port: int) -> int:
|
|
23
|
+
if not _is_port_in_use(requested_port):
|
|
24
|
+
return requested_port
|
|
25
|
+
|
|
26
|
+
if requested_port != 8000:
|
|
27
|
+
raise click.ClickException(
|
|
28
|
+
f"Proxy port {requested_port} is already in use. Choose another with --proxy-port."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
for candidate in (8001, 8002, 18000):
|
|
32
|
+
if not _is_port_in_use(candidate):
|
|
33
|
+
print("[WARN] Port 8000 in use")
|
|
34
|
+
print(f"[INFO] Using proxy port: {candidate}")
|
|
35
|
+
return candidate
|
|
36
|
+
|
|
37
|
+
raise click.ClickException(
|
|
38
|
+
"No free proxy port found in fallback list (8000, 8001, 8002, 18000)."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _with_ngrok_skip_warning(url: str) -> str:
|
|
43
|
+
parts = urlsplit(url)
|
|
44
|
+
if "ngrok" not in parts.netloc:
|
|
45
|
+
return url
|
|
46
|
+
|
|
47
|
+
query = dict(parse_qsl(parts.query, keep_blank_values=True))
|
|
48
|
+
query["ngrok-skip-browser-warning"] = "true"
|
|
49
|
+
new_query = urlencode(query)
|
|
50
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _print_summary(
|
|
54
|
+
frontend_port: int,
|
|
55
|
+
backend_port: int,
|
|
56
|
+
proxy_port: int,
|
|
57
|
+
public_url: str | None,
|
|
58
|
+
startup_seconds: float,
|
|
59
|
+
) -> None:
|
|
60
|
+
print(f"\nDevLinker Ready (in {startup_seconds:.1f}s)")
|
|
61
|
+
print(f"Frontend: http://localhost:{frontend_port}")
|
|
62
|
+
print(f"Backend: http://localhost:{backend_port}")
|
|
63
|
+
print(f"Proxy: http://localhost:{proxy_port}")
|
|
64
|
+
if public_url:
|
|
65
|
+
print(f"PUBLIC URL: {public_url}")
|
|
66
|
+
print("Tip: Press Ctrl+Click to open link")
|
|
67
|
+
else:
|
|
68
|
+
print("Public: unavailable (local proxy still active)")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@click.command()
|
|
72
|
+
@click.version_option(version=__version__, prog_name="devlinker")
|
|
73
|
+
@click.option("--frontend", type=int, default=None, help="Override detected frontend port.")
|
|
74
|
+
@click.option(
|
|
75
|
+
"--backend",
|
|
76
|
+
"--backend-port",
|
|
77
|
+
"backend_port_override",
|
|
78
|
+
type=int,
|
|
79
|
+
default=None,
|
|
80
|
+
help="Override detected backend port.",
|
|
81
|
+
)
|
|
82
|
+
@click.option("--proxy-port", type=int, default=8000, show_default=True, help="Proxy listen port.")
|
|
83
|
+
@click.option(
|
|
84
|
+
"--docker",
|
|
85
|
+
"auto_start_docker",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
help="Auto-start Docker backends (manual Docker is the default).",
|
|
88
|
+
)
|
|
89
|
+
@click.option("--no-tunnel", is_flag=True, help="Skip public tunnel and run local proxy only.")
|
|
90
|
+
@click.option("--debug", is_flag=True, hidden=True, help="Enable debug logging.")
|
|
91
|
+
def cli(
|
|
92
|
+
frontend: int | None,
|
|
93
|
+
backend_port_override: int | None,
|
|
94
|
+
proxy_port: int,
|
|
95
|
+
auto_start_docker: bool,
|
|
96
|
+
no_tunnel: bool,
|
|
97
|
+
debug: bool,
|
|
98
|
+
) -> None:
|
|
99
|
+
started = time.perf_counter()
|
|
100
|
+
print(f"\nDev Linker v{__version__}")
|
|
101
|
+
print("[INFO] Mode: Auto (Flask + Docker detection)")
|
|
102
|
+
print("[INFO] Booting local services...")
|
|
103
|
+
|
|
104
|
+
start_servers(auto_start_docker=auto_start_docker)
|
|
105
|
+
|
|
106
|
+
backend_port = detect_backend_port(
|
|
107
|
+
default_port=5000,
|
|
108
|
+
override_port=backend_port_override,
|
|
109
|
+
debug=debug,
|
|
110
|
+
)
|
|
111
|
+
if backend_port is None:
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
print("[INFO] Detecting frontend/backend ports...")
|
|
115
|
+
frontend_port, backend_port = detect_ports(frontend=frontend, backend=backend_port)
|
|
116
|
+
|
|
117
|
+
if frontend_port is None:
|
|
118
|
+
raise click.ClickException(
|
|
119
|
+
"Frontend not detected on common ports. Start frontend first or set --frontend (example: 5173)."
|
|
120
|
+
)
|
|
121
|
+
if backend_port is None:
|
|
122
|
+
raise click.ClickException(
|
|
123
|
+
"Backend not detected on common ports. Start backend first or set --backend (example: 5000)."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not is_vite_port(frontend_port):
|
|
127
|
+
raise click.ClickException(
|
|
128
|
+
f"Frontend port {frontend_port} is reachable but does not look like a Vite dev server. "
|
|
129
|
+
"Run frontend with Dev Linker or pass the correct --frontend port."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not check_port(backend_port):
|
|
133
|
+
raise click.ClickException(
|
|
134
|
+
f"Backend port {backend_port} is not reachable. Verify backend is running and listening on localhost."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
proxy_port = _select_proxy_port(proxy_port)
|
|
138
|
+
|
|
139
|
+
print(f"[OK] Frontend -> {frontend_port}")
|
|
140
|
+
print(f"[OK] Backend -> {backend_port}\n")
|
|
141
|
+
|
|
142
|
+
print(f"[INFO] Starting proxy on :{proxy_port}...")
|
|
143
|
+
start_proxy(frontend_port, backend_port, proxy_port=proxy_port)
|
|
144
|
+
|
|
145
|
+
# Allow Flask thread to bind before opening tunnel.
|
|
146
|
+
time.sleep(1)
|
|
147
|
+
|
|
148
|
+
print(f"\n[OK] Proxy ready at http://localhost:{proxy_port}\n")
|
|
149
|
+
warning_free_url: str | None = None
|
|
150
|
+
if no_tunnel:
|
|
151
|
+
print("[INFO] Tunnel disabled by --no-tunnel; local proxy only.")
|
|
152
|
+
else:
|
|
153
|
+
try:
|
|
154
|
+
print("[INFO] Opening public tunnel...")
|
|
155
|
+
provider, public_url = start_tunnel(proxy_port)
|
|
156
|
+
warning_free_url = _with_ngrok_skip_warning(public_url)
|
|
157
|
+
provider_label = "Cloudflare" if provider == "cloudflare" else "ngrok"
|
|
158
|
+
print(f"[OK] Tunnel provider: {provider_label}")
|
|
159
|
+
print("[OK] Public URL:")
|
|
160
|
+
print(f" {warning_free_url}\n")
|
|
161
|
+
print("Tip: Press Ctrl+Click to open link")
|
|
162
|
+
print("[INFO] Share this link with collaborators.")
|
|
163
|
+
except RuntimeError as exc:
|
|
164
|
+
print(f"[WARN] Tunnel failed: {exc}")
|
|
165
|
+
print("[INFO] Next step: install cloudflared or configure ngrok auth.")
|
|
166
|
+
print("[INFO] Tip: run 'ngrok config add-authtoken <token>' for ngrok fallback.")
|
|
167
|
+
print(f"[OK] Continuing with local proxy at http://localhost:{proxy_port}")
|
|
168
|
+
|
|
169
|
+
_print_summary(
|
|
170
|
+
frontend_port,
|
|
171
|
+
backend_port,
|
|
172
|
+
proxy_port,
|
|
173
|
+
warning_free_url,
|
|
174
|
+
startup_seconds=time.perf_counter() - started,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
while True:
|
|
179
|
+
time.sleep(1)
|
|
180
|
+
except KeyboardInterrupt:
|
|
181
|
+
print("\n[INFO] Dev Linker stopped.")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
cli()
|