feat(terminal): use project name for in-container cwd and clone directory

This commit is contained in:
Developer
2026-06-17 13:26:16 +00:00
parent 93da9b42a2
commit 3e59a257dc
9 changed files with 196 additions and 146 deletions
+49 -25
View File
@@ -5,6 +5,7 @@ import contextlib
import glob as glob_module
import logging
import os
import re
import shutil
import subprocess
import uuid
@@ -81,29 +82,27 @@ logger = logging.getLogger(__name__)
_event_bus = InstanceEventBus()
def _slugify_directory_name(name: str) -> str:
"""Return a filesystem-safe, lowercase directory slug."""
slug = name.lower().strip()
slug = re.sub(r"[^a-z0-9_-]+", "-", slug)
slug = re.sub(r"-+", "-", slug).strip("-")
return slug or "project"
def _get_repository_mount_name(
project: Project,
repo: GitRepository,
workspace: "Workspace | None" = None,
) -> str:
"""Return the directory name the repository should appear under in the container.
When a workspace exists, the on-disk layout is
``/data/working-copies/{workspace_id}/{repo_name}/``, so the repo-named
directory is already available as the basename of ``workspace.path``. For
legacy repo-only instances we fall back to parsing the remote URL like a
standard ``git clone`` would, then to the user-provided repository name.
The in-container layout is now ``/home/user/{project_name}/`` so that the
terminal starts directly in the project directory. The workspace path is
ignored for naming purposes; it only provides the source directory to
mount.
"""
if workspace is not None and workspace.path:
return os.path.basename(os.path.normpath(workspace.path))
if repo.remote_url:
base_url = extract_base_repo_url(repo.remote_url) or repo.remote_url
name = base_url.rstrip("/").split("/")[-1]
if name.endswith(".git"):
name = name[:-4]
if name:
return name
return repo.name
return _slugify_directory_name(project.name)
def _chown_path(path: str, uid: int, gid: int) -> None:
@@ -271,10 +270,11 @@ def clone_git_repo(
remote_url: str,
branch: str | None,
clone_parent: str,
project_name: str | None = None,
) -> str:
"""Clone or pull a git repository.
Returns the path to the cloned repo (repo-clone directory).
Returns the path to the cloned repo directory.
"""
import hashlib
@@ -286,7 +286,13 @@ def clone_git_repo(
).hexdigest()[:12]
repo_name = remote_url.split("/")[-1].replace(".git", "") or "repo"
clone_dir = os.path.join(clone_parent, "git-mounts", f"{repo_name}-{url_hash}")
repo_path = os.path.join(clone_dir, "repo-clone")
repo_path = clone_repository(
remote_url,
None, # No SSH key for now - can be added later
clone_dir,
branch or "main",
project_name=project_name,
)
if not os.path.exists(repo_path):
try:
@@ -296,6 +302,7 @@ def clone_git_repo(
None, # No SSH key for now - can be added later
clone_dir,
branch or "main",
project_name=project_name,
)
logger.debug("Cloned git mount repository %s to %s", remote_url, repo_path)
except Exception as exc:
@@ -443,7 +450,8 @@ async def resolve_single_git_mount(
logger.warning("Git mount skipped: no instance_dir provided for cloning")
return []
# Clone or pull the repository
# Clone or pull the repository. Git mounts are auxiliary, so they keep
# using the repository URL basename rather than the project name.
try:
repo_path = await asyncio.to_thread(
clone_git_repo, remote_url, branch, instance_dir
@@ -1020,9 +1028,10 @@ async def prepare_manifest_instance(
workspace: Workspace | None = None
if instance.workspace_id:
workspace = await session.get(Workspace, instance.workspace_id)
project = await session.get(Project, instance.project_id) if instance.project_id else None
repo_name = (
_get_repository_mount_name(repo, workspace)
if repo
_get_repository_mount_name(project, repo, workspace)
if project and repo
else os.path.basename(os.path.normpath(repo_path))
)
variables = {
@@ -1186,7 +1195,7 @@ async def create_tool_instance(
)
home_dir = tool_type.home_directory or "/home/user"
mount_name = _get_repository_mount_name(repo, workspace)
mount_name = _get_repository_mount_name(project, repo, workspace)
workspace_target = f"{home_dir}/{mount_name}"
compose_content = f"""version: "3.8"\nservices:
@@ -1223,7 +1232,7 @@ async def create_tool_instance(
# Manifest templates use WORKSPACE_PATH; REPO_PATH is retained as a
# deprecated alias for backward compatibility with older templates.
mount_name = _get_repository_mount_name(repo, workspace)
mount_name = _get_repository_mount_name(project, repo, workspace)
variables = {
"IMAGE_TAG": image_tag,
"INSTANCE_NAME": instance_name.lower(),
@@ -1252,7 +1261,7 @@ async def create_tool_instance(
"TOOL_PORT": tool_port,
"USER_ID": str(user_id),
"PROJECT_ID": str(project_id),
"WORKSPACE_NAME": _get_repository_mount_name(repo, workspace),
"WORKSPACE_NAME": _get_repository_mount_name(project, repo, workspace),
"HOME_DIRECTORY": tool_type.home_directory or "/home/user",
}
compose_content = render_compose_template(tool_type.compose_template, variables)
@@ -1574,6 +1583,10 @@ async def start_tool_instance(
else:
# ── LEGACY FLOW ──────────────────────────────────────────
if instance.clone_mode == "clone" and not instance.workspace_id:
clone_project = await session.get(Project, instance.project_id) if instance.project_id else None
clone_name = _slugify_directory_name(clone_project.name) if clone_project else "repo-clone"
repo_path = os.path.join(instance_dir, clone_name)
repo = await session.get(GitRepository, instance.repository_id)
if repo and repo.ssh_key_id:
ssh_key = await session.get(SSHKey, repo.ssh_key_id)
@@ -1599,6 +1612,13 @@ async def start_tool_instance(
exc,
)
# Ensure the legacy compose mounts the project-named clone directory.
extra_volumes.append({
"source": repo_path,
"target": f"{home_dir}/{clone_name}",
"type": "bind",
})
if port_override or start_command or working_directory or extra_volumes:
modify_compose_file(
instance.compose_path,
@@ -2104,7 +2124,11 @@ async def delete_tool_instance(
os.path.dirname(instance.compose_path) if instance.compose_path else None
)
if instance_dir:
clone_path = os.path.join(instance_dir, "repo-clone")
clone_project = await session.get(Project, instance.project_id) if instance.project_id else None
clone_name = _slugify_directory_name(clone_project.name) if clone_project else "repo-clone"
clone_path = os.path.join(instance_dir, clone_name)
if not os.path.exists(clone_path):
clone_path = os.path.join(instance_dir, "repo-clone")
if os.path.exists(clone_path):
is_dirty, changed_files = check_dirty_state(clone_path)
if is_dirty: