devs-common 3.3.0__tar.gz → 3.3.2__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.0 → devs_common-3.3.2}/PKG-INFO +1 -1
- devs_common-3.3.2/devs_common/core/__init__.py +22 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/core/container.py +248 -118
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common.egg-info/PKG-INFO +1 -1
- {devs_common-3.3.0 → devs_common-3.3.2}/pyproject.toml +1 -1
- devs_common-3.3.0/devs_common/core/__init__.py +0 -13
- {devs_common-3.3.0 → devs_common-3.3.2}/LICENSE +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/README.md +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/__init__.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/config.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/core/project.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/core/workspace.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/devs_config.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/exceptions.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/Dockerfile +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/devcontainer.json +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/copy-ssh.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/setup-workspace.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/start-tunnel.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/tunnel-status.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/vscode-settings-reference.md +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/__init__.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/config_hash.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/console.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/devcontainer.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/devcontainer_template.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/docker_client.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/file_utils.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/git_utils.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common/utils/repo_cache.py +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common.egg-info/SOURCES.txt +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common.egg-info/dependency_links.txt +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common.egg-info/requires.txt +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/devs_common.egg-info/top_level.txt +0 -0
- {devs_common-3.3.0 → devs_common-3.3.2}/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.
|
|
@@ -908,20 +941,31 @@ class ContainerManager:
|
|
|
908
941
|
console.print("[yellow]Authentication was cancelled or failed.[/yellow]")
|
|
909
942
|
return False
|
|
910
943
|
|
|
911
|
-
def
|
|
912
|
-
|
|
944
|
+
def _kill_tunnel_processes(self, container_name: str) -> None:
|
|
945
|
+
"""Kill all tunnel processes in the container."""
|
|
946
|
+
kill_tunnel_processes(container_name)
|
|
947
|
+
|
|
948
|
+
def _start_and_poll_tunnel(self, container_name: str, tunnel_cmd: str,
|
|
949
|
+
log_file: str, debug: bool = False) -> dict:
|
|
913
950
|
"""Start a detached tunnel and poll the log for startup confirmation.
|
|
914
951
|
|
|
915
952
|
Returns:
|
|
916
|
-
|
|
953
|
+
Dict with 'status' key: 'running', 'auth_required', or 'starting'.
|
|
917
954
|
"""
|
|
955
|
+
# Kill any stale tunnel processes and clear the log before starting
|
|
956
|
+
self._kill_tunnel_processes(container_name)
|
|
957
|
+
subprocess.run(
|
|
958
|
+
['docker', 'exec', container_name, '/bin/sh', '-c', f'> {log_file}'],
|
|
959
|
+
capture_output=True
|
|
960
|
+
)
|
|
961
|
+
time.sleep(1)
|
|
962
|
+
|
|
918
963
|
subprocess.run(
|
|
919
964
|
['docker', 'exec', '-d', container_name,
|
|
920
965
|
'/bin/sh', '-c', f'{tunnel_cmd} > {log_file} 2>&1'],
|
|
921
966
|
check=True
|
|
922
967
|
)
|
|
923
968
|
|
|
924
|
-
console.print("[dim]Waiting for tunnel to start...[/dim]")
|
|
925
969
|
for i in range(15):
|
|
926
970
|
time.sleep(1)
|
|
927
971
|
result = subprocess.run(
|
|
@@ -931,22 +975,11 @@ class ContainerManager:
|
|
|
931
975
|
output = result.stdout
|
|
932
976
|
|
|
933
977
|
if 'Open this link' in output or 'vscode.dev/tunnel' in output:
|
|
934
|
-
|
|
935
|
-
console.print("")
|
|
936
|
-
console.print("[bold]Connect:[/bold]")
|
|
937
|
-
console.print(f" Desktop: {vscode_cmd}")
|
|
938
|
-
console.print(f" Browser: {web_url}")
|
|
939
|
-
console.print("")
|
|
940
|
-
console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
|
|
941
|
-
console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
|
|
942
|
-
return True
|
|
978
|
+
return {'status': 'running'}
|
|
943
979
|
|
|
944
980
|
if 'log in' in output.lower() or 'device' in output.lower() or 'invalid' in output.lower():
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
capture_output=True
|
|
948
|
-
)
|
|
949
|
-
return False
|
|
981
|
+
self._kill_tunnel_processes(container_name)
|
|
982
|
+
return {'status': 'auth_required'}
|
|
950
983
|
|
|
951
984
|
if debug and output.strip():
|
|
952
985
|
console.print(f"[dim]Log ({i+1}s): {output.strip()[-100:]}[/dim]")
|
|
@@ -956,16 +989,30 @@ class ContainerManager:
|
|
|
956
989
|
['docker', 'exec', container_name, 'cat', log_file],
|
|
957
990
|
capture_output=True, text=True
|
|
958
991
|
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
+
}
|
|
966
1013
|
|
|
967
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:
|
|
968
|
-
"""Authenticate VS Code tunnel (interactive, per-container setup).
|
|
1015
|
+
"""Authenticate VS Code tunnel (interactive CLI, per-container setup).
|
|
969
1016
|
|
|
970
1017
|
Args:
|
|
971
1018
|
dev_name: Development environment name
|
|
@@ -978,26 +1025,22 @@ class ContainerManager:
|
|
|
978
1025
|
ContainerError: If authentication fails
|
|
979
1026
|
"""
|
|
980
1027
|
try:
|
|
981
|
-
|
|
982
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
983
|
-
)
|
|
984
|
-
|
|
985
|
-
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)
|
|
986
1029
|
|
|
987
1030
|
console.print("🔐 Setting up VS Code tunnel authentication...")
|
|
988
|
-
console.print(f" Container: {container_name}")
|
|
1031
|
+
console.print(f" Container: {info['container_name']}")
|
|
989
1032
|
console.print("")
|
|
990
1033
|
|
|
991
|
-
if self._run_tunnel_auth(container_name, debug=debug):
|
|
1034
|
+
if self._run_tunnel_auth(info['container_name'], debug=debug):
|
|
992
1035
|
console.print("")
|
|
993
1036
|
console.print(f" Start a tunnel: devs tunnel {dev_name}")
|
|
994
|
-
console.print(f" Then connect:
|
|
1037
|
+
console.print(f" Then connect: {info['vscode_cmd']}")
|
|
995
1038
|
|
|
996
1039
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
997
1040
|
raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
|
|
998
1041
|
|
|
999
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:
|
|
1000
|
-
"""Start a VS Code tunnel in the background.
|
|
1043
|
+
"""Start a VS Code tunnel in the background (CLI version with interactive auth).
|
|
1001
1044
|
|
|
1002
1045
|
If authentication is needed, prompts interactively then retries.
|
|
1003
1046
|
|
|
@@ -1012,90 +1055,191 @@ class ContainerManager:
|
|
|
1012
1055
|
ContainerError: If tunnel startup fails
|
|
1013
1056
|
"""
|
|
1014
1057
|
try:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
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']
|
|
1020
1061
|
log_file = '/tmp/vscode-tunnel.log'
|
|
1021
1062
|
tunnel_cmd = f'/usr/local/bin/code tunnel --accept-server-license-terms --name {tunnel_name}'
|
|
1022
|
-
vscode_cmd = f"code --remote tunnel+{tunnel_name} {container_workspace_dir}"
|
|
1023
|
-
web_url = f"https://vscode.dev/tunnel/{tunnel_name}{container_workspace_dir}"
|
|
1024
1063
|
|
|
1025
1064
|
console.print(f"[bold cyan]Starting VS Code tunnel for: {dev_name}[/bold cyan]")
|
|
1026
1065
|
console.print(f" Container: {container_name}")
|
|
1027
1066
|
console.print(f" Tunnel name: {tunnel_name}")
|
|
1028
1067
|
console.print("")
|
|
1029
1068
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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':
|
|
1033
1081
|
console.print("[yellow]Authentication required. Starting login...[/yellow]")
|
|
1034
1082
|
console.print("")
|
|
1035
1083
|
if not self._run_tunnel_auth(container_name, debug=debug):
|
|
1036
1084
|
return
|
|
1037
|
-
|
|
1038
1085
|
console.print("")
|
|
1039
1086
|
console.print("[dim]Restarting tunnel...[/dim]")
|
|
1040
1087
|
console.print("")
|
|
1041
|
-
self._start_and_poll_tunnel(container_name,
|
|
1042
|
-
|
|
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")
|
|
1043
1112
|
|
|
1044
1113
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1045
1114
|
raise ContainerError(f"Failed to start tunnel in {dev_name}: {e}")
|
|
1046
1115
|
|
|
1047
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]:
|
|
1048
|
-
"""Get VS Code tunnel status
|
|
1117
|
+
"""Get VS Code tunnel status, formatted for CLI display.
|
|
1049
1118
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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.
|
|
1056
1128
|
|
|
1057
1129
|
Returns:
|
|
1058
|
-
|
|
1130
|
+
Dict with running, tunnel_name, web_url, vscode_cmd, raw_status, auth_info.
|
|
1059
1131
|
|
|
1060
1132
|
Raises:
|
|
1061
1133
|
ContainerError: If status check fails
|
|
1062
1134
|
"""
|
|
1063
1135
|
try:
|
|
1064
|
-
|
|
1065
|
-
container_name
|
|
1066
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1067
|
-
)
|
|
1068
|
-
|
|
1069
|
-
# Check tunnel status
|
|
1070
|
-
cmd = [
|
|
1071
|
-
'docker', 'exec',
|
|
1072
|
-
container_name,
|
|
1073
|
-
'/usr/local/bin/code', 'tunnel', 'status'
|
|
1074
|
-
]
|
|
1136
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1137
|
+
container_name = info['container_name']
|
|
1075
1138
|
|
|
1139
|
+
cmd = ['docker', 'exec', container_name,
|
|
1140
|
+
'/usr/local/bin/code', 'tunnel', 'status']
|
|
1076
1141
|
if debug:
|
|
1077
1142
|
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
1078
1143
|
|
|
1079
1144
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1080
1145
|
|
|
1146
|
+
# Get auth info
|
|
1147
|
+
user_result = subprocess.run(
|
|
1148
|
+
['docker', 'exec', container_name,
|
|
1149
|
+
'/usr/local/bin/code', 'tunnel', 'user', 'show'],
|
|
1150
|
+
capture_output=True, text=True
|
|
1151
|
+
)
|
|
1152
|
+
auth_info = user_result.stdout.strip() if user_result.returncode == 0 else None
|
|
1153
|
+
|
|
1154
|
+
import json
|
|
1155
|
+
raw_status = {}
|
|
1081
1156
|
if result.returncode == 0:
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
+
}
|
|
1085
1174
|
|
|
1086
1175
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1087
1176
|
raise ContainerError(f"Failed to get tunnel status in {dev_name}: {e}")
|
|
1088
1177
|
|
|
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')
|
|
1183
|
+
|
|
1184
|
+
if not tunnel:
|
|
1185
|
+
lines = ["[dim]● No tunnel running[/dim]"]
|
|
1186
|
+
if auth_info:
|
|
1187
|
+
lines.append(f" Auth: {auth_info}")
|
|
1188
|
+
return "\n".join(lines)
|
|
1189
|
+
|
|
1190
|
+
name = tunnel.get("name") or "unknown"
|
|
1191
|
+
state = tunnel.get("tunnel", "Unknown")
|
|
1192
|
+
started_at = tunnel.get("started_at")
|
|
1193
|
+
connected_at = tunnel.get("last_connected_at")
|
|
1194
|
+
disconnected_at = tunnel.get("last_disconnected_at")
|
|
1195
|
+
fail_reason = tunnel.get("last_fail_reason")
|
|
1196
|
+
|
|
1197
|
+
lines = []
|
|
1198
|
+
|
|
1199
|
+
if state == "Connected":
|
|
1200
|
+
lines.append(f"[bold green]● Connected[/bold green] ({name})")
|
|
1201
|
+
elif state == "Disconnected":
|
|
1202
|
+
lines.append(f"[bold red]● Disconnected[/bold red] ({name})")
|
|
1203
|
+
else:
|
|
1204
|
+
lines.append(f"[bold yellow]● {state}[/bold yellow] ({name})")
|
|
1205
|
+
|
|
1206
|
+
if started_at:
|
|
1207
|
+
lines.append(f" Started: {self._format_timestamp(started_at)}")
|
|
1208
|
+
if connected_at:
|
|
1209
|
+
lines.append(f" Connected: {self._format_timestamp(connected_at)}")
|
|
1210
|
+
if disconnected_at:
|
|
1211
|
+
lines.append(f" Disconnected: {self._format_timestamp(disconnected_at)}")
|
|
1212
|
+
if fail_reason:
|
|
1213
|
+
lines.append(f" Fail reason: {fail_reason}")
|
|
1214
|
+
if auth_info:
|
|
1215
|
+
lines.append(f" Auth: {auth_info}")
|
|
1216
|
+
|
|
1217
|
+
if state == "Connected" and data.get('vscode_cmd'):
|
|
1218
|
+
lines.append("")
|
|
1219
|
+
lines.append("[bold]Connect:[/bold]")
|
|
1220
|
+
lines.append(f" Desktop: {data['vscode_cmd']}")
|
|
1221
|
+
lines.append(f" Browser: {data['web_url']}")
|
|
1222
|
+
|
|
1223
|
+
return "\n".join(lines)
|
|
1224
|
+
|
|
1225
|
+
@staticmethod
|
|
1226
|
+
def _format_timestamp(ts: str) -> str:
|
|
1227
|
+
"""Format an ISO timestamp into a friendly local time string."""
|
|
1228
|
+
from datetime import datetime
|
|
1229
|
+
try:
|
|
1230
|
+
if "." in ts:
|
|
1231
|
+
base, frac = ts.split(".")
|
|
1232
|
+
frac = frac.rstrip("Z")[:6]
|
|
1233
|
+
ts = f"{base}.{frac}Z"
|
|
1234
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
1235
|
+
local_dt = dt.astimezone()
|
|
1236
|
+
return local_dt.strftime("%H:%M:%S %Z")
|
|
1237
|
+
except (ValueError, TypeError):
|
|
1238
|
+
return ts
|
|
1239
|
+
|
|
1089
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:
|
|
1090
1241
|
"""Kill a running VS Code tunnel in the container.
|
|
1091
1242
|
|
|
1092
|
-
Args:
|
|
1093
|
-
dev_name: Development environment name
|
|
1094
|
-
workspace_dir: Workspace directory path
|
|
1095
|
-
debug: Show debug output for devcontainer operations
|
|
1096
|
-
live: Whether the container is in live mode
|
|
1097
|
-
extra_env: Additional environment variables to pass to container
|
|
1098
|
-
|
|
1099
1243
|
Returns:
|
|
1100
1244
|
True if tunnel was killed successfully
|
|
1101
1245
|
|
|
@@ -1103,29 +1247,15 @@ class ContainerManager:
|
|
|
1103
1247
|
ContainerError: If tunnel kill fails
|
|
1104
1248
|
"""
|
|
1105
1249
|
try:
|
|
1106
|
-
|
|
1107
|
-
container_name
|
|
1108
|
-
dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
# Kill the tunnel
|
|
1112
|
-
cmd = [
|
|
1113
|
-
'docker', 'exec',
|
|
1114
|
-
container_name,
|
|
1115
|
-
'/usr/local/bin/code', 'tunnel', 'kill'
|
|
1116
|
-
]
|
|
1250
|
+
info = self.get_tunnel_info(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env)
|
|
1251
|
+
container_name = info['container_name']
|
|
1117
1252
|
|
|
1118
1253
|
if debug:
|
|
1119
|
-
console.print(f"[dim]
|
|
1254
|
+
console.print(f"[dim]Killing tunnel processes in {container_name}[/dim]")
|
|
1120
1255
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
console.print(f" Tunnel killed in: {dev_name}")
|
|
1125
|
-
return True
|
|
1126
|
-
else:
|
|
1127
|
-
console.print(f" No tunnel to kill in: {dev_name}")
|
|
1128
|
-
return False
|
|
1256
|
+
self._kill_tunnel_processes(container_name)
|
|
1257
|
+
console.print(f" Tunnel killed in: {dev_name}")
|
|
1258
|
+
return True
|
|
1129
1259
|
|
|
1130
1260
|
except (DockerError, subprocess.SubprocessError) as e:
|
|
1131
1261
|
raise ContainerError(f"Failed to kill tunnel in {dev_name}: {e}")
|
|
@@ -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.0 → devs_common-3.3.2}/devs_common/templates/scripts/post-create-wrapper.sh
RENAMED
|
File without changes
|
{devs_common-3.3.0 → devs_common-3.3.2}/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.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-devs-env.sh
RENAMED
|
File without changes
|
{devs_common-3.3.0 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh
RENAMED
|
File without changes
|
{devs_common-3.3.0 → devs_common-3.3.2}/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
|