devs-common 2.0.5__tar.gz → 2.0.7__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.5 → devs_common-2.0.7}/PKG-INFO +1 -1
  2. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/container.py +100 -5
  3. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/Dockerfile +8 -3
  4. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/devcontainer.json +2 -1
  5. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/__init__.py +4 -2
  6. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/docker_client.py +15 -0
  7. devs_common-2.0.7/devs_common/utils/repo_cache.py +227 -0
  8. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/PKG-INFO +1 -1
  9. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/SOURCES.txt +2 -1
  10. {devs_common-2.0.5 → devs_common-2.0.7}/pyproject.toml +1 -1
  11. {devs_common-2.0.5 → devs_common-2.0.7}/LICENSE +0 -0
  12. {devs_common-2.0.5 → devs_common-2.0.7}/README.md +0 -0
  13. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/__init__.py +0 -0
  14. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/config.py +0 -0
  15. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/__init__.py +0 -0
  16. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/project.py +0 -0
  17. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/core/workspace.py +0 -0
  18. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/devs_config.py +0 -0
  19. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/exceptions.py +0 -0
  20. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  21. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  22. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  23. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  24. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  25. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  26. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  27. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  28. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  29. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  30. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  31. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  32. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/templates/vscode-settings-reference.md +0 -0
  33. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/config_hash.py +0 -0
  34. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/console.py +0 -0
  35. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/devcontainer.py +0 -0
  36. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/devcontainer_template.py +0 -0
  37. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/file_utils.py +0 -0
  38. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common/utils/git_utils.py +0 -0
  39. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/dependency_links.txt +0 -0
  40. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/requires.txt +0 -0
  41. {devs_common-2.0.5 → devs_common-2.0.7}/devs_common.egg-info/top_level.txt +0 -0
  42. {devs_common-2.0.5 → devs_common-2.0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -293,9 +293,11 @@ class ContainerManager:
293
293
  self.docker.stop_container(existing_container['name'])
294
294
  self.docker.remove_container(existing_container['name'], force=True)
295
295
  else:
296
- # Container exists but not running, remove it
297
- console.print(f" 🗑️ Container exists but not running, removing...")
298
- self.docker.remove_container(existing_container['name'], force=True)
296
+ # Container exists but not running with matching config, just restart it
297
+ console.print(f" 🔄 Restarting stopped container...")
298
+ self.docker.start_container(existing_container['name'])
299
+ console.print(f" ✅ Container restarted successfully")
300
+ return True
299
301
  else:
300
302
  if rebuild_needed or force_rebuild:
301
303
  console.print(f" 🔨 Rebuild needed (devcontainer files changed or force_rebuild)")
@@ -467,7 +469,41 @@ class ContainerManager:
467
469
 
468
470
  except DockerError as e:
469
471
  raise ContainerError(f"Failed to list containers: {e}")
470
-
472
+
473
+ @staticmethod
474
+ def list_all_containers() -> List[ContainerInfo]:
475
+ """List all devs-managed containers across all projects.
476
+
477
+ Returns:
478
+ List of ContainerInfo objects sorted by project name then dev name
479
+ """
480
+ try:
481
+ docker_client = DockerClient()
482
+ containers = docker_client.find_containers_by_labels({"devs.managed": "true"})
483
+
484
+ result = []
485
+ for container_data in containers:
486
+ dev_name = container_data['labels'].get('devs.dev', 'unknown')
487
+ project_name = container_data['labels'].get('devs.project', 'unknown')
488
+
489
+ container_info = ContainerInfo(
490
+ name=container_data['name'],
491
+ dev_name=dev_name,
492
+ project_name=project_name,
493
+ status=container_data['status'],
494
+ container_id=container_data['id'],
495
+ created=_parse_docker_timestamp(container_data['created']),
496
+ labels=container_data['labels']
497
+ )
498
+
499
+ result.append(container_info)
500
+
501
+ result.sort(key=lambda c: (c.project_name, c.dev_name))
502
+ return result
503
+
504
+ except DockerError as e:
505
+ raise ContainerError(f"Failed to list containers: {e}")
506
+
471
507
  def find_aborted_containers(self, all_projects: bool = False) -> List[ContainerInfo]:
472
508
  """Find aborted devs containers that failed during setup.
473
509
 
@@ -841,6 +877,65 @@ class ContainerManager:
841
877
 
842
878
  return tunnel_name
843
879
 
880
+ 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
+ """Authenticate VS Code tunnel (interactive, one-time setup).
882
+
883
+ Runs the device code flow interactively inside the container. Auth is
884
+ stored in a bind-mounted directory (~/.devs/vscode-cli/) so it persists
885
+ across container recreations and is shared across all containers.
886
+
887
+ Args:
888
+ dev_name: Development environment name
889
+ workspace_dir: Workspace directory path
890
+ debug: Show debug output for devcontainer operations
891
+ live: Whether the container is in live mode
892
+ extra_env: Additional environment variables to pass to container
893
+
894
+ Raises:
895
+ ContainerError: If authentication fails
896
+ """
897
+ try:
898
+ container_name, container_workspace_dir = self._prepare_container_exec(
899
+ dev_name, workspace_dir, debug=debug, live=live, extra_env=extra_env
900
+ )
901
+
902
+ tunnel_name = self._get_tunnel_name(dev_name)
903
+
904
+ console.print("🔐 Setting up VS Code tunnel authentication...")
905
+ console.print(f" Container: {container_name}")
906
+ 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
+
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 is shared across all containers on this machine.")
929
+ console.print("")
930
+ console.print(f" Start a tunnel: devs tunnel {dev_name}")
931
+ 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
+
936
+ except (DockerError, subprocess.SubprocessError) as e:
937
+ raise ContainerError(f"Failed to authenticate tunnel in {dev_name}: {e}")
938
+
844
939
  def start_tunnel(self, dev_name: str, workspace_dir: Path, debug: bool = False, live: bool = False, extra_env: Optional[Dict[str, str]] = None) -> None:
845
940
  """Start a VS Code tunnel in the background.
846
941
 
@@ -877,7 +972,7 @@ class ContainerManager:
877
972
  'docker', 'exec', '-d',
878
973
  container_name,
879
974
  '/bin/sh', '-c',
880
- f'{tunnel_cmd} > {log_file} 2>&1'
975
+ f'VSCODE_CLI_DATA_DIR=/home/node/.vscode/cli {tunnel_cmd} > {log_file} 2>&1'
881
976
  ]
