diff --git a/claude_bottle/backend/docker/backend.py b/claude_bottle/backend/docker/backend.py index d699ae9..ca7719e 100644 --- a/claude_bottle/backend/docker/backend.py +++ b/claude_bottle/backend/docker/backend.py @@ -292,12 +292,48 @@ class DockerBottleBackend(BottleBackend): def provision_skills(self, plan: BottlePlan, target: str) -> None: """Copy each of the agent's named skills from the host's ~/.claude/skills// into the container's equivalent path. - No-op when the agent has no skills.""" + For each skill: ensure parent dir, wipe any prior copy, then + `docker cp /. :/` so the contents are + copied into a freshly-created destination dir. No-op when the + agent has no skills.""" assert isinstance(plan, DockerBottlePlan) agent = plan.spec.manifest.agents[plan.spec.agent_name] if not agent.skills: return - skills_mod.skills_copy_into(target, list(agent.skills)) + + container = target + container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") + skills_dir = os.environ.get( + "CLAUDE_BOTTLE_CONTAINER_SKILLS_DIR", f"{container_home}/.claude/skills" + ) + + subprocess.run( + ["docker", "exec", container, "mkdir", "-p", skills_dir], + stdout=subprocess.DEVNULL, + check=True, + ) + + for n in agent.skills: + src = skills_mod.host_skill_dir(n) + if not os.path.isdir(src): + die(f"skill '{n}' disappeared from host between validation and copy at {src}.") + dst = f"{skills_dir}/{n}" + info(f"copying skill {n} into {container}:{dst}") + subprocess.run( + ["docker", "exec", container, "rm", "-rf", dst], + stdout=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["docker", "exec", container, "mkdir", "-p", dst], + stdout=subprocess.DEVNULL, + check=True, + ) + subprocess.run( + ["docker", "cp", f"{src}/.", f"{container}:{dst}/"], + stdout=subprocess.DEVNULL, + check=True, + ) def provision_ssh(self, plan: BottlePlan, target: str) -> None: """If the bottle has SSH entries, set up the in-container diff --git a/claude_bottle/skills.py b/claude_bottle/skills.py index 4efee86..a879ec8 100644 --- a/claude_bottle/skills.py +++ b/claude_bottle/skills.py @@ -1,17 +1,11 @@ -"""Skill copier: host's ~/.claude/skills// -> container's -~/.claude/skills//, preserving directory structure.""" +"""Skill discovery and host-side validation. The copy step itself +lives on the backend (e.g. DockerBottleBackend.provision_skills).""" from __future__ import annotations import os -import subprocess -from .log import die, info - -CONTAINER_HOME = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") -CONTAINER_SKILLS_DIR = os.environ.get( - "CLAUDE_BOTTLE_CONTAINER_SKILLS_DIR", f"{CONTAINER_HOME}/.claude/skills" -) +from .log import die def host_skill_dir(name: str) -> str: @@ -38,39 +32,3 @@ def skills_validate_all(names: list[str]) -> None: that's already known to fail.""" for n in names: require_host_skill(n) - - -def skills_copy_into(container: str, names: list[str]) -> None: - """For each named skill, ensure the parent dir exists, wipe any - prior copy, then `docker cp /. :/` so the - contents are copied into a freshly-created destination dir.""" - if not names: - return - - subprocess.run( - ["docker", "exec", container, "mkdir", "-p", CONTAINER_SKILLS_DIR], - stdout=subprocess.DEVNULL, - check=True, - ) - - for n in names: - src = host_skill_dir(n) - if not os.path.isdir(src): - die(f"skill '{n}' disappeared from host between validation and copy at {src}.") - dst = f"{CONTAINER_SKILLS_DIR}/{n}" - info(f"copying skill {n} into {container}:{dst}") - subprocess.run( - ["docker", "exec", container, "rm", "-rf", dst], - stdout=subprocess.DEVNULL, - check=True, - ) - subprocess.run( - ["docker", "exec", container, "mkdir", "-p", dst], - stdout=subprocess.DEVNULL, - check=True, - ) - subprocess.run( - ["docker", "cp", f"{src}/.", f"{container}:{dst}/"], - stdout=subprocess.DEVNULL, - check=True, - )