From ce4d0711d6c315e968b8c42d704fc538b807aae3 Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Fri, 12 Jun 2026 20:49:48 +0100 Subject: [PATCH 1/4] Fix PATH accumulation when switching between projects with .luca/tools The cleanup loop in update_path was nested inside the else branch, meaning it only ran when the current directory had no .luca/tools. When navigating from project A to project B (both with .luca/tools), B's entry was appended to PATH while A's was never removed. Navigating back to A found its entry already present and returned early, leaving B's stale entry ahead of A's. Move the cleanup unconditionally to the top of the function so stale .luca/tools entries are always evicted before the current directory's entry is (re-)added. The add step is now a simple append-if-missing that follows the cleaned PATH. --- shell_hook.sh | 67 ++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/shell_hook.sh b/shell_hook.sh index 9edade6..636b080 100755 --- a/shell_hook.sh +++ b/shell_hook.sh @@ -38,11 +38,41 @@ update_path() { local tool_bin_dir="$(pwd)/$TOOL_FOLDER/tools" local current_pwd="$(pwd)" - # Only proceed if the tool directory exists in the current directory + # Always clean up stale .luca/tools entries from PATH first. + # This must run even when the current directory has .luca/tools, so that + # switching between two projects doesn't accumulate both entries in PATH. + if [[ ":$PATH:" == *"/$TOOL_FOLDER/tools:"* ]]; then + local p + local new_path="" + local current_path="$PATH" + + while [ -n "$current_path" ]; do + p="${current_path%%:*}" + + if [ "$current_path" = "$p" ]; then + current_path="" + else + current_path="${current_path#*:}" + fi + + if [[ "$p" == *"/$TOOL_FOLDER/tools" ]]; then + # Keep only entries whose project root matches the current directory + local project_root="${p%/$TOOL_FOLDER/tools}" + if [[ "$current_pwd" == "$project_root" || "$current_pwd" == "$project_root/"* ]]; then + new_path="${new_path:+$new_path:}$p" + fi + else + new_path="${new_path:+$new_path:}$p" + fi + done + export PATH="$new_path" + fi + + # Add the current directory's tool_bin_dir if it exists and isn't already in PATH if [ -d "$tool_bin_dir" ]; then # Check if tool_bin_dir is already in PATH to avoid duplicates case ":$PATH:" in - *":$tool_bin_dir:"*) + *":$tool_bin_dir:"*) # Already in PATH, do nothing (idempotent behavior) return 0 ;; @@ -53,39 +83,6 @@ update_path() { # echo "$TOOL_NAME: Added $tool_bin_dir to PATH" ;; esac - else - # Current directory doesn't have .luca/tools - # Check if there are any .luca/tools entries in PATH that should be cleaned up - if [[ ":$PATH:" == *"/$TOOL_FOLDER/tools:"* ]]; then - local p - local new_path="" - local current_path="$PATH" - - while [ -n "$current_path" ]; do - p="${current_path%%:*}" - - if [ "$current_path" = "$p" ]; then - current_path="" - else - current_path="${current_path#*:}" - fi - - if [[ "$p" == *"/$TOOL_FOLDER/tools" ]]; then - # This is a tool directory entry - check if we should keep it - # Get the project root (parent of .luca/tools) - local project_root="${p%/$TOOL_FOLDER/tools}" - # Keep this entry if current directory is the project root or a subdirectory of it - if [[ "$current_pwd" == "$project_root" || "$current_pwd" == "$project_root/"* ]]; then - new_path="${new_path:+$new_path:}$p" - fi - # Otherwise, skip this entry (effectively removing it from PATH) - else - # Not a tool directory, keep it in PATH - new_path="${new_path:+$new_path:}$p" - fi - done - export PATH="$new_path" - fi fi } From 7e851b091811b6e6a656479c7e0cded1db420d9b Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Fri, 12 Jun 2026 20:54:33 +0100 Subject: [PATCH 2/4] Add tests for PATH accumulation fix when switching between projects Cover the two missing cases: navigating from project A to project B removes A's entry, and navigating back to A removes B's entry. --- tests/shell_hook.bats | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/shell_hook.bats b/tests/shell_hook.bats index 2c0bff6..9ffd39a 100644 --- a/tests/shell_hook.bats +++ b/tests/shell_hook.bats @@ -105,6 +105,43 @@ setup() { assert_output --partial "$project/.luca/tools" } +@test "update_path: replaces entry when switching between two projects with .luca/tools" { + local project_a="$BATS_TEST_TMPDIR/project_a" + local project_b="$BATS_TEST_TMPDIR/project_b" + mkdir -p "$project_a/.luca/tools" "$project_b/.luca/tools" + + run bash -c " + export HOME='$TEST_HOME' + export SHELL=/bin/bash + source '$REPO_ROOT/shell_hook.sh' + cd '$project_a' && update_path + cd '$project_b' && update_path + echo \"\$PATH\" + " + + assert_output --partial "$project_b/.luca/tools" + refute_output --partial "$project_a/.luca/tools" +} + +@test "update_path: switching back to first project removes second project entry" { + local project_a="$BATS_TEST_TMPDIR/project_a" + local project_b="$BATS_TEST_TMPDIR/project_b" + mkdir -p "$project_a/.luca/tools" "$project_b/.luca/tools" + + run bash -c " + export HOME='$TEST_HOME' + export SHELL=/bin/bash + source '$REPO_ROOT/shell_hook.sh' + cd '$project_a' && update_path + cd '$project_b' && update_path + cd '$project_a' && update_path + echo \"\$PATH\" + " + + assert_output --partial "$project_a/.luca/tools" + refute_output --partial "$project_b/.luca/tools" +} + @test "update_path: non-luca PATH entries are never removed" { local empty_dir="$BATS_TEST_TMPDIR/empty" mkdir -p "$empty_dir" From 1b33eb56f1dcd5caf817f3d6bc9f572f37609828 Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Fri, 12 Jun 2026 21:03:28 +0100 Subject: [PATCH 3/4] Walk up directory tree to find nearest .luca/tools ancestor update_path previously only checked $(pwd)/.luca/tools, so navigating directly into a project subdirectory from outside would never pick up the project's tools. Walk up from $PWD to / and use the first ancestor that contains .luca/tools. Simplify the PATH cleanup to match against the single resolved tool_bin_dir rather than checking project-root prefixes. Add a test covering direct entry into a subdirectory without first visiting the project root. --- shell_hook.sh | 30 +++++++++++++++++------------- tests/shell_hook.bats | 20 +++++++++++++++++++- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/shell_hook.sh b/shell_hook.sh index 636b080..1630449 100755 --- a/shell_hook.sh +++ b/shell_hook.sh @@ -34,13 +34,21 @@ TOOL_FOLDER=".luca" # It is designed to be idempotent and prevent PATH duplication. # When navigating to subdirectories of a project, it keeps the project's tool path in PATH. update_path() { - # Look for the active tools directory in the current location - local tool_bin_dir="$(pwd)/$TOOL_FOLDER/tools" - local current_pwd="$(pwd)" + # Walk up the directory tree to find the nearest ancestor with .luca/tools. + # This allows navigating directly into a project subdirectory and still + # picking up the project's tools without having to visit the root first. + local tool_bin_dir="" + local dir="$(pwd)" + while [ "$dir" != "/" ]; do + if [ -d "$dir/$TOOL_FOLDER/tools" ]; then + tool_bin_dir="$dir/$TOOL_FOLDER/tools" + break + fi + dir="$(dirname "$dir")" + done # Always clean up stale .luca/tools entries from PATH first. - # This must run even when the current directory has .luca/tools, so that - # switching between two projects doesn't accumulate both entries in PATH. + # Keep only the entry that matches the one we just resolved (if any). if [[ ":$PATH:" == *"/$TOOL_FOLDER/tools:"* ]]; then local p local new_path="" @@ -56,9 +64,8 @@ update_path() { fi if [[ "$p" == *"/$TOOL_FOLDER/tools" ]]; then - # Keep only entries whose project root matches the current directory - local project_root="${p%/$TOOL_FOLDER/tools}" - if [[ "$current_pwd" == "$project_root" || "$current_pwd" == "$project_root/"* ]]; then + # Keep only the entry for the currently active project + if [ "$p" = "$tool_bin_dir" ]; then new_path="${new_path:+$new_path:}$p" fi else @@ -68,16 +75,13 @@ update_path() { export PATH="$new_path" fi - # Add the current directory's tool_bin_dir if it exists and isn't already in PATH - if [ -d "$tool_bin_dir" ]; then - # Check if tool_bin_dir is already in PATH to avoid duplicates + # Add the resolved tool_bin_dir if it isn't already in PATH + if [ -n "$tool_bin_dir" ]; then case ":$PATH:" in *":$tool_bin_dir:"*) # Already in PATH, do nothing (idempotent behavior) - return 0 ;; *) - # Not in PATH, add it to the front for priority export PATH="$tool_bin_dir:$PATH" # Optional: Uncomment the line below for debugging # echo "$TOOL_NAME: Added $tool_bin_dir to PATH" diff --git a/tests/shell_hook.bats b/tests/shell_hook.bats index 9ffd39a..dfa01a8 100644 --- a/tests/shell_hook.bats +++ b/tests/shell_hook.bats @@ -86,7 +86,7 @@ setup() { refute_output --partial "$project_a/.luca/tools" } -@test "update_path: keeps .luca/tools entry when in subdirectory of project" { +@test "update_path: keeps .luca/tools entry when navigating into a project subdirectory" { local project="$BATS_TEST_TMPDIR/myproject" local subdir="$project/src/lib" mkdir -p "$project/.luca/tools" "$subdir" @@ -105,6 +105,24 @@ setup() { assert_output --partial "$project/.luca/tools" } +@test "update_path: adds .luca/tools when entering a project subdirectory directly" { + local project="$BATS_TEST_TMPDIR/myproject" + local subdir="$project/src/lib" + mkdir -p "$project/.luca/tools" "$subdir" + + run bash -c " + export HOME='$TEST_HOME' + export SHELL=/bin/bash + source '$REPO_ROOT/shell_hook.sh' + # Navigate directly into a subdir without visiting the project root first + cd '$subdir' + update_path + echo \"\$PATH\" + " + + assert_output --partial "$project/.luca/tools" +} + @test "update_path: replaces entry when switching between two projects with .luca/tools" { local project_a="$BATS_TEST_TMPDIR/project_a" local project_b="$BATS_TEST_TMPDIR/project_b" From 251369f0f6f9010cbbebc212cf3573a1da62138c Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Fri, 12 Jun 2026 21:24:47 +0100 Subject: [PATCH 4/4] Stop directory walk at HOME to avoid picking up global .luca/tools The upward search now stops before reaching \$HOME, so \$HOME/.luca/tools (the global Luca installation present on every user's machine) is never treated as a project-specific tools directory. Projects nested inside HOME are still found correctly. --- shell_hook.sh | 4 +++- tests/shell_hook.bats | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/shell_hook.sh b/shell_hook.sh index 1630449..8335573 100755 --- a/shell_hook.sh +++ b/shell_hook.sh @@ -37,9 +37,11 @@ update_path() { # Walk up the directory tree to find the nearest ancestor with .luca/tools. # This allows navigating directly into a project subdirectory and still # picking up the project's tools without having to visit the root first. + # Stop at $HOME (exclusive): $HOME/.luca/tools is the global Luca installation + # and must not be treated as a project-specific tools directory. local tool_bin_dir="" local dir="$(pwd)" - while [ "$dir" != "/" ]; do + while [ "$dir" != "/" ] && [ "$dir" != "$HOME" ]; do if [ -d "$dir/$TOOL_FOLDER/tools" ]; then tool_bin_dir="$dir/$TOOL_FOLDER/tools" break diff --git a/tests/shell_hook.bats b/tests/shell_hook.bats index dfa01a8..b2e6ba4 100644 --- a/tests/shell_hook.bats +++ b/tests/shell_hook.bats @@ -160,6 +160,42 @@ setup() { refute_output --partial "$project_b/.luca/tools" } +@test "update_path: does not add HOME/.luca/tools when in a non-project directory under HOME" { + # $HOME/.luca/tools is always present (global Luca installation) and must be ignored + mkdir -p "$TEST_HOME/.luca/tools" + local plain_dir="$TEST_HOME/documents" + mkdir -p "$plain_dir" + + run bash -c " + export HOME='$TEST_HOME' + export SHELL=/bin/bash + source '$REPO_ROOT/shell_hook.sh' + cd '$plain_dir' + update_path + echo \"\$PATH\" + " + + refute_output --partial "$TEST_HOME/.luca/tools" +} + +@test "update_path: project inside HOME is found but HOME/.luca/tools is not added" { + mkdir -p "$TEST_HOME/.luca/tools" + local project="$TEST_HOME/projects/myapp" + mkdir -p "$project/.luca/tools" "$project/src" + + run bash -c " + export HOME='$TEST_HOME' + export SHELL=/bin/bash + source '$REPO_ROOT/shell_hook.sh' + cd '$project/src' + update_path + echo \"\$PATH\" + " + + assert_output --partial "$project/.luca/tools" + refute_output --partial "$TEST_HOME/.luca/tools" +} + @test "update_path: non-luca PATH entries are never removed" { local empty_dir="$BATS_TEST_TMPDIR/empty" mkdir -p "$empty_dir"