devs-common 3.3.1__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.
Files changed (43) hide show
  1. {devs_common-3.3.1 → devs_common-3.3.2}/PKG-INFO +1 -1
  2. devs_common-3.3.2/devs_common/core/__init__.py +22 -0
  3. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/core/container.py +173 -131
  4. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common.egg-info/PKG-INFO +1 -1
  5. {devs_common-3.3.1 → devs_common-3.3.2}/pyproject.toml +1 -1
  6. devs_common-3.3.1/devs_common/core/__init__.py +0 -13
  7. {devs_common-3.3.1 → devs_common-3.3.2}/LICENSE +0 -0
  8. {devs_common-3.3.1 → devs_common-3.3.2}/README.md +0 -0
  9. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/__init__.py +0 -0
  10. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/config.py +0 -0
  11. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/core/project.py +0 -0
  12. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/core/workspace.py +0 -0
  13. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/devs_config.py +0 -0
  14. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/exceptions.py +0 -0
  15. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/Dockerfile +0 -0
  16. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/devcontainer.json +0 -0
  17. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  18. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  19. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  20. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  21. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  22. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  23. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  24. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  25. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  26. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  27. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  28. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  29. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/templates/vscode-settings-reference.md +0 -0
  30. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/__init__.py +0 -0
  31. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/config_hash.py +0 -0
  32. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/console.py +0 -0
  33. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/devcontainer.py +0 -0
  34. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/devcontainer_template.py +0 -0
  35. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/docker_client.py +0 -0
  36. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/file_utils.py +0 -0
  37. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/git_utils.py +0 -0
  38. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common/utils/repo_cache.py +0 -0
  39. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common.egg-info/SOURCES.txt +0 -0
  40. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common.egg-info/dependency_links.txt +0 -0
  41. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common.egg-info/requires.txt +0 -0
  42. {devs_common-3.3.1 → devs_common-3.3.2}/devs_common.egg-info/top_level.txt +0 -0
  43. {devs_common-3.3.1 → devs_common-3.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -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, truncated to 20 chars.
856
-
857
- VS Code tunnel names have a 20 character limit. We keep the dev name
858
- suffix and shorten the project portion if needed.
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
- Uses pkill to ensure detached shell wrappers and tunnel processes
915
- are fully terminated, not just the service-level shutdown that
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
- True if tunnel started (or timed out), False if auth is needed.
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
- console.print(f"[bold green]Tunnel running![/bold green]")
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 False
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
- if result.stdout.strip():
981
- console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
982
- console.print(result.stdout.strip())
983
- else:
984
- console.print("[yellow]Tunnel started but no output yet. Check status with:[/yellow]")
985
- console.print(f" devs tunnel {dev_name} --status")
986
- return True
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
- 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
-
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: code --remote tunnel+{tunnel_name} {container_workspace_dir}")
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
- container_name, container_workspace_dir = self._prepare_container_exec(
1037
- dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
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
- if not self._start_and_poll_tunnel(container_name, dev_name, tunnel_cmd,
1052
- log_file, vscode_cmd, web_url, debug=debug):
1053
- # Auth needed
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, dev_name, tunnel_cmd,
1063
- log_file, vscode_cmd, web_url, debug=debug)
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 in the container.
1117
+ """Get VS Code tunnel status, formatted for CLI display.
1070
1118
 
1071
- Args:
1072
- dev_name: Development environment name
1073
- workspace_dir: Workspace directory path
1074
- debug: Show debug output for devcontainer operations
1075
- live: Whether the container is in live mode
1076
- extra_env: Additional environment variables to pass to container
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
- Tuple of (is_running, status_message)
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
- # Prepare container for execution
1086
- container_name, container_workspace_dir = self._prepare_container_exec(
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 (logged in user/provider)
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
- raw = result.stdout.strip()
1112
- return True, self._format_tunnel_status(raw, dev_name, auth_info)
1113
- else:
1114
- return False, result.stderr.strip() if result.stderr else "No tunnel running"
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, raw_json: str, dev_name: str, auth_info: Optional[str] = None) -> str:
1120
- """Format tunnel status JSON into human-readable output."""
1121
- import json
1122
- try:
1123
- data = json.loads(raw_json)
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
- # Connection info when connected
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: code --remote tunnel+{tunnel_name}")
1168
- lines.append(f" Browser: https://vscode.dev/tunnel/{tunnel_name}")
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
- # Prepare container for execution
1206
- container_name, container_workspace_dir = self._prepare_container_exec(
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-common"
7
- version = "3.3.1"
7
+ version = "3.3.2"
8
8
  description = "Shared utilities for devs package ecosystem"
9
9
  authors = [
10
10
  {name = "Dan Lester"},
@@ -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