devs-common 2.0.6__tar.gz → 2.0.8__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.6 → devs_common-2.0.8}/PKG-INFO +1 -1
  2. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/config.py +18 -1
  3. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/core/container.py +40 -4
  4. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/Dockerfile +7 -3
  5. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/__init__.py +4 -2
  6. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/docker_client.py +15 -0
  7. devs_common-2.0.8/devs_common/utils/repo_cache.py +227 -0
  8. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common.egg-info/PKG-INFO +1 -1
  9. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common.egg-info/SOURCES.txt +2 -1
  10. {devs_common-2.0.6 → devs_common-2.0.8}/pyproject.toml +1 -1
  11. {devs_common-2.0.6 → devs_common-2.0.8}/LICENSE +0 -0
  12. {devs_common-2.0.6 → devs_common-2.0.8}/README.md +0 -0
  13. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/__init__.py +0 -0
  14. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/core/__init__.py +0 -0
  15. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/core/project.py +0 -0
  16. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/core/workspace.py +0 -0
  17. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/devs_config.py +0 -0
  18. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/exceptions.py +0 -0
  19. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/devcontainer.json +0 -0
  20. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  21. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/post-create-wrapper.sh +0 -0
  22. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  23. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  24. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  25. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  26. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  27. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  28. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  29. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  30. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  31. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  32. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/templates/vscode-settings-reference.md +0 -0
  33. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/config_hash.py +0 -0
  34. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/console.py +0 -0
  35. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/devcontainer.py +0 -0
  36. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/devcontainer_template.py +0 -0
  37. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/file_utils.py +0 -0
  38. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common/utils/git_utils.py +0 -0
  39. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common.egg-info/dependency_links.txt +0 -0
  40. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common.egg-info/requires.txt +0 -0
  41. {devs_common-2.0.6 → devs_common-2.0.8}/devs_common.egg-info/top_level.txt +0 -0
  42. {devs_common-2.0.6 → devs_common-2.0.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 2.0.6
3
+ Version: 2.0.8
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -9,11 +9,25 @@ from abc import ABC, abstractmethod
9
9
  class BaseConfig(ABC):
10
10
  """Base configuration class for devs ecosystem packages."""
11
11
 
12
+ # Default directories shared across CLI and webhook
13
+ CLAUDE_CONFIG_DIR = Path.home() / ".devs" / "claudeconfig"
14
+ CODEX_CONFIG_DIR = Path.home() / ".devs" / "codexconfig"
15
+ VSCODE_CLI_DIR = Path.home() / ".devs" / "vscode-cli"
16
+
12
17
  def __init__(self) -> None:
13
18
  """Initialize base configuration."""
14
19
  self._workspaces_dir: Optional[Path] = None
15
20
  self._bridge_dir: Optional[Path] = None
16
21
  self._project_prefix: Optional[str] = None
22
+
23
+ claude_config_env = os.getenv("DEVS_CLAUDE_CONFIG_DIR")
24
+ self.claude_config_dir = Path(claude_config_env) if claude_config_env else self.CLAUDE_CONFIG_DIR
25
+
26
+ codex_config_env = os.getenv("DEVS_CODEX_CONFIG_DIR")
27
+ self.codex_config_dir = Path(codex_config_env) if codex_config_env else self.CODEX_CONFIG_DIR
28
+
29
+ vscode_cli_env = os.getenv("DEVS_VSCODE_CLI_DIR")
30
+ self.vscode_cli_dir = Path(vscode_cli_env) if vscode_cli_env else self.VSCODE_CLI_DIR
17
31
 
18
32
  @property
19
33
  def workspaces_dir(self) -> Path:
@@ -87,4 +101,7 @@ class BaseConfig(ABC):
87
101
  def ensure_directories(self) -> None:
88
102
  """Ensure required directories exist."""
89
103
  self.workspaces_dir.mkdir(parents=True, exist_ok=True)
90
- self.bridge_dir.mkdir(parents=True, exist_ok=True)
104
+ self.bridge_dir.mkdir(parents=True, exist_ok=True)
105
+ self.claude_config_dir.mkdir(parents=True, exist_ok=True)
106
+ self.codex_config_dir.mkdir(parents=True, exist_ok=True)
107
+ self.vscode_cli_dir.mkdir(parents=True, exist_ok=True)
@@ -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
 
@@ -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
@@ -140,7 +143,8 @@ 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
145
  echo "export VSCODE_CLI_DATA_DIR=\"/home/node/.vscode/cli\"" >> "$shell_rc" && \
143
- echo "alias claude=\"/usr/local/share/npm-global/bin/claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
146
+ echo "export PATH=\"/home/node/.local/bin:\$PATH\"" >> "$shell_rc" && \
147
+ echo "alias claude=\"claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
144
148
  echo "alias codex=\"/usr/local/share/npm-global/bin/codex --full-auto\"" >> "$shell_rc" && \
145
149
  echo "" >> "$shell_rc" && \
146
150
  echo "# Auto-activate Python venv if it exists" >> "$shell_rc" && \
@@ -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.6
3
+ Version: 2.0.8
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.6"
7
+ version = "2.0.8"
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