devs-common 3.3.1__tar.gz → 3.3.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.
- {devs_common-3.3.1 → devs_common-3.3.3}/PKG-INFO +1 -1
- devs_common-3.3.3/devs_common/core/__init__.py +22 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/core/container.py +173 -131
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common.egg-info/PKG-INFO +1 -1
- {devs_common-3.3.1 → devs_common-3.3.3}/pyproject.toml +1 -1
- devs_common-3.3.1/devs_common/core/__init__.py +0 -13
- {devs_common-3.3.1 → devs_common-3.3.3}/LICENSE +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/README.md +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/__init__.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/config.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/core/project.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/core/workspace.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/devs_config.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/exceptions.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/Dockerfile +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/devcontainer.json +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/copy-ssh.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/setup-workspace.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/start-tunnel.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/tunnel-status.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/vscode-settings-reference.md +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/__init__.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/config_hash.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/console.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/devcontainer.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/devcontainer_template.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/docker_client.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/file_utils.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/git_utils.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common/utils/repo_cache.py +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common.egg-info/SOURCES.txt +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common.egg-info/dependency_links.txt +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common.egg-info/requires.txt +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/devs_common.egg-info/top_level.txt +0 -0
- {devs_common-3.3.1 → devs_common-3.3.3}/setup.cfg +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Core classes for devs ecosystem."""
|
|
2
|
+
|
|
3
|
+
from .project import Project, ProjectInfo
|
|
4
|
+
from .workspace import WorkspaceManager
|
|
5
|
+
from .container import (
|
|
6
|
+
ContainerManager,
|
|
7
|
+
ContainerInfo,
|
|
8
|
+
make_tunnel_name,
|
|
9
|
+
get_container_workspace_dir,
|
|
10
|
+
kill_tunnel_processes,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Project",
|
|
15
|
+
"ProjectInfo",
|
|
16
|
+
"WorkspaceManager",
|
|
17
|
+
"ContainerManager",
|
|
18
|
+
"ContainerInfo",
|
|
19
|
+
"make_tunnel_name",
|
|
20
|
+
"get_container_workspace_dir",
|
|
21
|
+
"kill_tunnel_processes",
|
|
22
|
+
]
|
|
@@ -47,6 +47,55 @@ from .project import Project
|
|
|
47
47
|
console = get_console()
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def make_tunnel_name(container_name: str) -> str:
|
|
51
|
+
"""Derive a VS Code tunnel name from a container name (max 20 chars).
|
|
52
|
+
|
|
53
|
+
VS Code tunnel names have a 20 character limit. We keep the dev name
|
|
54
|
+
suffix and shorten the project portion if needed.
|
|
55
|
+
|
|
56
|
+
Container names are like dev-ideonate-devs-sally.
|
|
57
|
+
"""
|
|
58
|
+
name = container_name.replace(".", "-").replace("_", "-")
|
|
59
|
+
if len(name) <= 20:
|
|
60
|
+
return name
|
|
61
|
+
# Keep the last segment (dev name) and truncate the rest
|
|
62
|
+
parts = name.rsplit("-", 1)
|
|
63
|
+
if len(parts) == 2:
|
|
64
|
+
suffix = f"-{parts[1]}"
|
|
65
|
+
budget = 20 - len(suffix)
|
|
66
|
+
if budget >= 3:
|
|
67
|
+
return parts[0][:budget].rstrip("-") + suffix
|
|
68
|
+
return name[:20]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_container_workspace_dir(container_name: str) -> str:
|
|
72
|
+
"""Derive the workspace path inside a container from its name.
|
|
73
|
+
|
|
74
|
+
Container name 'dev-ideonate-devs-bob' -> '/workspaces/ideonate-devs-bob'.
|
|
75
|
+
"""
|
|
76
|
+
if container_name.startswith("dev-"):
|
|
77
|
+
workspace_name = container_name[4:]
|
|
78
|
+
else:
|
|
79
|
+
workspace_name = container_name
|
|
80
|
+
return f"/workspaces/{workspace_name}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def kill_tunnel_processes(container_name: str) -> None:
|
|
84
|
+
"""Kill all tunnel processes in a container.
|
|
85
|
+
|
|
86
|
+
Uses both 'code tunnel kill' (service-level) and pkill (process-level)
|
|
87
|
+
to ensure detached shell wrappers and tunnel processes are fully terminated.
|
|
88
|
+
"""
|
|
89
|
+
subprocess.run(
|
|
90
|
+
['docker', 'exec', container_name, '/usr/local/bin/code', 'tunnel', 'kill'],
|
|
91
|
+
capture_output=True
|
|
92
|
+
)
|
|
93
|
+
subprocess.run(
|
|
94
|
+
['docker', 'exec', container_name, 'pkill', '-f', 'code tunnel'],
|
|
95
|
+
capture_output=True
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
50
99
|
class ContainerInfo:
|
|
51
100
|
"""Information about a devcontainer."""
|
|
52
101
|
|
|
@@ -852,27 +901,11 @@ class ContainerManager:
|
|
|
852
901
|
)
|
|
853
902
|
|
|
854
903
|
def _get_tunnel_name(self, dev_name: str) -> str:
|
|
855
|
-
"""Generate a VS Code tunnel name
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
project_prefix = self.config.project_prefix if self.config else "dev"
|
|
861
|
-
tunnel_name = f"{project_prefix}-{self.project.info.name}-{dev_name}"
|
|
862
|
-
# Sanitize: tunnel names can only contain alphanumeric, dash, underscore
|
|
863
|
-
tunnel_name = tunnel_name.replace(".", "-").replace("_", "-")
|
|
864
|
-
|
|
865
|
-
if len(tunnel_name) > 20:
|
|
866
|
-
suffix = f"-{dev_name}"
|
|
867
|
-
prefix_budget = 20 - len(suffix)
|
|
868
|
-
if prefix_budget >= 3:
|
|
869
|
-
project_part = f"{project_prefix}-{self.project.info.name}"
|
|
870
|
-
project_part = project_part[:prefix_budget].rstrip("-")
|
|
871
|
-
tunnel_name = f"{project_part}{suffix}"
|
|
872
|
-
else:
|
|
873
|
-
tunnel_name = tunnel_name[:20]
|
|
874
|
-
|
|
875
|
-
return tunnel_name
|
|
904
|
+
"""Generate a VS Code tunnel name for this project/dev combo."""
|
|
905
|
+
container_name = self.project.get_container_name(
|
|
906
|
+
dev_name, self.config.project_prefix if self.config else "dev"
|
|
907
|
+
)
|
|
908
|
+
return make_tunnel_name(container_name)
|
|
876
909
|
|
|
877
910
|
def _run_tunnel_auth(self, container_name: str, debug: bool = False) -> bool:
|
|
878
911
|
"""Run interactive VS Code tunnel authentication in a container.
|
|
@@ -909,27 +942,15 @@ class ContainerManager:
|
|
|
909
942
|
return False
|
|
910
943
|
|
|
911
944
|
def _kill_tunnel_processes(self, container_name: str) -> None:
|
|
912
|
-
"""Kill all tunnel processes in the container.
|
|
945
|
+
"""Kill all tunnel processes in the container."""
|
|
946
|
+
kill_tunnel_processes(container_name)
|
|
913
947
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
'code tunnel kill' provides.
|
|
917
|
-
"""
|
|
918
|
-
subprocess.run(
|
|
919
|
-
['docker', 'exec', container_name, '/usr/local/bin/code', 'tunnel', 'kill'],
|
|
920
|
-
capture_output=True
|
|
921
|
-
)
|
|
922
|
-
subprocess.run(
|
|
923
|
-
['docker', 'exec', container_name, 'pkill', '-f', 'code tunnel'],
|
|
924
|
-
capture_output=True
|
|
925
|
-
)
|
|
926
|
-
|
|
927
|
-
def _start_and_poll_tunnel(self, container_name: str, dev_name: str, tunnel_cmd: str,
|
|
928
|
-
log_file: str, vscode_cmd: str, web_url: str, debug: bool = False) -> bool:
|
|
948
|
+
def _start_and_poll_tunnel(self, container_name: str, tunnel_cmd: str,
|
|
949
|
+
log_file: str, debug: bool = False) -> dict:
|
|
929
950
|
"""Start a detached tunnel and poll the log for startup confirmation.
|
|
930
951
|
|
|
931
952
|
Returns:
|
|
932
|
-
|
|
953
|
+
Dict with 'status' key: 'running', 'auth_required', or 'starting'.
|
|
933
954
|
"""
|
|
934
955
|
# Kill any stale tunnel processes and clear the log before starting
|
|
935
956
|
self._kill_tunnel_processes(container_name)
|
|
@@ -945,7 +966,6 @@ class ContainerManager:
|
|
|
945
966
|
check=True
|
|
946
967
|
)
|
|
947
968
|
|
|
948
|
-
console.print("[dim]Waiting for tunnel to start...[/dim]")
|
|
949
969
|
for i in range(15):
|
|
950
970
|
time.sleep(1)
|
|
951
971
|
result = subprocess.run(
|
|
@@ -955,19 +975,11 @@ class ContainerManager:
|
|
|
955
975
|
output = result.stdout
|
|
956
976
|
|
|
957
977
|
if 'Open this link' in output or 'vscode.dev/tunnel' in output:
|
|
958
|
-
|
|
959
|
-
console.print("")
|
|
960
|
-
console.print("[bold]Connect:[/bold]")
|
|
961
|
-
console.print(f" Desktop: {vscode_cmd}")
|
|
962
|
-
console.print(f" Browser: {web_url}")
|
|
963
|
-
console.print("")
|
|
964
|
-
console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
|
|
965
|
-
console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
|
|
966
|
-
return True
|
|
978
|
+
return {'status': 'running'}
|
|
967
979
|
|
|
968
980
|
if 'log in' in output.lower() or 'device' in output.lower() or 'invalid' in output.lower():
|
|
969
981
|
self._kill_tunnel_processes(container_name)
|
|
970
|
-
return
|
|
982
|
+
return {'status': 'auth_required'}
|
|
971
983
|
|
|
972
984
|
if debug and output.strip():
|
|
973
985
|
console.print(f"[dim]Log ({i+1}s): {output.strip()[-100:]}[/dim]")
|
|
@@ -977,16 +989,30 @@ class ContainerManager:
|
|
|
977
989
|
['docker', 'exec', container_name, 'cat', log_file],
|
|
978
990
|
capture_output=True, text=True
|
|
979
991
|
)
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
992
|
+
return {'status': 'starting', 'log': result.stdout.strip()}
|
|
993
|
+
|
|
994
|
+
def get_tunnel_info(self, dev_name: str, workspace_dir: Path, debug: bool = False,
|
|
995
|
+
live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> dict:
|
|
996
|
+
"""Get tunnel connection info (names, paths) without starting anything.
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
Dict with container_name, tunnel_name, container_workspace_dir,
|
|
1000
|
+
vscode_cmd, web_url.
|
|
1001
|
+
"""
|
|
1002
|
+
container_name, container_workspace_dir = self._prepare_container_exec(
|
|
1003
|
+
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1004
|
+
)
|
|
1005
|
+
tunnel_name = self._get_tunnel_name(dev_name)
|
|
1006
|
+
return {
|
|
1007
|
+
'container_name': container_name,
|
|
1008
|
+
'tunnel_name': tunnel_name,
|
|
1009
|
+
'container_workspace_dir': container_workspace_dir,
|
|
1010
|
+
'vscode_cmd': f"code --remote tunnel+{tunnel_name} {container_workspace_dir}",
|
|
1011
|
+
'web_url': f"https://vscode.dev/tunnel/{tunnel_name}{container_workspace_dir}",
|
|
1012
|
+
}
|
|
987
1013
|
|
|
988
1014
|
def tunnel_auth(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
|
|
989
|
-
"""Authenticate VS Code tunnel (interactive, per-container setup).
|
|
1015
|
+
"""Authenticate VS Code tunnel (interactive CLI, per-container setup).
|
|
990
1016
|
|
|
991
1017
|
Args:
|
|
992
1018
|
dev_name: Development environment name
|
|
@@ -999,26 +1025,22 @@ class ContainerManager:
|
|
|
999
1025
|
ContainerError: If authentication fails
|
|
1000
1026
|
"""
|
|
1001
1027
|
try:
|
|
1002
|
-
|
|
1003
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1004
|
-
)
|
|
1005
|
-
|
|
1006
|
-
tunnel_name = self._get_tunnel_name(dev_name)
|
|
1028
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1007
1029
|
|
|
1008
1030
|
console.print("🔐 Setting up VS Code tunnel authentication...")
|
|
1009
|
-
console.print(f" Container: {container_name}")
|
|
1031
|
+
console.print(f" Container: {info['container_name']}")
|
|
1010
1032
|
console.print("")
|
|
1011
1033
|
|
|
1012
|
-
if self._run_tunnel_auth(container_name, debug=debug):
|
|
1034
|
+
if self._run_tunnel_auth(info['container_name'], debug=debug):
|
|
1013
1035
|
console.print("")
|
|
1014
1036
|
console.print(f" Start a tunnel: devs tunnel {dev_name}")
|
|
1015
|
-
console.print(f" Then connect:
|
|
1037
|
+
console.print(f" Then connect: {info['vscode_cmd']}")
|
|
1016
1038
|
|
|
1017
1039
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1018
1040
|
raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
|
|
1019
1041
|
|
|
1020
1042
|
def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
|
|
1021
|
-
"""Start a VS Code tunnel in the background.
|
|
1043
|
+
"""Start a VS Code tunnel in the background (CLI version with interactive auth).
|
|
1022
1044
|
|
|
1023
1045
|
If authentication is needed, prompts interactively then retries.
|
|
1024
1046
|
|
|
@@ -1033,73 +1055,95 @@ class ContainerManager:
|
|
|
1033
1055
|
ContainerError: If tunnel startup fails
|
|
1034
1056
|
"""
|
|
1035
1057
|
try:
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
tunnel_name = self._get_tunnel_name(dev_name)
|
|
1058
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1059
|
+
container_name = info['container_name']
|
|
1060
|
+
tunnel_name = info['tunnel_name']
|
|
1041
1061
|
log_file = '/tmp/vscode-tunnel.log'
|
|
1042
1062
|
tunnel_cmd = f'/usr/local/bin/code tunnel --accept-server-license-terms --name {tunnel_name}'
|
|
1043
|
-
vscode_cmd = f"code --remote tunnel+{tunnel_name} {container_workspace_dir}"
|
|
1044
|
-
web_url = f"https://vscode.dev/tunnel/{tunnel_name}{container_workspace_dir}"
|
|
1045
1063
|
|
|
1046
1064
|
console.print(f"[bold cyan]Starting VS Code tunnel for: {dev_name}[/bold cyan]")
|
|
1047
1065
|
console.print(f" Container: {container_name}")
|
|
1048
1066
|
console.print(f" Tunnel name: {tunnel_name}")
|
|
1049
1067
|
console.print("")
|
|
1050
1068
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1069
|
+
result = self._start_and_poll_tunnel(container_name, tunnel_cmd, log_file, debug=debug)
|
|
1070
|
+
|
|
1071
|
+
if result['status'] == 'running':
|
|
1072
|
+
console.print(f"[bold green]Tunnel running![/bold green]")
|
|
1073
|
+
console.print("")
|
|
1074
|
+
console.print("[bold]Connect:[/bold]")
|
|
1075
|
+
console.print(f" Desktop: {info['vscode_cmd']}")
|
|
1076
|
+
console.print(f" Browser: {info['web_url']}")
|
|
1077
|
+
console.print("")
|
|
1078
|
+
console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
|
|
1079
|
+
console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
|
|
1080
|
+
elif result['status'] == 'auth_required':
|
|
1054
1081
|
console.print("[yellow]Authentication required. Starting login...[/yellow]")
|
|
1055
1082
|
console.print("")
|
|
1056
1083
|
if not self._run_tunnel_auth(container_name, debug=debug):
|
|
1057
1084
|
return
|
|
1058
|
-
|
|
1059
1085
|
console.print("")
|
|
1060
1086
|
console.print("[dim]Restarting tunnel...[/dim]")
|
|
1061
1087
|
console.print("")
|
|
1062
|
-
self._start_and_poll_tunnel(container_name,
|
|
1063
|
-
|
|
1088
|
+
retry = self._start_and_poll_tunnel(container_name, tunnel_cmd, log_file, debug=debug)
|
|
1089
|
+
if retry['status'] == 'running':
|
|
1090
|
+
console.print(f"[bold green]Tunnel running![/bold green]")
|
|
1091
|
+
console.print("")
|
|
1092
|
+
console.print("[bold]Connect:[/bold]")
|
|
1093
|
+
console.print(f" Desktop: {info['vscode_cmd']}")
|
|
1094
|
+
console.print(f" Browser: {info['web_url']}")
|
|
1095
|
+
console.print("")
|
|
1096
|
+
console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
|
|
1097
|
+
console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
|
|
1098
|
+
else:
|
|
1099
|
+
log = retry.get('log', '')
|
|
1100
|
+
if log:
|
|
1101
|
+
console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
|
|
1102
|
+
console.print(log)
|
|
1103
|
+
console.print(f" devs tunnel {dev_name} --status")
|
|
1104
|
+
else:
|
|
1105
|
+
log = result.get('log', '')
|
|
1106
|
+
if log:
|
|
1107
|
+
console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
|
|
1108
|
+
console.print(log)
|
|
1109
|
+
else:
|
|
1110
|
+
console.print("[yellow]Tunnel started but no output yet. Check status with:[/yellow]")
|
|
1111
|
+
console.print(f" devs tunnel {dev_name} --status")
|
|
1064
1112
|
|
|
1065
1113
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1066
1114
|
raise ContainerError(f"Failed to start tunnel in {dev_name}: {e}")
|
|
1067
1115
|
|
|
1068
1116
|
def get_tunnel_status(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> tuple[bool, str]:
|
|
1069
|
-
"""Get VS Code tunnel status
|
|
1117
|
+
"""Get VS Code tunnel status, formatted for CLI display.
|
|
1070
1118
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1119
|
+
Returns:
|
|
1120
|
+
Tuple of (is_running, formatted_status_message)
|
|
1121
|
+
"""
|
|
1122
|
+
data = self.get_tunnel_status_data(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1123
|
+
return data.get('running', False), self._format_tunnel_status(data)
|
|
1124
|
+
|
|
1125
|
+
def get_tunnel_status_data(self, dev_name: str, workspace_dir: Path, debug: bool = False,
|
|
1126
|
+
live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> dict:
|
|
1127
|
+
"""Get VS Code tunnel status as structured data.
|
|
1077
1128
|
|
|
1078
1129
|
Returns:
|
|
1079
|
-
|
|
1130
|
+
Dict with running, tunnel_name, web_url, vscode_cmd, raw_status, auth_info.
|
|
1080
1131
|
|
|
1081
1132
|
Raises:
|
|
1082
1133
|
ContainerError: If status check fails
|
|
1083
1134
|
"""
|
|
1084
1135
|
try:
|
|
1085
|
-
|
|
1086
|
-
container_name
|
|
1087
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1088
|
-
)
|
|
1089
|
-
|
|
1090
|
-
# Check tunnel status
|
|
1091
|
-
cmd = [
|
|
1092
|
-
'docker', 'exec',
|
|
1093
|
-
container_name,
|
|
1094
|
-
'/usr/local/bin/code', 'tunnel', 'status'
|
|
1095
|
-
]
|
|
1136
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1137
|
+
container_name = info['container_name']
|
|
1096
1138
|
|
|
1139
|
+
cmd = ['docker', 'exec', container_name,
|
|
1140
|
+
'/usr/local/bin/code', 'tunnel', 'status']
|
|
1097
1141
|
if debug:
|
|
1098
1142
|
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
1099
1143
|
|
|
1100
1144
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1101
1145
|
|
|
1102
|
-
# Get auth info
|
|
1146
|
+
# Get auth info
|
|
1103
1147
|
user_result = subprocess.run(
|
|
1104
1148
|
['docker', 'exec', container_name,
|
|
1105
1149
|
'/usr/local/bin/code', 'tunnel', 'user', 'show'],
|
|
@@ -1107,29 +1151,42 @@ class ContainerManager:
|
|
|
1107
1151
|
)
|
|
1108
1152
|
auth_info = user_result.stdout.strip() if user_result.returncode == 0 else None
|
|
1109
1153
|
|
|
1154
|
+
import json
|
|
1155
|
+
raw_status = {}
|
|
1110
1156
|
if result.returncode == 0:
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1157
|
+
try:
|
|
1158
|
+
raw_status = json.loads(result.stdout.strip())
|
|
1159
|
+
except json.JSONDecodeError:
|
|
1160
|
+
pass
|
|
1161
|
+
|
|
1162
|
+
tunnel_data = raw_status.get('tunnel') or {}
|
|
1163
|
+
is_running = result.returncode == 0 and tunnel_data.get('tunnel') == 'Connected'
|
|
1164
|
+
|
|
1165
|
+
return {
|
|
1166
|
+
'running': is_running,
|
|
1167
|
+
'tunnel_name': info['tunnel_name'],
|
|
1168
|
+
'web_url': info['web_url'] if is_running else None,
|
|
1169
|
+
'vscode_cmd': info['vscode_cmd'] if is_running else None,
|
|
1170
|
+
'auth_info': auth_info,
|
|
1171
|
+
'raw_status': raw_status,
|
|
1172
|
+
'container_name': container_name,
|
|
1173
|
+
}
|
|
1115
1174
|
|
|
1116
1175
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1117
1176
|
raise ContainerError(f"Failed to get tunnel status in {dev_name}: {e}")
|
|
1118
1177
|
|
|
1119
|
-
def _format_tunnel_status(self,
|
|
1120
|
-
"""Format tunnel status
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
except json.JSONDecodeError:
|
|
1125
|
-
return raw_json
|
|
1178
|
+
def _format_tunnel_status(self, data: dict) -> str:
|
|
1179
|
+
"""Format tunnel status data into human-readable CLI output."""
|
|
1180
|
+
raw_status = data.get('raw_status', {})
|
|
1181
|
+
tunnel = raw_status.get('tunnel')
|
|
1182
|
+
auth_info = data.get('auth_info')
|
|
1126
1183
|
|
|
1127
|
-
tunnel = data.get("tunnel")
|
|
1128
1184
|
if not tunnel:
|
|
1129
1185
|
lines = ["[dim]● No tunnel running[/dim]"]
|
|
1130
1186
|
if auth_info:
|
|
1131
1187
|
lines.append(f" Auth: {auth_info}")
|
|
1132
1188
|
return "\n".join(lines)
|
|
1189
|
+
|
|
1133
1190
|
name = tunnel.get("name") or "unknown"
|
|
1134
1191
|
state = tunnel.get("tunnel", "Unknown")
|
|
1135
1192
|
started_at = tunnel.get("started_at")
|
|
@@ -1139,7 +1196,6 @@ class ContainerManager:
|
|
|
1139
1196
|
|
|
1140
1197
|
lines = []
|
|
1141
1198
|
|
|
1142
|
-
# Status line with color
|
|
1143
1199
|
if state == "Connected":
|
|
1144
1200
|
lines.append(f"[bold green]● Connected[/bold green] ({name})")
|
|
1145
1201
|
elif state == "Disconnected":
|
|
@@ -1147,7 +1203,6 @@ class ContainerManager:
|
|
|
1147
1203
|
else:
|
|
1148
1204
|
lines.append(f"[bold yellow]● {state}[/bold yellow] ({name})")
|
|
1149
1205
|
|
|
1150
|
-
# Timestamps
|
|
1151
1206
|
if started_at:
|
|
1152
1207
|
lines.append(f" Started: {self._format_timestamp(started_at)}")
|
|
1153
1208
|
if connected_at:
|
|
@@ -1159,13 +1214,11 @@ class ContainerManager:
|
|
|
1159
1214
|
if auth_info:
|
|
1160
1215
|
lines.append(f" Auth: {auth_info}")
|
|
1161
1216
|
|
|
1162
|
-
|
|
1163
|
-
if state == "Connected":
|
|
1164
|
-
tunnel_name = self._get_tunnel_name(dev_name)
|
|
1217
|
+
if state == "Connected" and data.get('vscode_cmd'):
|
|
1165
1218
|
lines.append("")
|
|
1166
1219
|
lines.append("[bold]Connect:[/bold]")
|
|
1167
|
-
lines.append(f" Desktop:
|
|
1168
|
-
lines.append(f" Browser:
|
|
1220
|
+
lines.append(f" Desktop: {data['vscode_cmd']}")
|
|
1221
|
+
lines.append(f" Browser: {data['web_url']}")
|
|
1169
1222
|
|
|
1170
1223
|
return "\n".join(lines)
|
|
1171
1224
|
|
|
@@ -1174,7 +1227,6 @@ class ContainerManager:
|
|
|
1174
1227
|
"""Format an ISO timestamp into a friendly local time string."""
|
|
1175
1228
|
from datetime import datetime
|
|
1176
1229
|
try:
|
|
1177
|
-
# Handle nanosecond precision by truncating to microseconds
|
|
1178
1230
|
if "." in ts:
|
|
1179
1231
|
base, frac = ts.split(".")
|
|
1180
1232
|
frac = frac.rstrip("Z")[:6]
|
|
@@ -1188,13 +1240,6 @@ class ContainerManager:
|
|
|
1188
1240
|
def kill_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> bool:
|
|
1189
1241
|
"""Kill a running VS Code tunnel in the container.
|
|
1190
1242
|
|
|
1191
|
-
Args:
|
|
1192
|
-
dev_name: Development environment name
|
|
1193
|
-
workspace_dir: Workspace directory path
|
|
1194
|
-
debug: Show debug output for devcontainer operations
|
|
1195
|
-
live: Whether the container is in live mode
|
|
1196
|
-
extra_env: Additional environment variables to pass to container
|
|
1197
|
-
|
|
1198
1243
|
Returns:
|
|
1199
1244
|
True if tunnel was killed successfully
|
|
1200
1245
|
|
|
@@ -1202,12 +1247,9 @@ class ContainerManager:
|
|
|
1202
1247
|
ContainerError: If tunnel kill fails
|
|
1203
1248
|
"""
|
|
1204
1249
|
try:
|
|
1205
|
-
|
|
1206
|
-
container_name
|
|
1207
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1208
|
-
)
|
|
1250
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1251
|
+
container_name = info['container_name']
|
|
1209
1252
|
|
|
1210
|
-
# Kill the tunnel (both service-level and process-level)
|
|
1211
1253
|
if debug:
|
|
1212
1254
|
console.print(f"[dim]Killing tunnel processes in {container_name}[/dim]")
|
|
1213
1255
|
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""Core classes for devs ecosystem."""
|
|
2
|
-
|
|
3
|
-
from .project import Project, ProjectInfo
|
|
4
|
-
from .workspace import WorkspaceManager
|
|
5
|
-
from .container import ContainerManager, ContainerInfo
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"Project",
|
|
9
|
-
"ProjectInfo",
|
|
10
|
-
"WorkspaceManager",
|
|
11
|
-
"ContainerManager",
|
|
12
|
-
"ContainerInfo",
|
|
13
|
-
]
|
|
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
|
|
File without changes
|
{devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/post-create-wrapper.sh
RENAMED
|
File without changes
|
{devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/scripts/python-venv-integration.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/setup-devs-env.sh
RENAMED
|
File without changes
|
{devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh
RENAMED
|
File without changes
|
{devs_common-3.3.1 → devs_common-3.3.3}/devs_common/templates/sudo-scripts/start-services.sh
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|