devs-common 2.0.3__tar.gz → 2.0.4__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 (41) hide show
  1. {devs_common-2.0.3 → devs_common-2.0.4}/PKG-INFO +1 -1
  2. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/core/container.py +139 -40
  3. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/Dockerfile +2 -2
  4. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/devcontainer.json +2 -1
  5. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common.egg-info/PKG-INFO +1 -1
  6. {devs_common-2.0.3 → devs_common-2.0.4}/pyproject.toml +1 -1
  7. {devs_common-2.0.3 → devs_common-2.0.4}/LICENSE +0 -0
  8. {devs_common-2.0.3 → devs_common-2.0.4}/README.md +0 -0
  9. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/__init__.py +0 -0
  10. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/config.py +0 -0
  11. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/core/__init__.py +0 -0
  12. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/core/project.py +0 -0
  13. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/core/workspace.py +0 -0
  14. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/devs_config.py +0 -0
  15. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/exceptions.py +0 -0
  16. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  17. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  18. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  19. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  20. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  21. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  22. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  23. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  24. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  25. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  26. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  27. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  28. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/templates/vscode-settings-reference.md +0 -0
  29. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/__init__.py +0 -0
  30. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/config_hash.py +0 -0
  31. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/console.py +0 -0
  32. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/devcontainer.py +0 -0
  33. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/devcontainer_template.py +0 -0
  34. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/docker_client.py +0 -0
  35. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/file_utils.py +0 -0
  36. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common/utils/git_utils.py +0 -0
  37. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common.egg-info/SOURCES.txt +0 -0
  38. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common.egg-info/dependency_links.txt +0 -0
  39. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common.egg-info/requires.txt +0 -0
  40. {devs_common-2.0.3 → devs_common-2.0.4}/devs_common.egg-info/top_level.txt +0 -0
  41. {devs_common-2.0.3 → devs_common-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import re
5
+ import time
5
6
  from datetime import datetime, timezone
6
7
  from pathlib import Path
7
8
  from typing import Dict, List, Optional, Tuple
@@ -817,14 +818,34 @@ class ContainerManager:
817
818
  extra_env=extra_env
818
819
  )
819
820
 
820
- def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
821
- """Start a VS Code tunnel in the container.
821
+ def _get_tunnel_name(self, dev_name: str) -> str:
822
+ """Generate a VS Code tunnel name, truncated to 20 chars.
823
+
824
+ VS Code tunnel names have a 20 character limit. We keep the dev name
825
+ suffix and shorten the project portion if needed.
826
+ """
827
+ project_prefix = self.config.project_prefix if self.config else "dev"
828
+ tunnel_name = f"{project_prefix}-{self.project.info.name}-{dev_name}"
829
+ # Sanitize: tunnel names can only contain alphanumeric, dash, underscore
830
+ tunnel_name = tunnel_name.replace(".", "-").replace("_", "-")
831
+
832
+ if len(tunnel_name) > 20:
833
+ suffix = f"-{dev_name}"
834
+ prefix_budget = 20 - len(suffix)
835
+ if prefix_budget >= 3:
836
+ project_part = f"{project_prefix}-{self.project.info.name}"
837
+ project_part = project_part[:prefix_budget].rstrip("-")
838
+ tunnel_name = f"{project_part}{suffix}"
839
+ else:
840
+ tunnel_name = tunnel_name[:20]
841
+
842
+ return tunnel_name
822
843
 
823
- This runs an interactive tunnel that allows VS Code to connect directly
824
- to the container without SSH. The tunnel process will run in the foreground
825
- and can be stopped with Ctrl+C.
844
+ def tunnel_auth(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
845
+ """Authenticate VS Code tunnel (interactive, one-time setup).
826
846
 
827
- First-time usage requires authentication via a device code flow.
847
+ Runs the device code flow interactively. Auth is stored in a shared
848
+ Docker volume so it persists across containers and recreations.
828
849
 
829
850
  Args:
830
851
  dev_name: Development environment name
@@ -834,62 +855,140 @@ class ContainerManager:
834
855
  extra_env: Additional environment variables to pass to container
835
856
 
836
857
  Raises:
