devlinker 1.2.0__tar.gz → 1.2.1__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.2.0 → devlinker-1.2.1}/PKG-INFO +51 -7
- {devlinker-1.2.0 → devlinker-1.2.1}/README.md +50 -6
- devlinker-1.2.1/devlinker/__init__.py +16 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker/main.py +57 -23
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker/runner.py +127 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/PKG-INFO +51 -7
- {devlinker-1.2.0 → devlinker-1.2.1}/pyproject.toml +1 -1
- devlinker-1.2.0/devlinker/__init__.py +0 -11
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker/detector.py +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker/proxy.py +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker/tunnel.py +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/setup.cfg +0 -0
- {devlinker-1.2.0 → devlinker-1.2.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -20,6 +20,9 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
20
20
|
- Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
|
|
21
21
|
- Auto-starts Python/Node backends; Docker is manual by default for reliability
|
|
22
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
|
|
23
26
|
- Serves both through one proxy at http://localhost:8000
|
|
24
27
|
- Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
|
|
25
28
|
- Terminal-first workflow
|
|
@@ -28,8 +31,8 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
28
31
|
## Project Structure
|
|
29
32
|
|
|
30
33
|
```text
|
|
31
|
-
|
|
32
|
-
├──
|
|
34
|
+
devlinker/
|
|
35
|
+
├── devlinker/
|
|
33
36
|
│ ├── __init__.py
|
|
34
37
|
│ ├── main.py
|
|
35
38
|
│ ├── runner.py
|
|
@@ -52,7 +55,7 @@ pip install .
|
|
|
52
55
|
After publishing to PyPI:
|
|
53
56
|
|
|
54
57
|
```bash
|
|
55
|
-
pip install
|
|
58
|
+
pip install devlinker
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
## Run
|
|
@@ -64,8 +67,9 @@ devlinker
|
|
|
64
67
|
Typical startup output:
|
|
65
68
|
|
|
66
69
|
```text
|
|
67
|
-
Dev Linker
|
|
70
|
+
Dev Linker v0.2.0
|
|
68
71
|
|
|
72
|
+
[INFO] Mode: Auto (Flask + Docker detection)
|
|
69
73
|
[INFO] Booting local services...
|
|
70
74
|
[INFO] Detecting frontend/backend ports...
|
|
71
75
|
[OK] Frontend -> 5173
|
|
@@ -76,14 +80,16 @@ Dev Linker v1.2.0
|
|
|
76
80
|
[OK] Tunnel provider: Cloudflare
|
|
77
81
|
[OK] Public URL:
|
|
78
82
|
https://xxxx.trycloudflare.com
|
|
83
|
+
Tip: Press Ctrl+Click to open link
|
|
79
84
|
|
|
80
85
|
[INFO] Share this link with collaborators.
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
DevLinker Ready (in 2.4s)
|
|
83
88
|
Frontend: http://localhost:5173
|
|
84
89
|
Backend: http://localhost:5000
|
|
85
90
|
Proxy: http://localhost:8000
|
|
86
|
-
|
|
91
|
+
PUBLIC URL: https://xxxx.trycloudflare.com
|
|
92
|
+
Tip: Press Ctrl+Click to open link
|
|
87
93
|
```
|
|
88
94
|
|
|
89
95
|
Version check:
|
|
@@ -98,12 +104,24 @@ Optional overrides:
|
|
|
98
104
|
devlinker --frontend 5173 --backend 5000
|
|
99
105
|
```
|
|
100
106
|
|
|
107
|
+
Backend override alias:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
devlinker --backend-port 3001
|
|
111
|
+
```
|
|
112
|
+
|
|
101
113
|
Enable Docker auto-start explicitly:
|
|
102
114
|
|
|
103
115
|
```bash
|
|
104
116
|
devlinker --docker
|
|
105
117
|
```
|
|
106
118
|
|
|
119
|
+
Run local-only mode without tunnel:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
devlinker --no-tunnel
|
|
123
|
+
```
|
|
124
|
+
|
|
107
125
|
If port 8000 is already in use:
|
|
108
126
|
|
|
109
127
|
```bash
|
|
@@ -112,6 +130,11 @@ devlinker --frontend 5173 --backend 5000 --proxy-port 18000
|
|
|
112
130
|
|
|
113
131
|
Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
114
132
|
|
|
133
|
+
```text
|
|
134
|
+
[WARN] Port 8000 in use
|
|
135
|
+
[INFO] Using proxy port: 8001
|
|
136
|
+
```
|
|
137
|
+
|
|
115
138
|
- 8001
|
|
116
139
|
- 8002
|
|
117
140
|
- 18000
|
|
@@ -128,6 +151,27 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
128
151
|
|
|
129
152
|
## Backend Auto-Detection
|
|
130
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
|
+
|
|
131
175
|
Dev Linker checks backend runtime in this order:
|
|
132
176
|
|
|
133
177
|
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
@@ -8,6 +8,9 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
8
8
|
- Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
|
|
9
9
|
- Auto-starts Python/Node backends; Docker is manual by default for reliability
|
|
10
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
|
|
11
14
|
- Serves both through one proxy at http://localhost:8000
|
|
12
15
|
- Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
|
|
13
16
|
- Terminal-first workflow
|
|
@@ -16,8 +19,8 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
16
19
|
## Project Structure
|
|
17
20
|
|
|
18
21
|
```text
|
|
19
|
-
|
|
20
|
-
├──
|
|
22
|
+
devlinker/
|
|
23
|
+
├── devlinker/
|
|
21
24
|
│ ├── __init__.py
|
|
22
25
|
│ ├── main.py
|
|
23
26
|
│ ├── runner.py
|
|
@@ -40,7 +43,7 @@ pip install .
|
|
|
40
43
|
After publishing to PyPI:
|
|
41
44
|
|
|
42
45
|
```bash
|
|
43
|
-
pip install
|
|
46
|
+
pip install devlinker
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
## Run
|
|
@@ -52,8 +55,9 @@ devlinker
|
|
|
52
55
|
Typical startup output:
|
|
53
56
|
|
|
54
57
|
```text
|
|
55
|
-
Dev Linker
|
|
58
|
+
Dev Linker v0.2.0
|
|
56
59
|
|
|
60
|
+
[INFO] Mode: Auto (Flask + Docker detection)
|
|
57
61
|
[INFO] Booting local services...
|
|
58
62
|
[INFO] Detecting frontend/backend ports...
|
|
59
63
|
[OK] Frontend -> 5173
|
|
@@ -64,14 +68,16 @@ Dev Linker v1.2.0
|
|
|
64
68
|
[OK] Tunnel provider: Cloudflare
|
|
65
69
|
[OK] Public URL:
|
|
66
70
|
https://xxxx.trycloudflare.com
|
|
71
|
+
Tip: Press Ctrl+Click to open link
|
|
67
72
|
|
|
68
73
|
[INFO] Share this link with collaborators.
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
DevLinker Ready (in 2.4s)
|
|
71
76
|
Frontend: http://localhost:5173
|
|
72
77
|
Backend: http://localhost:5000
|
|
73
78
|
Proxy: http://localhost:8000
|
|
74
|
-
|
|
79
|
+
PUBLIC URL: https://xxxx.trycloudflare.com
|
|
80
|
+
Tip: Press Ctrl+Click to open link
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
Version check:
|
|
@@ -86,12 +92,24 @@ Optional overrides:
|
|
|
86
92
|
devlinker --frontend 5173 --backend 5000
|
|
87
93
|
```
|
|
88
94
|
|
|
95
|
+
Backend override alias:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
devlinker --backend-port 3001
|
|
99
|
+
```
|
|
100
|
+
|
|
89
101
|
Enable Docker auto-start explicitly:
|
|
90
102
|
|
|
91
103
|
```bash
|
|
92
104
|
devlinker --docker
|
|
93
105
|
```
|
|
94
106
|
|
|
107
|
+
Run local-only mode without tunnel:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
devlinker --no-tunnel
|
|
111
|
+
```
|
|
112
|
+
|
|
95
113
|
If port 8000 is already in use:
|
|
96
114
|
|
|
97
115
|
```bash
|
|
@@ -100,6 +118,11 @@ devlinker --frontend 5173 --backend 5000 --proxy-port 18000
|
|
|
100
118
|
|
|
101
119
|
Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
102
120
|
|
|
121
|
+
```text
|
|
122
|
+
[WARN] Port 8000 in use
|
|
123
|
+
[INFO] Using proxy port: 8001
|
|
124
|
+
```
|
|
125
|
+
|
|
103
126
|
- 8001
|
|
104
127
|
- 8002
|
|
105
128
|
- 18000
|
|
@@ -116,6 +139,27 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
116
139
|
|
|
117
140
|
## Backend Auto-Detection
|
|
118
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
|
+
|
|
119
163
|
Dev Linker checks backend runtime in this order:
|
|
120
164
|
|
|
121
165
|
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
@@ -9,7 +9,7 @@ import click
|
|
|
9
9
|
from . import __version__
|
|
10
10
|
from .detector import check_port, detect_ports, is_vite_port
|
|
11
11
|
from .proxy import start_proxy
|
|
12
|
-
from .runner import start_servers
|
|
12
|
+
from .runner import detect_backend_port, start_servers
|
|
13
13
|
from .tunnel import start_tunnel
|
|
14
14
|
|
|
15
15
|
|
|
@@ -30,7 +30,8 @@ def _select_proxy_port(requested_port: int) -> int:
|
|
|
30
30
|
|
|
31
31
|
for candidate in (8001, 8002, 18000):
|
|
32
32
|
if not _is_port_in_use(candidate):
|
|
33
|
-
print(
|
|
33
|
+
print("[WARN] Port 8000 in use")
|
|
34
|
+
print(f"[INFO] Using proxy port: {candidate}")
|
|
34
35
|
return candidate
|
|
35
36
|
|
|
36
37
|
raise click.ClickException(
|
|
@@ -54,13 +55,15 @@ def _print_summary(
|
|
|
54
55
|
backend_port: int,
|
|
55
56
|
proxy_port: int,
|
|
56
57
|
public_url: str | None,
|
|
58
|
+
startup_seconds: float,
|
|
57
59
|
) -> None:
|
|
58
|
-
print("\
|
|
60
|
+
print(f"\nDevLinker Ready (in {startup_seconds:.1f}s)")
|
|
59
61
|
print(f"Frontend: http://localhost:{frontend_port}")
|
|
60
62
|
print(f"Backend: http://localhost:{backend_port}")
|
|
61
63
|
print(f"Proxy: http://localhost:{proxy_port}")
|
|
62
64
|
if public_url:
|
|
63
|
-
print(f"
|
|
65
|
+
print(f"PUBLIC URL: {public_url}")
|
|
66
|
+
print("Tip: Press Ctrl+Click to open link")
|
|
64
67
|
else:
|
|
65
68
|
print("Public: unavailable (local proxy still active)")
|
|
66
69
|
|
|
@@ -68,7 +71,14 @@ def _print_summary(
|
|
|
68
71
|
@click.command()
|
|
69
72
|
@click.version_option(version=__version__, prog_name="devlinker")
|
|
70
73
|
@click.option("--frontend", type=int, default=None, help="Override detected frontend port.")
|
|
71
|
-
@click.option(
|
|
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
|
+
)
|
|
72
82
|
@click.option("--proxy-port", type=int, default=8000, show_default=True, help="Proxy listen port.")
|
|
73
83
|
@click.option(
|
|
74
84
|
"--docker",
|
|
@@ -76,19 +86,33 @@ def _print_summary(
|
|
|
76
86
|
is_flag=True,
|
|
77
87
|
help="Auto-start Docker backends (manual Docker is the default).",
|
|
78
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.")
|
|
79
91
|
def cli(
|
|
80
92
|
frontend: int | None,
|
|
81
|
-
|
|
93
|
+
backend_port_override: int | None,
|
|
82
94
|
proxy_port: int,
|
|
83
95
|
auto_start_docker: bool,
|
|
96
|
+
no_tunnel: bool,
|
|
97
|
+
debug: bool,
|
|
84
98
|
) -> None:
|
|
99
|
+
started = time.perf_counter()
|
|
85
100
|
print(f"\nDev Linker v{__version__}")
|
|
101
|
+
print("[INFO] Mode: Auto (Flask + Docker detection)")
|
|
86
102
|
print("[INFO] Booting local services...")
|
|
87
103
|
|
|
88
104
|
start_servers(auto_start_docker=auto_start_docker)
|
|
89
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
|
+
|
|
90
114
|
print("[INFO] Detecting frontend/backend ports...")
|
|
91
|
-
frontend_port, backend_port = detect_ports(frontend=frontend, backend=
|
|
115
|
+
frontend_port, backend_port = detect_ports(frontend=frontend, backend=backend_port)
|
|
92
116
|
|
|
93
117
|
if frontend_port is None:
|
|
94
118
|
raise click.ClickException(
|
|
@@ -123,22 +147,32 @@ def cli(
|
|
|
123
147
|
|
|
124
148
|
print(f"\n[OK] Proxy ready at http://localhost:{proxy_port}\n")
|
|
125
149
|
warning_free_url: str | None = None
|
|
126
|
-
|
|
127
|
-
print("[INFO]
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
)
|
|
142
176
|
|
|
143
177
|
try:
|
|
144
178
|
while True:
|
|
@@ -2,9 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import shutil
|
|
7
|
+
import socket
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
10
|
+
import time
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
from typing import List
|
|
10
13
|
|
|
@@ -22,6 +25,130 @@ def _log(level: str, message: str) -> None:
|
|
|
22
25
|
print(f"{prefix} {message}")
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
def _debug_log(enabled: bool, message: str) -> None:
|
|
29
|
+
if enabled:
|
|
30
|
+
print(f"[DEBUG] {message}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_port_open(port: int) -> bool:
|
|
34
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
35
|
+
sock.settimeout(1)
|
|
36
|
+
return sock.connect_ex(("localhost", port)) == 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _extract_host_port(ports_text: str, container_port: int) -> int | None:
|
|
40
|
+
# Covers typical Docker mappings: 0.0.0.0:32768->5000/tcp, [::]:32768->5000/tcp
|
|
41
|
+
pattern = rf"(?:0\.0\.0\.0|127\.0\.0\.1|\[::\]|::):(\d+)->{container_port}/tcp"
|
|
42
|
+
match = re.search(pattern, ports_text)
|
|
43
|
+
if match:
|
|
44
|
+
return int(match.group(1))
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_docker_backend_port(
|
|
49
|
+
default_container_port: int = 5000,
|
|
50
|
+
debug: bool = False,
|
|
51
|
+
) -> tuple[int, str, int] | None:
|
|
52
|
+
try:
|
|
53
|
+
output = subprocess.check_output( # noqa: S603
|
|
54
|
+
["docker", "ps", "--format", "{{.Names}}\t{{.Ports}}"],
|
|
55
|
+
stderr=subprocess.DEVNULL,
|
|
56
|
+
).decode("utf-8", errors="ignore")
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
_debug_log(debug, "docker ps port map output:")
|
|
61
|
+
if debug:
|
|
62
|
+
for raw_line in output.splitlines():
|
|
63
|
+
_debug_log(True, raw_line)
|
|
64
|
+
|
|
65
|
+
candidates: list[tuple[str, int]] = []
|
|
66
|
+
for line in output.splitlines():
|
|
67
|
+
stripped = line.strip()
|
|
68
|
+
if not stripped:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if "\t" in stripped:
|
|
72
|
+
name, ports = stripped.split("\t", 1)
|
|
73
|
+
else:
|
|
74
|
+
parts = stripped.split(maxsplit=1)
|
|
75
|
+
if len(parts) != 2:
|
|
76
|
+
continue
|
|
77
|
+
name, ports = parts[0], parts[1]
|
|
78
|
+
|
|
79
|
+
host_port = _extract_host_port(ports, default_container_port)
|
|
80
|
+
if host_port is not None:
|
|
81
|
+
candidates.append((name, host_port))
|
|
82
|
+
|
|
83
|
+
if not candidates:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
# Prefer container names that look like backend services.
|
|
87
|
+
for name, port in candidates:
|
|
88
|
+
if "backend" in name.lower():
|
|
89
|
+
_debug_log(debug, f"Selected Docker backend container '{name}' on host port {port}")
|
|
90
|
+
return port, name, len(candidates)
|
|
91
|
+
|
|
92
|
+
# docker ps is already newest-first; fallback to first match.
|
|
93
|
+
name, port = candidates[0]
|
|
94
|
+
_debug_log(debug, f"Selected first Docker match '{name}' on host port {port}")
|
|
95
|
+
return port, name, len(candidates)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _wait_for_port(port: int, retries: int = 5, delay_seconds: float = 1.0, debug: bool = False) -> bool:
|
|
99
|
+
for attempt in range(1, retries + 1):
|
|
100
|
+
if is_port_open(port):
|
|
101
|
+
return True
|
|
102
|
+
_debug_log(debug, f"Port {port} not open yet (attempt {attempt}/{retries})")
|
|
103
|
+
time.sleep(delay_seconds)
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detect_backend_port(
|
|
108
|
+
default_port: int = 5000,
|
|
109
|
+
override_port: int | None = None,
|
|
110
|
+
debug: bool = False,
|
|
111
|
+
) -> int | None:
|
|
112
|
+
started = time.perf_counter()
|
|
113
|
+
|
|
114
|
+
if override_port is not None:
|
|
115
|
+
_log("info", f"Using backend port override: {override_port}")
|
|
116
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
117
|
+
return override_port
|
|
118
|
+
|
|
119
|
+
_log("info", "Checking backend...")
|
|
120
|
+
_debug_log(debug, f"Scanned local port: {default_port}")
|
|
121
|
+
if is_port_open(default_port):
|
|
122
|
+
_log("ok", f"Backend detected (Local) -> port {default_port}")
|
|
123
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
124
|
+
return default_port
|
|
125
|
+
|
|
126
|
+
_log("warn", f"Backend not found on port {default_port}")
|
|
127
|
+
_log("info", "Checking Docker containers...")
|
|
128
|
+
_debug_log(debug, f"Scanned Docker container target port: {default_port}")
|
|
129
|
+
|
|
130
|
+
docker_match = get_docker_backend_port(default_container_port=default_port, debug=debug)
|
|
131
|
+
if docker_match is not None:
|
|
132
|
+
docker_port, container_name, match_count = docker_match
|
|
133
|
+
if match_count > 1:
|
|
134
|
+
_log("warn", "Multiple backend containers found")
|
|
135
|
+
_log("info", f"Using: {container_name}")
|
|
136
|
+
if _wait_for_port(docker_port, retries=5, delay_seconds=1.0, debug=debug):
|
|
137
|
+
_log("ok", f"Backend detected (Docker) -> port {docker_port}")
|
|
138
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
139
|
+
return docker_port
|
|
140
|
+
_log("warn", f"Docker mapped port {docker_port} found but backend is not ready yet")
|
|
141
|
+
|
|
142
|
+
_log("error", "Backend not detected")
|
|
143
|
+
print("Checked:")
|
|
144
|
+
print(f"- localhost:{default_port}")
|
|
145
|
+
print("- Docker containers")
|
|
146
|
+
print("Next step:")
|
|
147
|
+
print(" - Start Flask: python app.py")
|
|
148
|
+
print(" - OR expose Docker port: -p 5000:5000")
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
25
152
|
def _detect_backend_mode(backend_path: Path) -> str | None:
|
|
26
153
|
compose_files = (
|
|
27
154
|
"docker-compose.yml",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
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
|
|
@@ -20,6 +20,9 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
20
20
|
- Auto-detects backend runtime (Docker Compose, Dockerfile, Node, or Python)
|
|
21
21
|
- Auto-starts Python/Node backends; Docker is manual by default for reliability
|
|
22
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
|
|
23
26
|
- Serves both through one proxy at http://localhost:8000
|
|
24
27
|
- Creates a public tunnel for sharing (Cloudflare first, ngrok fallback)
|
|
25
28
|
- Terminal-first workflow
|
|
@@ -28,8 +31,8 @@ Dev Linker runs frontend and backend dev servers, proxies both through a single
|
|
|
28
31
|
## Project Structure
|
|
29
32
|
|
|
30
33
|
```text
|
|
31
|
-
|
|
32
|
-
├──
|
|
34
|
+
devlinker/
|
|
35
|
+
├── devlinker/
|
|
33
36
|
│ ├── __init__.py
|
|
34
37
|
│ ├── main.py
|
|
35
38
|
│ ├── runner.py
|
|
@@ -52,7 +55,7 @@ pip install .
|
|
|
52
55
|
After publishing to PyPI:
|
|
53
56
|
|
|
54
57
|
```bash
|
|
55
|
-
pip install
|
|
58
|
+
pip install devlinker
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
## Run
|
|
@@ -64,8 +67,9 @@ devlinker
|
|
|
64
67
|
Typical startup output:
|
|
65
68
|
|
|
66
69
|
```text
|
|
67
|
-
Dev Linker
|
|
70
|
+
Dev Linker v0.2.0
|
|
68
71
|
|
|
72
|
+
[INFO] Mode: Auto (Flask + Docker detection)
|
|
69
73
|
[INFO] Booting local services...
|
|
70
74
|
[INFO] Detecting frontend/backend ports...
|
|
71
75
|
[OK] Frontend -> 5173
|
|
@@ -76,14 +80,16 @@ Dev Linker v1.2.0
|
|
|
76
80
|
[OK] Tunnel provider: Cloudflare
|
|
77
81
|
[OK] Public URL:
|
|
78
82
|
https://xxxx.trycloudflare.com
|
|
83
|
+
Tip: Press Ctrl+Click to open link
|
|
79
84
|
|
|
80
85
|
[INFO] Share this link with collaborators.
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
DevLinker Ready (in 2.4s)
|
|
83
88
|
Frontend: http://localhost:5173
|
|
84
89
|
Backend: http://localhost:5000
|
|
85
90
|
Proxy: http://localhost:8000
|
|
86
|
-
|
|
91
|
+
PUBLIC URL: https://xxxx.trycloudflare.com
|
|
92
|
+
Tip: Press Ctrl+Click to open link
|
|
87
93
|
```
|
|
88
94
|
|
|
89
95
|
Version check:
|
|
@@ -98,12 +104,24 @@ Optional overrides:
|
|
|
98
104
|
devlinker --frontend 5173 --backend 5000
|
|
99
105
|
```
|
|
100
106
|
|
|
107
|
+
Backend override alias:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
devlinker --backend-port 3001
|
|
111
|
+
```
|
|
112
|
+
|
|
101
113
|
Enable Docker auto-start explicitly:
|
|
102
114
|
|
|
103
115
|
```bash
|
|
104
116
|
devlinker --docker
|
|
105
117
|
```
|
|
106
118
|
|
|
119
|
+
Run local-only mode without tunnel:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
devlinker --no-tunnel
|
|
123
|
+
```
|
|
124
|
+
|
|
107
125
|
If port 8000 is already in use:
|
|
108
126
|
|
|
109
127
|
```bash
|
|
@@ -112,6 +130,11 @@ devlinker --frontend 5173 --backend 5000 --proxy-port 18000
|
|
|
112
130
|
|
|
113
131
|
Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
114
132
|
|
|
133
|
+
```text
|
|
134
|
+
[WARN] Port 8000 in use
|
|
135
|
+
[INFO] Using proxy port: 8001
|
|
136
|
+
```
|
|
137
|
+
|
|
115
138
|
- 8001
|
|
116
139
|
- 8002
|
|
117
140
|
- 18000
|
|
@@ -128,6 +151,27 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
128
151
|
|
|
129
152
|
## Backend Auto-Detection
|
|
130
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
|
+
|
|
131
175
|
Dev Linker checks backend runtime in this order:
|
|
132
176
|
|
|
133
177
|
1. Docker Compose (`backend/docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|