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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlinker
3
- Version: 0.1.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 and backend (when frontend and backend/app.py exist)
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
- onelink/
30
- ├── onelink/
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 dev-linker
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
- Dev Linker v0.1.0
70
+ Dev Linker v0.2.0
66
71
 
67
- 🚀 Starting services...
68
- 🔍 Detecting services...
69
- Frontend -> 5173
70
- Backend -> 5000
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
- 🌐 Proxy ready at http://localhost:8000
78
+ [OK] Proxy ready at http://localhost:8000
73
79
 
74
- Tunnel provider: Cloudflare
75
- 🌍 Public URL:
76
- https://xxxx.trycloudflare.com
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
- 👉 Share this link with anyone
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.
@@ -1,6 +1,6 @@
1
1
  """Dev Linker package."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "1.2.0"
4
4
 
5
5
  __all__ = [
6
6
  "main",
@@ -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()