From 3cf12bad0084b72fb5072bd654fe0e26d583aa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jorge=20Lopes?= Date: Tue, 28 Jul 2026 21:52:49 +0200 Subject: [PATCH] fix(watch): pin scheduled entry to an absolute, stable devflow launcher command -v devflow can return a RELATIVE path (./bin/devflow) when PATH has a relative entry, so devflow watch setup wrote a launchd plist / cron entry whose target resolved against the working directory (the source checkout) instead of the stable installed launcher - defeating the survives-auto-reinstall property. New _devflow_launcher resolves an absolute, stable target: the copy-install BINDIR launcher derived from DEVFLOW_ROOT, else an absolutized command -v result. _watch_setup uses it for the launchd ProgramArguments/PATH and the cron entry. watch.bats 34/34. Follow-up to #84/#87. --- lib/watch.sh | 19 ++++++++++++++++++- tests/unit/watch.bats | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/watch.sh b/lib/watch.sh index ce5f8b3..aedb1c0 100644 --- a/lib/watch.sh +++ b/lib/watch.sh @@ -549,6 +549,23 @@ _watch_run() { # ── Setup / Remove ─────────────────────────────────────────────────────────── +# Absolute, stable path to the installed devflow launcher for a scheduled entry. +# `command -v devflow` can return a RELATIVE path (e.g. ./bin/devflow when PATH has a +# relative entry), and a launchd/cron job needs an ABSOLUTE target that survives +# auto-reinstall. Prefer the copy-install BINDIR launcher derived from DEVFLOW_ROOT +# (/share/devflow -> /bin/devflow); otherwise absolutize command -v. +_devflow_launcher() { + if [[ "${DEVFLOW_ROOT:-}" == */share/devflow && -x "${DEVFLOW_ROOT%/share/devflow}/bin/devflow" ]]; then + echo "${DEVFLOW_ROOT%/share/devflow}/bin/devflow"; return + fi + local dfb; dfb="$(command -v devflow 2>/dev/null || echo "${DEVFLOW_ROOT:-$(devflow_root)}/bin/devflow")" + case "$dfb" in + /*) echo "$dfb" ;; + *) local d; d="$(cd "$(dirname "$dfb")" 2>/dev/null && pwd)" + if [[ -n "$d" ]]; then echo "${d}/$(basename "$dfb")"; else echo "${HOME}/.local/bin/devflow"; fi ;; + esac +} + # ── native scheduler backend: macOS launchd ────────────────────────────────── # A per-project LaunchAgent, not cron: macOS user-cron is unreliable (does not fire # after sleep). StartCalendarInterval (not StartInterval) is used deliberately - @@ -645,7 +662,7 @@ _watch_setup() { project_dir="$(cd "$project_dir" && pwd)" # resolve to absolute local devflow_bin - devflow_bin="$(command -v devflow 2>/dev/null || echo "${DEVFLOW_ROOT:-$(devflow_root)}/bin/devflow")" + devflow_bin="$(_devflow_launcher)" # absolute + stable (never a relative ./bin/devflow) section "Sensitive File Watchdog Setup" echo "" diff --git a/tests/unit/watch.bats b/tests/unit/watch.bats index cce320b..ec6169b 100644 --- a/tests/unit/watch.bats +++ b/tests/unit/watch.bats @@ -404,3 +404,22 @@ EOF assert_output --partial "EnvironmentVariables" assert_output --partial "PATH/Users/x/.local/bin:" } + +# ── _devflow_launcher: absolute + stable scheduler target ────── + +@test "devflow_launcher derives the copy-install BINDIR launcher from DEVFLOW_ROOT" { + local pfx="${BATS_TEST_TMPDIR}/pref"; mkdir -p "$pfx/bin" "$pfx/share/devflow" + printf '#!/bin/bash\n' > "$pfx/bin/devflow"; chmod +x "$pfx/bin/devflow" + DEVFLOW_ROOT="$pfx/share/devflow" run _devflow_launcher + assert_success + assert_output "$pfx/bin/devflow" +} + +@test "devflow_launcher always returns an absolute path" { + # copy-install derivation (the common path) must be absolute + local pfx="${BATS_TEST_TMPDIR}/pref2"; mkdir -p "$pfx/bin" "$pfx/share/devflow" + printf '#!/bin/bash\n' > "$pfx/bin/devflow"; chmod +x "$pfx/bin/devflow" + DEVFLOW_ROOT="$pfx/share/devflow" run _devflow_launcher + assert_success + [[ "$output" == /* ]] || fail "not absolute: $output" +}