Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 23 additions & 32 deletions explain/claude_code_plugin/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
from . import xdg_dirs


# IMPORTANT: only import standard library modules or modules with a similar warning here as this
# code must run before dependencies are installed.


repo_root = Path(__file__).resolve().parent.parent.parent


def ensure_sys_paths() -> None:
# Use per-Python-version package directories so switching Python versions
# doesn't require reinstalling (each version keeps its own cached packages).
"""
Add the dependencies directory and repo root to `sys.path`, installing dependencies if needed.
"""
py_version = f"{sys.version_info[0]}.{sys.version_info[1]}"
deps_dir = xdg_dirs.get_plugin_data_dir() / f"packages-{py_version}"
sys.path.insert(0, str(deps_dir))
Expand All @@ -28,12 +25,14 @@ def ensure_sys_paths() -> None:


def _install_deps(deps_dir: Path) -> None:
# First get `explain`'s dependencies.
"""
Install dependencies from `manifest.json` to the given directory using `uv`.
"""
# First, find out `explain`'s dependencies.
manifest = json.loads(
(repo_root / "private/manifest.json").read_text(encoding="utf-8"),
)
deps = manifest["udb_addons"]["explain"]["python_package_deps"]
assert isinstance(deps, list)
deps: list[str] = manifest["udb_addons"]["explain"]["python_package_deps"]

# Then add the ones for the Claude plugin itself.
deps.extend(
Expand All @@ -43,6 +42,7 @@ def _install_deps(deps_dir: Path) -> None:
]
)

# Skip installation if dependencies haven't changed.
checksum_path = deps_dir / "checksum.txt"
checksum_current = hashlib.sha224(json.dumps(deps).encode("utf-8")).hexdigest()
try:
Expand All @@ -55,45 +55,36 @@ def _install_deps(deps_dir: Path) -> None:

deps_dir.mkdir(parents=True, exist_ok=True, mode=0o700)

_run_install_command(
[
sys.executable,
"-m",
"ensurepip",
]
)
try:
uv = os.environ["_UNDO_uv_path"] # Set by `run.sh`.
except KeyError as exc:
raise RuntimeError(
"_UNDO_uv_path not set. This module must be invoked via run.sh."
) from exc

deps_cmd = [
sys.executable,
"-s", # Don't use user site packages.
"-m",
uv,
"pip",
"-q",
"install",
"--ignore-installed", # Ignore what may be installed in the system.
"--quiet",
"--upgrade",
"--target",
".",
str(deps_dir),
] + deps
_run_install_command(deps_cmd, cwd=deps_dir)

checksum_path.write_text(checksum_current)


def _run_install_command(cmd: list[str], *, cwd: Path | None = None) -> None:
try:
subprocess.check_output(
cmd,
deps_cmd,
stderr=subprocess.STDOUT,
text=True,
cwd=cwd,
env={
**os.environ,
"PIP_NO_WARN_SCRIPT_LOCATION": "1",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"UV_NO_PROGRESS": "1",
},
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(
f"Failed to install dependencies with command {shlex.join(cmd)}:\n{exc.output}"
f"Failed to install dependencies with command {shlex.join(deps_cmd)}:\n{exc.output}"
) from exc

checksum_path.write_text(checksum_current)
54 changes: 42 additions & 12 deletions explain/claude_code_plugin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,48 @@

set -euo pipefail

readonly me=$(realpath "${BASH_SOURCE[0]:-$0}")
readonly root=$(dirname "$me")/../..

export PYTHONPATH
PYTHONPATH="$root":${PYTHONPATH:-}
readonly python=$(which python3 2> /dev/null)
if [[ -z "$python" ]]; then
echo "python3 not found" >&2
exit 1
fi
me=$(realpath "${BASH_SOURCE[0]:-$0}")
readonly me
root=$(dirname "$me")/../..
readonly root

readonly plugin_data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/undo/udb_claude_code_plugin"
readonly uv_bin_dir="$plugin_data_dir/bin"

export PYTHONPATH="$root":${PYTHONPATH:-}
export UNDO_telemetry_ui=ai

# -S prevents site packages from being loaded.
exec "$python" -S -m explain.claude_code_plugin "$@"
# Return the path to the uv binary, checking PATH first, then local installation.
find_uv() {
if command -v uv &>/dev/null; then
command -v uv
elif [[ -x "$uv_bin_dir/uv" ]]; then
echo "$uv_bin_dir/uv"
else
return 1
fi
}

# Download and install the uv binary to the plugin data directory.
install_uv() {
echo "Downloading uv..." >&2
if command -v curl &>/dev/null; then
curl -fsSL https://astral.sh/uv/install.sh
elif command -v wget &>/dev/null; then
wget -qO- https://astral.sh/uv/install.sh
else
echo "Neither curl nor wget found. Please install one of them." >&2
return 1
fi | UV_INSTALL_DIR="$plugin_data_dir" sh >&2
}

if ! uv_path=$(find_uv); then
install_uv
if ! uv_path=$(find_uv); then
echo "Failed to install uv" >&2
exit 1
fi
fi
export _UNDO_uv_path="$uv_path" # Used bt `deps.py`.

exec "$uv_path" run --no-project --python 3.10 -m explain.claude_code_plugin "$@"