devs-common 2.0.10__tar.gz → 2.0.12__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 (42) hide show
  1. {devs_common-2.0.10 → devs_common-2.0.12}/PKG-INFO +1 -1
  2. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/core/container.py +126 -104
  3. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/Dockerfile +5 -0
  4. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common.egg-info/PKG-INFO +1 -1
  5. {devs_common-2.0.10 → devs_common-2.0.12}/pyproject.toml +1 -1
  6. {devs_common-2.0.10 → devs_common-2.0.12}/LICENSE +0 -0
  7. {devs_common-2.0.10 → devs_common-2.0.12}/README.md +0 -0
  8. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/__init__.py +0 -0
  9. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/config.py +0 -0
  10. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/core/__init__.py +0 -0
  11. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/core/project.py +0 -0
  12. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/core/workspace.py +0 -0
  13. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/devs_config.py +0 -0
  14. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/exceptions.py +0 -0
  15. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/devcontainer.json +0 -0
  16. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  17. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  18. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  19. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  20. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  21. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  22. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  23. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  24. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  25. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  26. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  27. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  28. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/templates/vscode-settings-reference.md +0 -0
  29. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/__init__.py +0 -0
  30. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/config_hash.py +0 -0
  31. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/console.py +0 -0
  32. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/devcontainer.py +0 -0
  33. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/devcontainer_template.py +0 -0
  34. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/docker_client.py +0 -0
  35. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/file_utils.py +0 -0
  36. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/git_utils.py +0 -0
  37. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common/utils/repo_cache.py +0 -0
  38. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common.egg-info/SOURCES.txt +0 -0
  39. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common.egg-info/dependency_links.txt +0 -0
  40. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common.egg-info/requires.txt +0 -0
  41. {devs_common-2.0.10 → devs_common-2.0.12}/devs_common.egg-info/top_level.txt +0 -0
  42. {devs_common-2.0.10 → devs_common-2.0.12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.10
3
+ Version: 2.0.12
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -399,27 +399,20 @@ class ContainerManager:
399
399
  project_labels = self._get_project_labels(dev_name)
400
400
 
401
401
  try:
402
- console.print(f" 🔍 Looking for containers with labels: {project_labels}")
403
402
  existing_containers = self.docker.find_containers_by_labels(project_labels)
404
- console.print(f" 📋 Found {len(existing_containers)} containers")
405
403
 
406
404
  if existing_containers:
407
405
  for container_info in existing_containers:
408
406
  container_name = container_info['name']
409
- container_status = container_info['status']
410
407
 
411
- console.print(f" 🛑 Stopping container: {container_name} (status: {container_status})")
412
408
  try:
413
- stop_result = self.docker.stop_container(container_name)
414
- console.print(f" 📋 Stop result: {stop_result}")
409
+ self.docker.stop_container(container_name)
415
410
  except DockerError as stop_e:
416
411
  console.print(f" ⚠️ Stop failed for {container_name}: {stop_e}")
417
412
 
418
413
  if remove:
419
- console.print(f" 🗑️ Removing container: {container_name}")
420
414
  try:
421
- remove_result = self.docker.remove_container(container_name)
422
- console.print(f" 📋 Remove result: {remove_result}")
415
+ self.docker.remove_container(container_name)
423
416
  except DockerError as remove_e:
424
417
  console.print(f" ⚠️ Remove failed for {container_name}: {remove_e}")
425
418
 
@@ -586,7 +579,7 @@ class ContainerManager:
586
579
 
587
580
  return removed_count
588
581
 
589
- def _prepare_container_exec(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> Tuple[str, str]:
582
+ def _prepare_container_exec(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None, reuse_existing: bool = False) -> Tuple[str, str]:
590
583
  """Prepare container for exec operations (shared by exec_shell and exec_command).
591
584
 
592
585
  Args:
@@ -595,6 +588,10 @@ class ContainerManager:
595
588
  debug: Show debug output for devcontainer operations
596
589
  live: Whether the container is in live mode
597
590
  extra_env: Additional environment variables to pass to container
591
+ reuse_existing: If True, use existing container as-is (starting
592
+ it if stopped) without rebuilding. Only creates a new
593
+ container if none exists. Used by tunnel commands to
594
+ avoid accidentally destroying work in the container.
598
595
 
599
596
  Returns:
600
597
  Tuple of (container_name, container_workspace_dir)
@@ -623,9 +620,17 @@ class ContainerManager:
623
620
  workspace_name = workspace_info["workspace_name"]
624
621
  container_workspace_dir = f"/workspaces/{workspace_name}"
625
622
 
626
- # Ensure container is running
627
- if not self.ensure_container_running(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env):
628
- raise ContainerError(f"Failed to start container for {dev_name}")
623
+ if reuse_existing and existing_containers:
624
+ # Use existing container as-is (start if stopped, but never rebuild)
625
+ status = existing_containers[0].get('status', '')
626
+ if status != 'running':
627
+ container_id = existing_containers[0].get('name', container_name)
628
+ console.print(f" ▶️ Starting stopped container: {container_id}")
629
+ self.docker.start_container(container_id)
630
+ else:
631
+ # Ensure container is running (may create/restart as needed)
632
+ if not self.ensure_container_running(dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env):
633
+ raise ContainerError(f"Failed to start container for {dev_name}")
629
634
 
630
635
  return container_name, container_workspace_dir
631
636
 
@@ -877,13 +882,99 @@ class ContainerManager:
877
882
 
878
883
  return tunnel_name
879
884
 
885
+ def _run_tunnel_auth(self, container_name: str, debug: bool = False) -> bool:
886
+ """Run interactive VS Code tunnel authentication in a container.
887
+
888
+ Returns:
889
+ True if authentication succeeded
890
+ """
891
+ console.print("[yellow]Follow the prompts to authenticate:[/yellow]")
892
+ console.print(" 1. A URL and code will be displayed")
893
+ console.print(" 2. Open the URL in your browser")
894
+ console.print(" 3. Enter the code to authenticate with GitHub/Microsoft")
895
+ console.print("")
896
+
897
+ cmd = [
898
+ 'docker', 'exec', '-it',
899
+ container_name,
900
+ '/usr/local/bin/code', 'tunnel', 'user', 'login',
901
+ '--provider', 'github'
902
+ ]
903
+
904
+ if debug:
905
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
906
+
907
+ result = subprocess.run(cmd, check=False)
908
+
909
+ if result.returncode == 0:
910
+ console.print("")
911
+ console.print("[bold green]Authentication successful![/bold green]")
912
+ console.print(" Auth persists across container stop/restart cycles.")
913
+ return True
914
+ else:
915
+ console.print("")
916
+ console.print("[yellow]Authentication was cancelled or failed.[/yellow]")
917
+ return False
918
+
919
+ def _start_and_poll_tunnel(self, container_name: str, dev_name: str, tunnel_cmd: str,
920
+ log_file: str, vscode_cmd: str, web_url: str, debug: bool = False) -> bool:
921
+ """Start a detached tunnel and poll the log for startup confirmation.
922
+
923
+ Returns:
924
+ True if tunnel started (or timed out), False if auth is needed.
925
+ """
926
+ subprocess.run(
927
+ ['docker', 'exec', '-d', container_name,
928
+ '/bin/sh', '-c', f'{tunnel_cmd} > {log_file} 2>&1'],
929
+ check=True
930
+ )
931
+
932
+ console.print("[dim]Waiting for tunnel to start...[/dim]")
933
+ for i in range(15):
934
+ time.sleep(1)
935
+ result = subprocess.run(
936
+ ['docker', 'exec', container_name, 'cat', log_file],
937
+ capture_output=True, text=True
938
+ )
939
+ output = result.stdout
940
+
941
+ if 'Open this link' in output or 'vscode.dev/tunnel' in output:
942
+ console.print(f"[bold green]Tunnel running![/bold green]")
943
+ console.print("")
944
+ console.print("[bold]Connect:[/bold]")
945
+ console.print(f" Desktop: {vscode_cmd}")
946
+ console.print(f" Browser: {web_url}")
947
+ console.print("")
948
+ console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
949
+ console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
950
+ return True
951
+
952
+ if 'log in' in output.lower() or 'device' in output.lower() or 'invalid' in output.lower():
953
+ subprocess.run(
954
+ ['docker', 'exec', container_name, '/usr/local/bin/code', 'tunnel', 'kill'],
955
+ capture_output=True
956
+ )
957
+ return False
958
+
959
+ if debug and output.strip():
960
+ console.print(f"[dim]Log ({i+1}s): {output.strip()[-100:]}[/dim]")
961
+
962
+ # Timeout
963
+ result = subprocess.run(
964
+ ['docker', 'exec', container_name, 'cat', log_file],
965
+ capture_output=True, text=True
966
+ )
967
+ if result.stdout.strip():
968
+ console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
969
+ console.print(result.stdout.strip())
970
+ else:
971
+ console.print("[yellow]Tunnel started but no output yet. Check status with:[/yellow]")
972
+ console.print(f" devs tunnel {dev_name} --status")
973
+ return True
974
+
880
975
  def tunnel_auth(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
881
976
  """Authenticate VS Code tunnel (interactive, per-container setup).
882
977
 
883
- Runs the device code flow interactively inside the container. Auth is
884
- stored in the container's filesystem and persists across stop/restart
885
- cycles (but not container removal/recreation).
886
-
887
978
  Args:
888
979
  dev_name: Development environment name
889
980
  workspace_dir: Workspace directory path
@@ -896,7 +987,7 @@ class ContainerManager:
896
987
  """
897
988
  try:
898
989
  container_name, container_workspace_dir = self._prepare_container_exec(
899
- dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
990
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
900
991
  )
901
992
 
902
993
  tunnel_name = self._get_tunnel_name(dev_name)
@@ -904,34 +995,11 @@ class ContainerManager:
904
995
  console.print("🔐 Setting up VS Code tunnel authentication...")
905
996
  console.print(f" Container: {container_name}")
906
997
  console.print("")
907
- console.print("[yellow]Follow the prompts to authenticate:[/yellow]")
908
- console.print(" 1. A URL and code will be displayed")
909
- console.print(" 2. Open the URL in your browser")
910
- console.print(" 3. Enter the code to authenticate with GitHub/Microsoft")
911
- console.print("")
912
998
 
913
- cmd = [
914
- 'docker', 'exec', '-it',
915
- container_name,
916
- '/usr/local/bin/code', 'tunnel', 'user', 'login',
917
- '--provider', 'github'
918
- ]
919
-
920
- if debug:
921
- console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
922
-
923
- result = subprocess.run(cmd, check=False)
924
-
925
- if result.returncode == 0:
926
- console.print("")
927
- console.print("[bold green]Authentication successful![/bold green]")
928
- console.print(" Auth persists across container stop/restart cycles.")
999
+ if self._run_tunnel_auth(container_name, debug=debug):
929
1000
  console.print("")
930
1001
  console.print(f" Start a tunnel: devs tunnel {dev_name}")
931
1002
  console.print(f" Then connect: code --remote tunnel+{tunnel_name} {container_workspace_dir}")
932
- else:
933
- console.print("")
934
- console.print("[yellow]Authentication was cancelled or failed.[/yellow]")
935
1003
 
936
1004
  except (DockerError, subprocess.SubprocessError) as e:
937
1005
  raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
@@ -939,8 +1007,7 @@ class ContainerManager:
939
1007
  def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
940
1008
  """Start a VS Code tunnel in the background.
941
1009
 
942
- Requires prior authentication via tunnel_auth(). The tunnel runs
943
- detached inside the container.
1010
+ If authentication is needed, prompts interactively then retries.
944
1011
 
945
1012
  Args:
946
1013
  dev_name: Development environment name
@@ -954,78 +1021,33 @@ class ContainerManager:
954
1021
  """
955
1022
  try:
956
1023
  container_name, container_workspace_dir = self._prepare_container_exec(
957
- dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
1024
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
958
1025
  )
959
1026
 
960
1027
  tunnel_name = self._get_tunnel_name(dev_name)
961
1028
  log_file = '/tmp/vscode-tunnel.log'
962
1029
  tunnel_cmd = f'/usr/local/bin/code tunnel --accept-server-license-terms --name {tunnel_name}'
963
1030
  vscode_cmd = f"code --remote tunnel+{tunnel_name} {container_workspace_dir}"
1031
+ web_url = f"https://vscode.dev/tunnel/{tunnel_name}{container_workspace_dir}"
964
1032
 
965
1033
  console.print(f"[bold cyan]Starting VS Code tunnel for: {dev_name}[/bold cyan]")
966
1034
  console.print(f" Container: {container_name}")
967
1035
  console.print(f" Tunnel name: {tunnel_name}")
968
1036
  console.print("")
969
1037
 
970
- # Start detached
971
- cmd = [
972
- 'docker', 'exec', '-d',
973
- container_name,
974
- '/bin/sh', '-c',
975
- f'{tunnel_cmd} > {log_file} 2>&1'
976
- ]
977
-
978
- if debug:
979
- console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
980
-
981
- subprocess.run(cmd, check=True)
982
-
983
- # Poll the log to confirm startup and show the URL
984
- console.print("[dim]Waiting for tunnel to start...[/dim]")
985
- for i in range(15):
986
- time.sleep(1)
987
- result = subprocess.run(
988
- ['docker', 'exec', container_name, 'cat', log_file],
989
- capture_output=True, text=True
990
- )
991
- output = result.stdout
992
-
993
- if 'Open this link' in output or 'vscode.dev/tunnel' in output:
994
- console.print(f"[bold green]Tunnel running![/bold green]")
995
- console.print("")
996
- console.print("[bold]Connect from your local machine:[/bold]")
997
- console.print(f" {vscode_cmd}")
998
- console.print("")
999
- for line in output.split('\n'):
1000
- if 'vscode.dev/tunnel' in line:
1001
- console.print(f"[dim]Or open in browser: {line.strip()}[/dim]")
1002
- console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
1003
- console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
1004
- return
1005
-
1006
- if 'log in' in output.lower() or 'device' in output.lower() or 'invalid' in output.lower():
1007
- console.print("[yellow]Authentication required.[/yellow]")
1008
- console.print(f" Run: devs tunnel {dev_name} --auth")
1009
- subprocess.run(
1010
- ['docker', 'exec', container_name, '/usr/local/bin/code', 'tunnel', 'kill'],
1011
- capture_output=True
1012
- )
1038
+ if not self._start_and_poll_tunnel(container_name, dev_name, tunnel_cmd,
1039
+ log_file, vscode_cmd, web_url, debug=debug):
1040
+ # Auth needed
1041
+ console.print("[yellow]Authentication required. Starting login...[/yellow]")
1042
+ console.print("")
1043
+ if not self._run_tunnel_auth(container_name, debug=debug):
1013
1044
  return
1014
1045
 
1015
- if debug and output.strip():
1016
- console.print(f"[dim]Log ({i+1}s): {output.strip()[-100:]}[/dim]")
1017
-
1018
- # Timeout - show what we have
1019
- result = subprocess.run(
1020
- ['docker', 'exec', container_name, 'cat', log_file],
1021
- capture_output=True, text=True
1022
- )
1023
- if result.stdout.strip():
1024
- console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
1025
- console.print(result.stdout.strip())
1026
- else:
1027
- console.print("[yellow]Tunnel started but no output yet. Check status with:[/yellow]")
1028
- console.print(f" devs tunnel {dev_name} --status")
1046
+ console.print("")
1047
+ console.print("[dim]Restarting tunnel...[/dim]")
1048
+ console.print("")
1049
+ self._start_and_poll_tunnel(container_name, dev_name, tunnel_cmd,
1050
+ log_file, vscode_cmd, web_url, debug=debug)
1029
1051
 
1030
1052
  except (DockerError, subprocess.SubprocessError) as e:
1031
1053
  raise ContainerError(f"Failed to start tunnel in {dev_name}: {e}")
@@ -1049,7 +1071,7 @@ class ContainerManager:
1049
1071
  try:
1050
1072
  # Prepare container for execution
1051
1073
  container_name, container_workspace_dir = self._prepare_container_exec(
1052
- dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
1074
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
1053
1075
  )
1054
1076
 
1055
1077
  # Check tunnel status
@@ -1091,7 +1113,7 @@ class ContainerManager:
1091
1113
  try:
1092
1114
  # Prepare container for execution
1093
1115
  container_name, container_workspace_dir = self._prepare_container_exec(
1094
- dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
1116
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env, reuse_existing=True
1095
1117
  )
1096
1118
 
1097
1119
  # Kill the tunnel
@@ -125,9 +125,14 @@ RUN curl -fsSL https://claude.ai/install.sh | bash
125
125
  # Install OpenAI Codex CLI
126
126
  RUN npm install -g @openai/codex
127
127
 
128
+ # Install cloudflared for tunnel support (proxies all HTTP methods correctly)
128
129
  # Install VS Code CLI for tunnel support (needs root for /usr/local/bin)
129
130
  # Using the standalone CLI which supports the 'tunnel' command
130
131
  USER root
132
+ RUN ARCH=$(dpkg --print-architecture) && \
133
+ curl -fSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${ARCH}" -o /usr/local/bin/cloudflared && \
134
+ chmod +x /usr/local/bin/cloudflared
135
+
131
136
  RUN ARCH=$(dpkg --print-architecture) && \
132
137
  if [ "$ARCH" = "amd64" ]; then \
133
138
  curl -fSL 'https://update.code.visualstudio.com/latest/cli-linux-x64/stable' -o /tmp/vscode-cli.tar.gz; \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.10
3
+ Version: 2.0.12
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 = "2.0.10"
7
+ version = "2.0.12"
8
8
  description = "Shared utilities for devs package ecosystem"
9
9
  authors = [
10
10
  {name = "Dan Lester"},
File without changes
File without changes
File without changes