devlinker 1.2.1__tar.gz → 1.2.3__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.1 → devlinker-1.2.3}/PKG-INFO +29 -5
- {devlinker-1.2.1 → devlinker-1.2.3}/README.md +28 -4
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/detector.py +19 -2
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/main.py +8 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/runner.py +170 -38
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/PKG-INFO +29 -5
- {devlinker-1.2.1 → devlinker-1.2.3}/pyproject.toml +1 -1
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/__init__.py +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/proxy.py +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker/tunnel.py +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/setup.cfg +0 -0
- {devlinker-1.2.1 → devlinker-1.2.3}/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.3
|
|
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,7 @@ 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
|
+
- Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
|
|
23
24
|
- Supports Docker backend port auto-detection
|
|
24
25
|
- Works with dynamic container host ports
|
|
25
26
|
- No config needed for standard Flask/Docker flows
|
|
@@ -67,7 +68,7 @@ devlinker
|
|
|
67
68
|
Typical startup output:
|
|
68
69
|
|
|
69
70
|
```text
|
|
70
|
-
Dev Linker
|
|
71
|
+
Dev Linker v1.2.2
|
|
71
72
|
|
|
72
73
|
[INFO] Mode: Auto (Flask + Docker detection)
|
|
73
74
|
[INFO] Booting local services...
|
|
@@ -122,6 +123,18 @@ Run local-only mode without tunnel:
|
|
|
122
123
|
devlinker --no-tunnel
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
Interactive backend selection (when local and Docker are both detected):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
devlinker --interactive-backend
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Disable interactive backend selection (keeps local-first behavior):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
devlinker --no-interactive-backend
|
|
136
|
+
```
|
|
137
|
+
|
|
125
138
|
If port 8000 is already in use:
|
|
126
139
|
|
|
127
140
|
```bash
|
|
@@ -139,6 +152,12 @@ Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
|
139
152
|
- 8002
|
|
140
153
|
- 18000
|
|
141
154
|
|
|
155
|
+
Frontend detection behavior:
|
|
156
|
+
|
|
157
|
+
- Scans Vite defaults and fallback ports (`5173` through `5190`)
|
|
158
|
+
- Also checks common alternatives (`3000`, `4173`, `8080`)
|
|
159
|
+
- Retries during startup to catch slow boot cases
|
|
160
|
+
|
|
142
161
|
## Important Frontend Rule
|
|
143
162
|
|
|
144
163
|
Frontend requests must use relative API paths:
|
|
@@ -154,9 +173,14 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
154
173
|
Backend port detection runs in this order:
|
|
155
174
|
|
|
156
175
|
1. Check localhost port 5000
|
|
157
|
-
2. If not found,
|
|
158
|
-
3.
|
|
159
|
-
4.
|
|
176
|
+
2. If not found, parse all Docker host-to-container port mappings
|
|
177
|
+
3. Rank containers by likely backend identity (name hints like backend/api plus project-name hints)
|
|
178
|
+
4. Use the best mapped host port automatically, even when internal port is not 5000
|
|
179
|
+
5. If nothing is found, print next-step guidance and exit
|
|
180
|
+
|
|
181
|
+
When both Local and Docker backends are available, Dev Linker prompts you to choose one (TTY mode) unless `--no-interactive-backend` is used.
|
|
182
|
+
|
|
183
|
+
If backend detection fails, Dev Linker prints a clear checklist showing what it checked and how to recover.
|
|
160
184
|
|
|
161
185
|
Detection messages include source labels, for example:
|
|
162
186
|
|
|
@@ -8,6 +8,7 @@ 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
|
+
- Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
|
|
11
12
|
- Supports Docker backend port auto-detection
|
|
12
13
|
- Works with dynamic container host ports
|
|
13
14
|
- No config needed for standard Flask/Docker flows
|
|
@@ -55,7 +56,7 @@ devlinker
|
|
|
55
56
|
Typical startup output:
|
|
56
57
|
|
|
57
58
|
```text
|
|
58
|
-
Dev Linker
|
|
59
|
+
Dev Linker v1.2.2
|
|
59
60
|
|
|
60
61
|
[INFO] Mode: Auto (Flask + Docker detection)
|
|
61
62
|
[INFO] Booting local services...
|
|
@@ -110,6 +111,18 @@ Run local-only mode without tunnel:
|
|
|
110
111
|
devlinker --no-tunnel
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
Interactive backend selection (when local and Docker are both detected):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
devlinker --interactive-backend
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Disable interactive backend selection (keeps local-first behavior):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
devlinker --no-interactive-backend
|
|
124
|
+
```
|
|
125
|
+
|
|
113
126
|
If port 8000 is already in use:
|
|
114
127
|
|
|
115
128
|
```bash
|
|
@@ -127,6 +140,12 @@ Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
|
127
140
|
- 8002
|
|
128
141
|
- 18000
|
|
129
142
|
|
|
143
|
+
Frontend detection behavior:
|
|
144
|
+
|
|
145
|
+
- Scans Vite defaults and fallback ports (`5173` through `5190`)
|
|
146
|
+
- Also checks common alternatives (`3000`, `4173`, `8080`)
|
|
147
|
+
- Retries during startup to catch slow boot cases
|
|
148
|
+
|
|
130
149
|
## Important Frontend Rule
|
|
131
150
|
|
|
132
151
|
Frontend requests must use relative API paths:
|
|
@@ -142,9 +161,14 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
142
161
|
Backend port detection runs in this order:
|
|
143
162
|
|
|
144
163
|
1. Check localhost port 5000
|
|
145
|
-
2. If not found,
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
164
|
+
2. If not found, parse all Docker host-to-container port mappings
|
|
165
|
+
3. Rank containers by likely backend identity (name hints like backend/api plus project-name hints)
|
|
166
|
+
4. Use the best mapped host port automatically, even when internal port is not 5000
|
|
167
|
+
5. If nothing is found, print next-step guidance and exit
|
|
168
|
+
|
|
169
|
+
When both Local and Docker backends are available, Dev Linker prompts you to choose one (TTY mode) unless `--no-interactive-backend` is used.
|
|
170
|
+
|
|
171
|
+
If backend detection fails, Dev Linker prints a clear checklist showing what it checked and how to recover.
|
|
148
172
|
|
|
149
173
|
Detection messages include source labels, for example:
|
|
150
174
|
|
|
@@ -41,6 +41,18 @@ def _pick_open_port(
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def _ordered_unique_ports(*port_groups: Iterable[int]) -> list[int]:
|
|
45
|
+
ordered: list[int] = []
|
|
46
|
+
seen: set[int] = set()
|
|
47
|
+
for group in port_groups:
|
|
48
|
+
for port in group:
|
|
49
|
+
if port in seen:
|
|
50
|
+
continue
|
|
51
|
+
seen.add(port)
|
|
52
|
+
ordered.append(port)
|
|
53
|
+
return ordered
|
|
54
|
+
|
|
55
|
+
|
|
44
56
|
def detect_ports(
|
|
45
57
|
frontend: Optional[int] = None,
|
|
46
58
|
backend: Optional[int] = None,
|
|
@@ -48,8 +60,13 @@ def detect_ports(
|
|
|
48
60
|
delay_seconds: float = 1.0,
|
|
49
61
|
) -> Tuple[Optional[int], Optional[int]]:
|
|
50
62
|
"""Detect frontend and backend ports with retry support for slow startups."""
|
|
51
|
-
frontend_ports =
|
|
52
|
-
|
|
63
|
+
frontend_ports = _ordered_unique_ports(
|
|
64
|
+
range(5173, 5191),
|
|
65
|
+
(3000, 4173, 8080),
|
|
66
|
+
)
|
|
67
|
+
backend_ports = _ordered_unique_ports(
|
|
68
|
+
(5000, 8000, 8001, 8080, 8081, 3001),
|
|
69
|
+
)
|
|
53
70
|
|
|
54
71
|
selected_frontend = frontend
|
|
55
72
|
selected_backend = backend
|
|
@@ -87,6 +87,12 @@ def _print_summary(
|
|
|
87
87
|
help="Auto-start Docker backends (manual Docker is the default).",
|
|
88
88
|
)
|
|
89
89
|
@click.option("--no-tunnel", is_flag=True, help="Skip public tunnel and run local proxy only.")
|
|
90
|
+
@click.option(
|
|
91
|
+
"--interactive-backend/--no-interactive-backend",
|
|
92
|
+
default=True,
|
|
93
|
+
show_default=True,
|
|
94
|
+
help="Prompt to choose backend when local and Docker candidates are both available.",
|
|
95
|
+
)
|
|
90
96
|
@click.option("--debug", is_flag=True, hidden=True, help="Enable debug logging.")
|
|
91
97
|
def cli(
|
|
92
98
|
frontend: int | None,
|
|
@@ -94,6 +100,7 @@ def cli(
|
|
|
94
100
|
proxy_port: int,
|
|
95
101
|
auto_start_docker: bool,
|
|
96
102
|
no_tunnel: bool,
|
|
103
|
+
interactive_backend: bool,
|
|
97
104
|
debug: bool,
|
|
98
105
|
) -> None:
|
|
99
106
|
started = time.perf_counter()
|
|
@@ -106,6 +113,7 @@ def cli(
|
|
|
106
113
|
backend_port = detect_backend_port(
|
|
107
114
|
default_port=5000,
|
|
108
115
|
override_port=backend_port_override,
|
|
116
|
+
interactive=interactive_backend,
|
|
109
117
|
debug=debug,
|
|
110
118
|
)
|
|
111
119
|
if backend_port is None:
|
|
@@ -36,34 +36,71 @@ def is_port_open(port: int) -> bool:
|
|
|
36
36
|
return sock.connect_ex(("localhost", port)) == 0
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
def _extract_port_mappings(ports_text: str) -> list[tuple[int, int]]:
|
|
40
|
+
"""
|
|
41
|
+
Extracts host and container ports from docker ps output.
|
|
42
|
+
Handles:
|
|
43
|
+
- 0.0.0.0:8000->8000/tcp
|
|
44
|
+
- [::]:8000->8000/tcp
|
|
45
|
+
- :::8000->8000/tcp
|
|
46
|
+
- 8000->8000/tcp
|
|
47
|
+
|
|
48
|
+
Skips unmapped ports (e.g., "8000/tcp" without a host binding).
|
|
49
|
+
"""
|
|
50
|
+
# Skip if no host mappings present (unmapped ports are not useful externally).
|
|
51
|
+
if "->" not in ports_text:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
# This regex specifically captures the host port (Group 1) and container port (Group 2)
|
|
55
|
+
# while optionally ignoring any IP binding prefix.
|
|
56
|
+
pattern = re.compile(
|
|
57
|
+
r"(?:(?:\d{1,3}(?:\.\d{1,3}){3}|\[[a-fA-F0-9:]+\]|:::):)?(\d+)->(\d+)/(?:tcp|udp)"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
mappings: list[tuple[int, int]] = []
|
|
61
|
+
for match in pattern.finditer(ports_text):
|
|
62
|
+
host_port = int(match.group(1))
|
|
63
|
+
container_port = int(match.group(2))
|
|
64
|
+
mappings.append((host_port, container_port))
|
|
65
|
+
return mappings
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _container_priority(name: str, container_port: int, default_container_port: int) -> int:
|
|
69
|
+
score = 0
|
|
70
|
+
lowered_name = name.lower()
|
|
71
|
+
if "backend" in lowered_name:
|
|
72
|
+
score += 6
|
|
73
|
+
if any(token in lowered_name for token in ("api", "server", "svc", "service")):
|
|
74
|
+
score += 3
|
|
75
|
+
if container_port == default_container_port:
|
|
76
|
+
score += 2
|
|
77
|
+
|
|
78
|
+
project_hint = Path.cwd().name.lower()
|
|
79
|
+
if len(project_hint) >= 3 and project_hint in lowered_name:
|
|
80
|
+
score += 5
|
|
46
81
|
|
|
82
|
+
return score
|
|
47
83
|
|
|
48
|
-
|
|
84
|
+
|
|
85
|
+
def get_docker_backend_candidates(
|
|
49
86
|
default_container_port: int = 5000,
|
|
50
87
|
debug: bool = False,
|
|
51
|
-
) -> tuple[
|
|
88
|
+
) -> list[tuple[str, int, int]]:
|
|
52
89
|
try:
|
|
53
90
|
output = subprocess.check_output( # noqa: S603
|
|
54
91
|
["docker", "ps", "--format", "{{.Names}}\t{{.Ports}}"],
|
|
55
92
|
stderr=subprocess.DEVNULL,
|
|
56
93
|
).decode("utf-8", errors="ignore")
|
|
57
94
|
except Exception:
|
|
58
|
-
return
|
|
95
|
+
return []
|
|
59
96
|
|
|
60
97
|
_debug_log(debug, "docker ps port map output:")
|
|
61
98
|
if debug:
|
|
62
99
|
for raw_line in output.splitlines():
|
|
63
100
|
_debug_log(True, raw_line)
|
|
64
101
|
|
|
65
|
-
candidates: list[tuple[str, int]] = []
|
|
66
|
-
for line in output.splitlines():
|
|
102
|
+
candidates: list[tuple[str, int, int, int]] = []
|
|
103
|
+
for line_index, line in enumerate(output.splitlines()):
|
|
67
104
|
stripped = line.strip()
|
|
68
105
|
if not stripped:
|
|
69
106
|
continue
|
|
@@ -76,23 +113,86 @@ def get_docker_backend_port(
|
|
|
76
113
|
continue
|
|
77
114
|
name, ports = parts[0], parts[1]
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
if
|
|
81
|
-
|
|
116
|
+
mappings = _extract_port_mappings(ports)
|
|
117
|
+
if not mappings:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
for host_port, container_port in mappings:
|
|
121
|
+
candidates.append((name, host_port, container_port, line_index))
|
|
122
|
+
_debug_log(
|
|
123
|
+
debug,
|
|
124
|
+
(
|
|
125
|
+
f"Candidate Docker port mapping: container='{name}', "
|
|
126
|
+
f"host={host_port}, container={container_port}"
|
|
127
|
+
),
|
|
128
|
+
)
|
|
82
129
|
|
|
83
130
|
if not candidates:
|
|
84
|
-
return
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
# Score candidates by likely backend identity and prefer newest on ties.
|
|
134
|
+
ranked = sorted(
|
|
135
|
+
candidates,
|
|
136
|
+
key=lambda item: (-_container_priority(item[0], item[2], default_container_port), item[3]),
|
|
137
|
+
)
|
|
138
|
+
ordered: list[tuple[str, int, int]] = []
|
|
139
|
+
seen: set[tuple[str, int, int]] = set()
|
|
140
|
+
for name, host_port, container_port, _line_index in ranked:
|
|
141
|
+
key = (name, host_port, container_port)
|
|
142
|
+
if key in seen:
|
|
143
|
+
continue
|
|
144
|
+
seen.add(key)
|
|
145
|
+
ordered.append(key)
|
|
146
|
+
|
|
147
|
+
if ordered:
|
|
148
|
+
name, host_port, container_port = ordered[0]
|
|
149
|
+
_debug_log(
|
|
150
|
+
debug,
|
|
151
|
+
(
|
|
152
|
+
f"Selected Docker container '{name}' with host port {host_port} "
|
|
153
|
+
f"(container port {container_port})"
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return ordered
|
|
85
158
|
|
|
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
159
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
160
|
+
def _choose_backend_candidate(
|
|
161
|
+
local_port: int,
|
|
162
|
+
docker_candidates: list[tuple[str, int, int]],
|
|
163
|
+
debug: bool = False,
|
|
164
|
+
) -> tuple[str, int, str | None, int | None]:
|
|
165
|
+
print("[INFO] Multiple backends detected:")
|
|
166
|
+
print(f"1. Local (localhost:{local_port})")
|
|
167
|
+
for index, (container_name, host_port, container_port) in enumerate(docker_candidates, start=2):
|
|
168
|
+
print(f"{index}. Docker: {container_name} (localhost:{host_port} -> {container_port})")
|
|
169
|
+
print("[INFO] Select backend [default: 1]: ", end="", flush=True)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
raw = input().strip()
|
|
173
|
+
except EOFError:
|
|
174
|
+
_debug_log(debug, "Interactive prompt unavailable (EOF); using local backend fallback")
|
|
175
|
+
return "local", local_port, None, None
|
|
176
|
+
|
|
177
|
+
if raw == "":
|
|
178
|
+
return "local", local_port, None, None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
selection = int(raw)
|
|
182
|
+
except ValueError:
|
|
183
|
+
_log("warn", "Invalid selection; using local backend")
|
|
184
|
+
return "local", local_port, None, None
|
|
185
|
+
|
|
186
|
+
if selection == 1:
|
|
187
|
+
return "local", local_port, None, None
|
|
188
|
+
|
|
189
|
+
docker_index = selection - 2
|
|
190
|
+
if 0 <= docker_index < len(docker_candidates):
|
|
191
|
+
container_name, host_port, container_port = docker_candidates[docker_index]
|
|
192
|
+
return "docker", host_port, container_name, container_port
|
|
193
|
+
|
|
194
|
+
_log("warn", "Selection out of range; using local backend")
|
|
195
|
+
return "local", local_port, None, None
|
|
96
196
|
|
|
97
197
|
|
|
98
198
|
def _wait_for_port(port: int, retries: int = 5, delay_seconds: float = 1.0, debug: bool = False) -> bool:
|
|
@@ -107,6 +207,7 @@ def _wait_for_port(port: int, retries: int = 5, delay_seconds: float = 1.0, debu
|
|
|
107
207
|
def detect_backend_port(
|
|
108
208
|
default_port: int = 5000,
|
|
109
209
|
override_port: int | None = None,
|
|
210
|
+
interactive: bool = False,
|
|
110
211
|
debug: bool = False,
|
|
111
212
|
) -> int | None:
|
|
112
213
|
started = time.perf_counter()
|
|
@@ -118,32 +219,63 @@ def detect_backend_port(
|
|
|
118
219
|
|
|
119
220
|
_log("info", "Checking backend...")
|
|
120
221
|
_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
222
|
|
|
126
|
-
|
|
223
|
+
local_open = is_port_open(default_port)
|
|
224
|
+
if not local_open:
|
|
225
|
+
_log("warn", f"Backend not found on port {default_port}")
|
|
226
|
+
|
|
127
227
|
_log("info", "Checking Docker containers...")
|
|
128
228
|
_debug_log(debug, f"Scanned Docker container target port: {default_port}")
|
|
129
229
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if
|
|
134
|
-
|
|
230
|
+
docker_candidates = get_docker_backend_candidates(default_container_port=default_port, debug=debug)
|
|
231
|
+
|
|
232
|
+
if local_open and docker_candidates:
|
|
233
|
+
if interactive and sys.stdin.isatty():
|
|
234
|
+
source, selected_port, container_name, container_port = _choose_backend_candidate(
|
|
235
|
+
local_port=default_port,
|
|
236
|
+
docker_candidates=docker_candidates,
|
|
237
|
+
debug=debug,
|
|
238
|
+
)
|
|
239
|
+
if source == "local":
|
|
240
|
+
_log("ok", f"Backend detected (Local) -> port {selected_port}")
|
|
241
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
242
|
+
return selected_port
|
|
243
|
+
|
|
244
|
+
if _wait_for_port(selected_port, retries=5, delay_seconds=1.0, debug=debug):
|
|
245
|
+
_log("ok", f"Backend detected (Docker: {container_name}) -> port {selected_port}")
|
|
246
|
+
_debug_log(debug, f"Selected Docker container port: {container_port}")
|
|
247
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
248
|
+
return selected_port
|
|
249
|
+
_log("warn", f"Docker mapped port {selected_port} found but backend is not ready yet")
|
|
250
|
+
else:
|
|
251
|
+
_log("info", f"Docker candidate also detected: {docker_candidates[0][0]} -> {docker_candidates[0][1]}")
|
|
252
|
+
_debug_log(debug, "Interactive backend picker disabled or no TTY; using local-first priority")
|
|
253
|
+
_log("ok", f"Backend detected (Local) -> port {default_port}")
|
|
254
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
255
|
+
return default_port
|
|
256
|
+
|
|
257
|
+
if local_open:
|
|
258
|
+
_log("ok", f"Backend detected (Local) -> port {default_port}")
|
|
259
|
+
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
260
|
+
return default_port
|
|
261
|
+
|
|
262
|
+
if docker_candidates:
|
|
263
|
+
container_name, docker_port, container_port = docker_candidates[0]
|
|
264
|
+
if len(docker_candidates) > 1:
|
|
265
|
+
_log("warn", f"Multiple Docker containers published ports ({len(docker_candidates)} candidates)")
|
|
135
266
|
_log("info", f"Using: {container_name}")
|
|
136
267
|
if _wait_for_port(docker_port, retries=5, delay_seconds=1.0, debug=debug):
|
|
137
|
-
_log("ok", f"Backend detected (Docker) -> port {docker_port}")
|
|
268
|
+
_log("ok", f"Backend detected (Docker: {container_name}) -> port {docker_port}")
|
|
269
|
+
_debug_log(debug, f"Selected Docker container port: {container_port}")
|
|
138
270
|
_log("info", f"Backend detected in {time.perf_counter() - started:.1f}s")
|
|
139
271
|
return docker_port
|
|
140
272
|
_log("warn", f"Docker mapped port {docker_port} found but backend is not ready yet")
|
|
141
273
|
|
|
142
|
-
_log("error", "
|
|
274
|
+
_log("error", "No backend found")
|
|
143
275
|
print("Checked:")
|
|
144
276
|
print(f"- localhost:{default_port}")
|
|
145
277
|
print("- Docker containers")
|
|
146
|
-
print("
|
|
278
|
+
print("Tip:")
|
|
147
279
|
print(" - Start Flask: python app.py")
|
|
148
280
|
print(" - OR expose Docker port: -p 5000:5000")
|
|
149
281
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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,7 @@ 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
|
+
- Detects Vite frontend across dynamic fallback ports (5173-5190, plus common alternatives)
|
|
23
24
|
- Supports Docker backend port auto-detection
|
|
24
25
|
- Works with dynamic container host ports
|
|
25
26
|
- No config needed for standard Flask/Docker flows
|
|
@@ -67,7 +68,7 @@ devlinker
|
|
|
67
68
|
Typical startup output:
|
|
68
69
|
|
|
69
70
|
```text
|
|
70
|
-
Dev Linker
|
|
71
|
+
Dev Linker v1.2.2
|
|
71
72
|
|
|
72
73
|
[INFO] Mode: Auto (Flask + Docker detection)
|
|
73
74
|
[INFO] Booting local services...
|
|
@@ -122,6 +123,18 @@ Run local-only mode without tunnel:
|
|
|
122
123
|
devlinker --no-tunnel
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
Interactive backend selection (when local and Docker are both detected):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
devlinker --interactive-backend
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Disable interactive backend selection (keeps local-first behavior):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
devlinker --no-interactive-backend
|
|
136
|
+
```
|
|
137
|
+
|
|
125
138
|
If port 8000 is already in use:
|
|
126
139
|
|
|
127
140
|
```bash
|
|
@@ -139,6 +152,12 @@ Default behavior also tries fallback ports automatically when 8000 is busy:
|
|
|
139
152
|
- 8002
|
|
140
153
|
- 18000
|
|
141
154
|
|
|
155
|
+
Frontend detection behavior:
|
|
156
|
+
|
|
157
|
+
- Scans Vite defaults and fallback ports (`5173` through `5190`)
|
|
158
|
+
- Also checks common alternatives (`3000`, `4173`, `8080`)
|
|
159
|
+
- Retries during startup to catch slow boot cases
|
|
160
|
+
|
|
142
161
|
## Important Frontend Rule
|
|
143
162
|
|
|
144
163
|
Frontend requests must use relative API paths:
|
|
@@ -154,9 +173,14 @@ Do not hardcode backend host URLs in frontend code.
|
|
|
154
173
|
Backend port detection runs in this order:
|
|
155
174
|
|
|
156
175
|
1. Check localhost port 5000
|
|
157
|
-
2. If not found,
|
|
158
|
-
3.
|
|
159
|
-
4.
|
|
176
|
+
2. If not found, parse all Docker host-to-container port mappings
|
|
177
|
+
3. Rank containers by likely backend identity (name hints like backend/api plus project-name hints)
|
|
178
|
+
4. Use the best mapped host port automatically, even when internal port is not 5000
|
|
179
|
+
5. If nothing is found, print next-step guidance and exit
|
|
180
|
+
|
|
181
|
+
When both Local and Docker backends are available, Dev Linker prompts you to choose one (TTY mode) unless `--no-interactive-backend` is used.
|
|
182
|
+
|
|
183
|
+
If backend detection fails, Dev Linker prints a clear checklist showing what it checked and how to recover.
|
|
160
184
|
|
|
161
185
|
Detection messages include source labels, for example:
|
|
162
186
|
|
|
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
|