Skip to content
Open
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
74 changes: 74 additions & 0 deletions plugin/scripts/_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,80 @@ importlib.import_module(sys.argv[1])
PY
}

claude_smart_canonical_dir() {
local dir
dir="$1"
[ -d "$dir" ] || return 1
(cd "$dir" 2>/dev/null && pwd -P)
}

claude_smart_is_reflexio_session_copy() {
local plugin_root root reflexio_root
plugin_root="$(claude_smart_canonical_dir "$1" 2>/dev/null || true)"
[ -n "$plugin_root" ] || return 1
reflexio_root="$(claude_smart_canonical_dir "$HOME/.reflexio" 2>/dev/null || true)"
[ -n "$reflexio_root" ] || return 1
case "$plugin_root" in
"$reflexio_root"/Cu*) ;;
*) return 1 ;;
esac
root="${plugin_root#"$reflexio_root"/}"
case "$root" in
Cu*/*|Cu*) return 0 ;;
esac
return 1
}

claude_smart_stable_plugin_root_for_session_copy() {
local current candidate current_real candidate_real glob
current="$1"
if ! claude_smart_is_reflexio_session_copy "$current"; then
return 1
fi
current_real="$(claude_smart_canonical_dir "$current" 2>/dev/null || true)"

for candidate in \
"$HOME/.reflexio/plugin-root" \
"$HOME/.claude/plugins/marketplaces/reflexioai/plugin" \
"$HOME/.codex/plugins/cache/reflexioai/claude-smart/current"
do
[ -f "$candidate/pyproject.toml" ] || continue
candidate_real="$(claude_smart_canonical_dir "$candidate" 2>/dev/null || true)"
[ -n "$candidate_real" ] || continue
[ "$candidate_real" != "$current_real" ] || continue
if claude_smart_is_reflexio_session_copy "$candidate_real"; then
continue
fi
printf '%s\n' "$candidate_real"
return 0
done

for glob in "$HOME/.claude/plugins/cache/reflexioai/claude-smart"/* "$HOME/.codex/plugins/cache/reflexioai/claude-smart"/*; do
[ -f "$glob/pyproject.toml" ] || continue
candidate_real="$(claude_smart_canonical_dir "$glob" 2>/dev/null || true)"
[ -n "$candidate_real" ] || continue
[ "$candidate_real" != "$current_real" ] || continue
if claude_smart_is_reflexio_session_copy "$candidate_real"; then
continue
fi
printf '%s\n' "$candidate_real"
return 0
done
return 1
}

claude_smart_reexec_stable_plugin_root_if_needed() {
local plugin_root script stable
plugin_root="$1"
script="$2"
stable="$(claude_smart_stable_plugin_root_for_session_copy "$plugin_root" 2>/dev/null || true)"
[ -n "$stable" ] || return 0
[ -x "$stable/scripts/$script" ] || return 0
echo "[claude-smart] redirecting per-session plugin copy $plugin_root to stable root $stable" >&2
shift 2
exec bash "$stable/scripts/$script" "$@"
}

claude_smart_download() {
local url dest src _CS_PY
url="$1"
Expand Down
1 change: 1 addition & 0 deletions plugin/scripts/backend-service.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fi
# CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
# explicitly if we can resolve it from our own (post-login-path) PATH.
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "backend-service.sh" "$@"

if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
Expand Down
1 change: 1 addition & 0 deletions plugin/scripts/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ claude_smart_prepend_node_bins
claude_smart_source_reflexio_env

PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "cli.sh" "$@"

# If the Setup hook recorded an install failure, surface that reason
# instead of falling through to a generic "uv not found" — mirrors the
Expand Down
1 change: 1 addition & 0 deletions plugin/scripts/dashboard-service.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ CMD="${1:-start}"
PORT=3001

PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "dashboard-service.sh" "$@"
DASHBOARD_DIR="$PLUGIN_ROOT/dashboard"
WORKSPACE_CWD="${PWD:-}"

Expand Down
1 change: 1 addition & 0 deletions plugin/scripts/hook_entry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ claude_smart_prepend_astral_bins
claude_smart_source_reflexio_env

PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "hook_entry.sh" "$@"

FAILURE_MARKER="$HOME/.claude-smart/install-failed"
STATE_DIR="$HOME/.claude-smart"
Expand Down
1 change: 1 addition & 0 deletions plugin/scripts/smart-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ claude_smart_prepend_node_bins
claude_smart_source_reflexio_env

PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "smart-install.sh" "$@"
REPO_ROOT="$(cd "$HERE/../.." && pwd)"

MARKER_DIR="$HOME/.claude-smart"
Expand Down
65 changes: 65 additions & 0 deletions tests/test_install_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,71 @@ def test_smart_install_installs_vendored_reflexio(tmp_path: Path) -> None:
assert f"-e {vendor}" in uv_log


def test_smart_install_redirects_reflexio_session_copies_to_stable_root(
tmp_path: Path,
) -> None:
"""Regression: Claude Code can expose CLAUDE_PLUGIN_ROOT as a per-session
~/.reflexio/Cu* copy. Installing .venv/node_modules there duplicates the
full runtime for every session; smart-install must instead use the stable
plugin root recorded in ~/.reflexio/plugin-root.
"""
session_root = tmp_path / ".reflexio" / "Cu123" / "plugin"
stable_root = tmp_path / ".claude" / "plugins" / "cache" / "reflexioai" / "claude-smart" / "0.2.42"
fake_bin = tmp_path / "bin"
for root in (session_root, stable_root):
scripts = root / "scripts"
scripts.mkdir(parents=True)
shutil.copy2(SMART_INSTALL, scripts / "smart-install.sh")
shutil.copy2(LIB, scripts / "_lib.sh")
shutil.copy2(
REPO_ROOT / "plugin" / "scripts" / "ensure-plugin-root.sh",
scripts / "ensure-plugin-root.sh",
)
(root / "pyproject.toml").write_text("[project]\nname='claude-smart'\n")
(root / "uv.lock").write_text("")

(tmp_path / ".reflexio").mkdir(exist_ok=True)
(tmp_path / ".reflexio" / "plugin-root").symlink_to(
stable_root,
target_is_directory=True,
)
fake_bin.mkdir()
uv = fake_bin / "uv"
uv.write_text(
"#!/bin/sh\n"
'printf "%s|%s\\n" "$PWD" "$*" >> "$HOME/uv.log"\n'
'if [ "$1" = "sync" ]; then\n'
' mkdir -p "$PWD/.venv/bin"\n'
' printf "#!/bin/sh\\nexit 0\\n" > "$PWD/.venv/bin/python"\n'
' chmod +x "$PWD/.venv/bin/python"\n'
"fi\n"
"exit 0\n"
)
uv.chmod(uv.stat().st_mode | stat.S_IXUSR)

env = _isolated_env(tmp_path)
env["PATH"] = f"{fake_bin}{os.pathsep}{env['PATH']}"
result = subprocess.run(
[
"/bin/bash",
"--noprofile",
"--norc",
str(session_root / "scripts" / "smart-install.sh"),
],
env=env,
text=True,
capture_output=True,
check=False,
)

assert result.returncode == 0, result.stderr
assert not (session_root / ".venv").exists()
assert (stable_root / ".venv" / "bin" / "python").exists()
uv_log = (tmp_path / "uv.log").read_text()
assert f"{stable_root}|sync --locked --python 3.12 --quiet" in uv_log
assert str(session_root) not in uv_log


def test_smart_install_repairs_reflexio_template_env_drift(tmp_path: Path) -> None:
plugin_root = tmp_path / "plugin"
scripts = plugin_root / "scripts"
Expand Down
Loading