837
- ContainerError: If tunnel startup fails
858
+ ContainerError: If authentication fails
838
859
  """
839
860
  try:
840
- # Prepare container for execution
841
861
  container_name, container_workspace_dir = self._prepare_container_exec(
842
862
  dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
843
863
  )
844
864
 
845
- # Create a tunnel name from the container's devcontainer name
846
- # This will be the name that appears in VS Code Remote Explorer
847
- # VS Code tunnel names have a 20 character limit
848
- project_prefix = self.config.project_prefix if self.config else "dev"
849
- tunnel_name = f"{project_prefix}-{self.project.info.name}-{dev_name}"
850
- # Sanitize: tunnel names can only contain alphanumeric, dash, underscore
851
- tunnel_name = tunnel_name.replace(".", "-").replace("_", "-")
852
-
853
- # Truncate to 20 chars if needed (VS Code tunnel limit)
854
- if len(tunnel_name) > 20:
855
- # Keep the dev name suffix, shorten the project part
856
- suffix = f"-{dev_name}"
857
- prefix_budget = 20 - len(suffix)
858
- if prefix_budget >= 3:
859
- project_part = f"{project_prefix}-{self.project.info.name}"
860
- project_part = project_part[:prefix_budget].rstrip("-")
861
- tunnel_name = f"{project_part}{suffix}"
862
- else:
863
- # Dev name itself is very long, just truncate the whole thing
864
- tunnel_name = tunnel_name[:20]
865
+ tunnel_name = self._get_tunnel_name(dev_name)
865
866
 
866
- console.print(f"[bold cyan]Starting VS Code tunnel for: {dev_name}[/bold cyan]")
867
+ console.print("[bold cyan]Setting up VS Code tunnel authentication...[/bold cyan]")
867
868
  console.print(f" Container: {container_name}")
868
- console.print(f" Tunnel name: {tunnel_name}")
869
869
  console.print("")
870
- console.print("[yellow]First-time usage requires authentication:[/yellow]")
870
+ console.print("[yellow]Follow the prompts to authenticate:[/yellow]")
871
871
  console.print(" 1. A URL and code will be displayed")
872
872
  console.print(" 2. Open the URL in your browser")
873
873
  console.print(" 3. Enter the code to authenticate with GitHub/Microsoft")
874
874
  console.print("")
875
- console.print("[dim]Press Ctrl+C to stop the tunnel[/dim]")
876
- console.print("")
877
875
 
878
- # Start the tunnel interactively
879
- # The code CLI binary is installed at /usr/local/bin/code in the container
880
876
  cmd = [
881
877
  'docker', 'exec', '-it',
882
878
  container_name,
883
- '/usr/local/bin/code', 'tunnel',
884
- '--accept-server-license-terms',
885
- '--name', tunnel_name
879
+ '/usr/local/bin/code', 'tunnel', 'user', 'login',
880
+ '--provider', 'github'
886
881
  ]
887
882
 
888
883
  if debug:
889
884
  console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
890
885
 
891
- # Run interactively so user can see auth prompts and tunnel status
892
- subprocess.run(cmd, check=False)
886
+ result = subprocess.run(cmd, check=False)
887
+
888
+ if result.returncode == 0:
889
+ console.print("")
890
+ console.print("[bold green]Authentication successful![/bold green]")
891
+ console.print(" Auth is shared across all containers on this machine.")
892
+ console.print("")
893
+ console.print(f" Start a tunnel: devs tunnel {dev_name}")
894
+ console.print(f" Then connect: code --remote tunnel+{tunnel_name} {container_workspace_dir}")
895
+ else:
896
+ console.print("")
897
+ console.print("[yellow]Authentication was cancelled or failed.[/yellow]")
898
+
899
+ except (DockerError, subprocess.SubprocessError) as e:
900
+ raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
901
+
902
+ def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
903
+ """Start a VS Code tunnel in the background.
904
+
905
+ Requires prior authentication via tunnel_auth(). The tunnel runs
906
+ detached inside the container.
907
+
908
+ Args:
909
+ dev_name: Development environment name
910
+ workspace_dir: Workspace directory path
911
+ debug: Show debug output for devcontainer operations
912
+ live: Whether the container is in live mode
913
+ extra_env: Additional environment variables to pass to container
914
+
915
+ Raises:
916
+ ContainerError: If tunnel startup fails
917
+ """
918
+ try:
919
+ container_name, container_workspace_dir = self._prepare_container_exec(
920
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
921
+ )
922
+
923
+ tunnel_name = self._get_tunnel_name(dev_name)
924
+ log_file = '/tmp/vscode-tunnel.log'
925
+ tunnel_cmd = f'/usr/local/bin/code tunnel --accept-server-license-terms --name {tunnel_name}'
926
+ vscode_cmd = f"code --remote tunnel+{tunnel_name} {container_workspace_dir}"
927
+
928
+ console.print(f"[bold cyan]Starting VS Code tunnel for: {dev_name}[/bold cyan]")
929
+ console.print(f" Container: {container_name}")
930
+ console.print(f" Tunnel name: {tunnel_name}")
931
+ console.print("")
932
+
933
+ # Start detached
934
+ cmd = [
935
+ 'docker', 'exec', '-d',
936
+ container_name,
937
+ '/bin/sh', '-c',
938
+ f'{tunnel_cmd} > {log_file} 2>&1'
939
+ ]
940
+
941
+ if debug:
942
+ console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
943
+
944
+ subprocess.run(cmd, check=True)
945
+
946
+ # Poll the log to confirm startup and show the URL
947
+ console.print("[dim]Waiting for tunnel to start...[/dim]")
948
+ for i in range(15):
949
+ time.sleep(1)
950
+ result = subprocess.run(
951
+ ['docker', 'exec', container_name, 'cat', log_file],
952
+ capture_output=True, text=True
953
+ )
954
+ output = result.stdout
955
+
956
+ if 'Open this link' in output or 'vscode.dev/tunnel' in output:
957
+ console.print(f"[bold green]Tunnel running![/bold green]")
958
+ console.print("")
959
+ console.print("[bold]Connect from your local machine:[/bold]")
960
+ console.print(f" {vscode_cmd}")
961
+ console.print("")
962
+ for line in output.split('\n'):
963
+ if 'vscode.dev/tunnel' in line:
964
+ console.print(f"[dim]Or open in browser: {line.strip()}[/dim]")
965
+ console.print(f"[dim]Use 'devs tunnel {dev_name} --status' to check status[/dim]")
966
+ console.print(f"[dim]Use 'devs tunnel {dev_name} --kill' to stop[/dim]")
967
+ return
968
+
969
+ if 'log in' in output.lower() or 'device' in output.lower() or 'invalid' in output.lower():
970
+ console.print("[yellow]Authentication required.[/yellow]")
971
+ console.print(f" Run: devs tunnel {dev_name} --auth")
972
+ subprocess.run(
973
+ ['docker', 'exec', container_name, '/usr/local/bin/code', 'tunnel', 'kill'],
974
+ capture_output=True
975
+ )
976
+ return
977
+
978
+ if debug and output.strip():
979
+ console.print(f"[dim]Log ({i+1}s): {output.strip()[-100:]}[/dim]")
980
+
981
+ # Timeout - show what we have
982
+ result = subprocess.run(
983
+ ['docker', 'exec', container_name, 'cat', log_file],
984
+ capture_output=True, text=True
985
+ )
986
+ if result.stdout.strip():
987
+ console.print("[yellow]Tunnel may still be starting. Log output:[/yellow]")
988
+ console.print(result.stdout.strip())
989
+ else:
990
+ console.print("[yellow]Tunnel started but no output yet. Check status with:[/yellow]")
991
+ console.print(f" devs tunnel {dev_name} --status")
893
992
 
894
993
  except (DockerError, subprocess.SubprocessError) as e:
895
994
  raise ContainerError(f"Failed to start tunnel in {dev_name}: {e}")
@@ -87,9 +87,9 @@ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhisto
87
87
  ENV DEVCONTAINER=true
88
88
 
89
89
  # Create workspace and config directories and set permissions
90
- RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig /home/node/.vscode-server/extensions && \
90
+ RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig /home/node/.vscode-server/extensions /home/node/.vscode/cli && \
91
91
  echo '[]' > /home/node/.vscode-server/extensions/extensions.json && \
92
- chown -R node:node /workspaces /home/node /home/node/.vscode-server && \
92
+ chown -R node:node /workspaces /home/node /home/node/.vscode-server /home/node/.vscode && \
93
93
  chmod 755 /workspaces
94
94
 
95
95
  WORKDIR /workspaces
@@ -83,7 +83,8 @@
83
83
  // "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-extensions,target=/home/node/.vscode-server/extensions,type=volume",
84
84
  // .vscode-server includes extension state which we don't want to keep
85
85
  // "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-data,target=/home/node/.vscode-server,type=volume",
86
- "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume"
86
+ "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume",
87
+ "source=devs-vscode-cli-tunnel,target=/home/node/.vscode/cli,type=volume"
87
88
  ],
88
89
  "remoteEnv": {
89
90
  "NODE_OPTIONS": "--max-old-space-size=4096",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.3
3
+ Version: 2.0.4
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.3"
7
+ version = "2.0.4"
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