399ed93dc8
Replaces cli.sh + lib/*.sh with a claude_bottle/ Python package and a
cli.py entry point. No external dependencies — uses only Python's
stdlib (json, subprocess, getpass, tempfile, argparse, re, etc.).
- claude_bottle/{log,docker,manifest,env_resolve,network,pipelock,
skills,ssh,cli}.py mirror the previous lib/*.sh modules.
- Tests converted to unittest under tests/test_*.py with a stdlib
runner at tests/run_tests.py (unit | integration | path).
- .githooks/commit-msg ported to Python; same Conventional Commits rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
"""Skill copier: host's ~/.claude/skills/<name>/ -> container's
|
|
~/.claude/skills/<name>/, preserving directory structure."""
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
def host_skill_dir(name: str) -> str:
|
|
home = os.environ.get("HOME")
|
|
if not home:
|
|
die("HOME not set")
|
|
return f"{home}/.claude/skills/{name}"
|
|
|
|
|
|
def host_skill_exists(name: str) -> bool:
|
|
return os.path.isdir(host_skill_dir(name))
|
|
|
|
|
|
def require_host_skill(name: str) -> None:
|
|
if not host_skill_exists(name):
|
|
die(
|
|
f"skill '{name}' not found on host at {host_skill_dir(name)}. "
|
|
f"Create it under ~/.claude/skills/, then re-run."
|
|
)
|
|
|
|
|
|
def skills_validate_all(names: list[str]) -> None:
|
|
"""Use BEFORE the y/N so the user does not get asked about a plan
|
|
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 <host>/. <container>:<dst>/` 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,
|
|
)
|