882
977
 
883
978
  if debug:
@@ -119,8 +119,11 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
119
119
  -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
120
120
  -x
121
121
 
122
- # Install Claude CLI and OpenAI Codex CLI
123
- RUN npm install -g @anthropic-ai/claude-code @openai/codex
122
+ # Install Claude CLI via native installer
123
+ RUN curl -fsSL https://claude.ai/install.sh | bash
124
+
125
+ # Install OpenAI Codex CLI
126
+ RUN npm install -g @openai/codex
124
127
 
125
128
  # Install VS Code CLI for tunnel support (needs root for /usr/local/bin)
126
129
  # Using the standalone CLI which supports the 'tunnel' command
@@ -139,7 +142,9 @@ RUN ARCH=$(dpkg --print-architecture) && \
139
142
  RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
140
143
  echo "export CLAUDE_CONFIG_DIR=\"/home/node/claudeconfig\"" >> "$shell_rc" && \
141
144
  echo "export CODEX_CONFIG_HOME=\"/home/node/codexconfig\"" >> "$shell_rc" && \
142
- echo "alias claude=\"/usr/local/share/npm-global/bin/claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
145
+ echo "export VSCODE_CLI_DATA_DIR=\"/home/node/.vscode/cli\"" >> "$shell_rc" && \
146
+ echo "export PATH=\"/home/node/.local/bin:\$PATH\"" >> "$shell_rc" && \
147
+ echo "alias claude=\"claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
143
148
  echo "alias codex=\"/usr/local/share/npm-global/bin/codex --full-auto\"" >> "$shell_rc" && \
144
149
  echo "" >> "$shell_rc" && \
145
150
  echo "# Auto-activate Python venv if it exists" >> "$shell_rc" && \
@@ -96,7 +96,8 @@
96
96
  "DEVS_DEBUG": "${localEnv:DEVS_DEBUG}",
97
97
  "DEVS_LIVE_MODE": "${localEnv:DEVS_LIVE_MODE}",
98
98
  "DEVS_ENV_MOUNT_PATH": "${localEnv:DEVS_ENV_MOUNT_PATH}",
99
- "DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}"
99
+ "DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}",
100
+ "VSCODE_CLI_DATA_DIR": "/home/node/.vscode/cli"
100
101
  },
101
102
  "workspaceFolder": "/workspaces/${localEnv:WORKSPACE_FOLDER_NAME}",
102
103
  "postCreateCommand": "/usr/local/bin/post-create-wrapper.sh",
@@ -16,10 +16,11 @@ from .git_utils import (
16
16
 
17
17
  from .docker_client import DockerClient
18
18
  from .devcontainer import DevContainerCLI
19
+ from .repo_cache import RepoCache
19
20
 
20
21
  __all__ = [
21
22
  "copy_file_list",
22
- "copy_directory_tree",
23
+ "copy_directory_tree",
23
24
  "safe_remove_directory",
24
25
  "ensure_directory_exists",
25
26
  "get_directory_size",
@@ -27,5 +28,6 @@ __all__ = [
27
28
  "get_tracked_files",
28
29
  "is_git_repository",
29
30
  "DockerClient",
30
- "DevContainerCLI"
31
+ "DevContainerCLI",
32
+ "RepoCache",
31
33
  ]
@@ -81,6 +81,21 @@ class DockerClient:
81
81
  except DockerException as e:
82
82
  raise DockerError(f"Error checking container status {name}: {e}")
83
83
 
84
+ def start_container(self, name: str) -> None:
85
+ """Start a stopped container.
86
+
87
+ Args:
88
+ name: Container name
89
+
90
+ Raises:
91
+ DockerError: If starting fails
92
+ """
93
+ try:
94
+ container = self.client.containers.get(name)
95
+ container.start()
96
+ except DockerException as e:
97
+ raise DockerError(f"Error starting container {name}: {e}")
98
+
84
99
  def stop_container(self, name: str) -> None:
85
100
  """Stop a container.
86
101
 
@@ -0,0 +1,227 @@
1
+ """Repository cache for cloning GitHub repos locally."""
2
+
3
+ import os
4
+ import subprocess
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+
9
+ from ..exceptions import DevsError
10
+ from .console import get_console
11
+
12
+ console = get_console()
13
+
14
+
15
+ class RepoCache:
16
+ """Manages a local cache of cloned GitHub repositories.
17
+
18
+ Repos are cloned into a cache directory (default: ~/.devs/repocache/)
19
+ using the naming convention org-repo (e.g. ideonate-devs).
20
+
21
+ Args:
22
+ cache_dir: Directory for cached repos. Defaults to ~/.devs/repocache/.
23
+ token: GitHub token for private repo access. If not provided, falls
24
+ back to GH_TOKEN / GITHUB_TOKEN environment variables.
25
+ default_branches: Ordered list of branch names to try when no explicit
26
+ branch is requested. Defaults to detecting via symbolic-ref, then
27
+ trying "main" and "master".
28
+ clean: If True, run ``git clean -fd`` after checkout to remove
29
+ untracked files from previous runs.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ cache_dir: Optional[Path] = None,
35
+ token: Optional[str] = None,
36
+ default_branches: Optional[List[str]] = None,
37
+ clean: bool = False,
38
+ ) -> None:
39
+ self.cache_dir = cache_dir or Path.home() / ".devs" / "repocache"
40
+ self._token = token
41
+ self._default_branches = default_branches
42
+ self._clean = clean
43
+
44
+ def _repo_name_to_dir_name(self, repo_name: str) -> str:
45
+ """Convert org/repo to org-repo directory name."""
46
+ return repo_name.replace("/", "-").lower()
47
+
48
+ def _get_token(self) -> Optional[str]:
49
+ """Return the configured token, falling back to environment variables."""
50
+ if self._token:
51
+ return self._token
52
+ return os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
53
+
54
+ def _build_clone_url(self, repo_name: str) -> str:
55
+ """Build clone URL, using token if available for private repos."""
56
+ token = self._get_token()
57
+ if token:
58
+ return f"https://x-access-token:{token}@github.com/{repo_name}.git"
59
+ return f"https://github.com/{repo_name}.git"
60
+
61
+ def get_repo_path(self, repo_name: str) -> Path:
62
+ """Return the local cache path for a repo without cloning.
63
+
64
+ Args:
65
+ repo_name: GitHub repo in org/repo format.
66
+
67
+ Returns:
68
+ Path where the repo would be cached.
69
+ """
70
+ return self.cache_dir / self._repo_name_to_dir_name(repo_name)
71
+
72
+ def ensure_repo(self, repo_name: str, branch: Optional[str] = None) -> Path:
73
+ """Ensure a repository is cloned and up-to-date in the cache.
74
+
75
+ Args:
76
+ repo_name: GitHub repo in org/repo format (e.g. "ideonate/devs")
77
+ branch: Optional branch to checkout. Defaults to repo's default branch.
78
+
79
+ Returns:
80
+ Path to the cached repository directory.
81
+
82
+ Raises:
83
+ DevsError: If cloning or updating fails.
84
+ """
85
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
86
+
87
+ dir_name = self._repo_name_to_dir_name(repo_name)
88
+ repo_path = self.cache_dir / dir_name
89
+
90
+ if repo_path.exists() and (repo_path / ".git").exists():
91
+ self._update_repo(repo_path, repo_name, branch)
92
+ else:
93
+ self._clone_repo(repo_path, repo_name, branch)
94
+
95
+ return repo_path
96
+
97
+ def _clone_repo(self, repo_path: Path, repo_name: str, branch: Optional[str] = None) -> None:
98
+ """Clone a repository into the cache."""
99
+ # Remove directory if it exists but isn't a valid git repo
100
+ if repo_path.exists():
101
+ shutil.rmtree(repo_path)
102
+
103
+ clone_url = self._build_clone_url(repo_name)
104
+ console.print(f" Cloning {repo_name} into cache...")
105
+
106
+ repo_path.parent.mkdir(parents=True, exist_ok=True)
107
+
108
+ cmd = ["git", "clone", clone_url, str(repo_path)]
109
+ result = subprocess.run(cmd, capture_output=True, text=True)
110
+ if result.returncode != 0:
111
+ raise DevsError(
112
+ f"Failed to clone {repo_name}: {result.stderr.strip()}"
113
+ )
114
+
115
+ target = branch or self._detect_branch(repo_path)
116
+ if target:
117
+ self._checkout_branch(repo_path, target)
118
+
119
+ def _update_repo(self, repo_path: Path, repo_name: str, branch: Optional[str] = None) -> None:
120
+ """Fetch latest changes for an existing cached repo."""
121
+ console.print(f" Updating cached repo {repo_name}...")
122
+
123
+ # Update remote URL in case token changed
124
+ clone_url = self._build_clone_url(repo_name)
125
+ subprocess.run(
126
+ ["git", "remote", "set-url", "origin", clone_url],
127
+ cwd=repo_path,
128
+ capture_output=True,
129
+ text=True,
130
+ )
131
+
132
+ result = subprocess.run(
133
+ ["git", "fetch", "--all"],
134
+ cwd=repo_path,
135
+ capture_output=True,
136
+ text=True,
137
+ )
138
+
139
+ if result.returncode != 0:
140
+ # Fetch failed – re-clone from scratch
141
+ console.print(f" Fetch failed, re-cloning {repo_name}...")
142
+ self._clone_repo(repo_path, repo_name, branch)
143
+ return
144
+
145
+ # Checkout the requested branch (or default)
146
+ target = branch or self._detect_branch(repo_path)
147
+ if target:
148
+ self._checkout_branch(repo_path, target)
149
+
150
+ def _checkout_branch(self, repo_path: Path, branch: str) -> None:
151
+ """Checkout a specific branch, pulling latest changes."""
152
+ result = subprocess.run(
153
+ ["git", "checkout", "-f", branch],
154
+ cwd=repo_path,
155
+ capture_output=True,
156
+ text=True,
157
+ )
158
+ if result.returncode != 0:
159
+ raise DevsError(
160
+ f"Failed to checkout branch '{branch}': {result.stderr.strip()}"
161
+ )
162
+
163
+ # Reset to match remote so we pick up fetched changes
164
+ subprocess.run(
165
+ ["git", "reset", "--hard", f"origin/{branch}"],
166
+ cwd=repo_path,
167
+ capture_output=True,
168
+ text=True,
169
+ )
170
+
171
+ if self._clean:
172
+ self._clean_untracked(repo_path)
173
+
174
+ def _clean_untracked(self, repo_path: Path) -> None:
175
+ """Remove untracked files and directories from the repo."""
176
+ subprocess.run(
177
+ ["git", "clean", "-fd"],
178
+ cwd=repo_path,
179
+ capture_output=True,
180
+ text=True,
181
+ )
182
+
183
+ def _detect_branch(self, repo_path: Path) -> Optional[str]:
184
+ """Detect the default branch to checkout.
185
+
186
+ If ``default_branches`` was provided at construction, tries each in
187
+ order. Otherwise falls back to symbolic-ref detection then main/master.
188
+ """
189
+ if self._default_branches:
190
+ for branch in self._default_branches:
191
+ result = subprocess.run(
192
+ ["git", "rev-parse", "--verify", f"origin/{branch}"],
193
+ cwd=repo_path,
194
+ capture_output=True,
195
+ text=True,
196
+ )
197
+ if result.returncode == 0:
198
+ return branch
199
+ return None
200
+
201
+ return self._get_default_branch(repo_path)
202
+
203
+ def _get_default_branch(self, repo_path: Path) -> Optional[str]:
204
+ """Detect the default branch of a repo (main or master)."""
205
+ result = subprocess.run(
206
+ ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
207
+ cwd=repo_path,
208
+ capture_output=True,
209
+ text=True,
210
+ )
211
+ if result.returncode == 0 and result.stdout.strip():
212
+ # Output like "refs/remotes/origin/main"
213
+ ref = result.stdout.strip()
214
+ return ref.split("/")[-1]
215
+
216
+ # Fallback: try common branch names
217
+ for branch in ("main", "master"):
218
+ result = subprocess.run(
219
+ ["git", "rev-parse", "--verify", f"origin/{branch}"],
220
+ cwd=repo_path,
221
+ capture_output=True,
222
+ text=True,
223
+ )
224
+ if result.returncode == 0:
225
+ return branch
226
+
227
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.5
3
+ Version: 2.0.7
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -36,4 +36,5 @@ devs_common/utils/devcontainer.py
36
36
  devs_common/utils/devcontainer_template.py
37
37
  devs_common/utils/docker_client.py
38
38
  devs_common/utils/file_utils.py
39
- devs_common/utils/git_utils.py
39
+ devs_common/utils/git_utils.py
40
+ devs_common/utils/repo_cache.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-common"
7
- version = "2.0.5"
7
+ version = "2.0.7"
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