From e848813bfa63d7d7955ec23cc5b2ef71e45d57f0 Mon Sep 17 00:00:00 2001 From: Tom Riglar Date: Mon, 22 Jun 2026 08:41:30 +0100 Subject: [PATCH] fix(installer): manage PATH via sentinel block, self-heal legacy markers The Unix installer appended a bare "# dcd" marker plus an export line to the shell rc, and deduped only by grep-ing for the install dir. The marker and the dedup key were managed separately, so any time the export was removed or edited while the marker stayed (manual edits, relocations, partial cleanups) the next run orphaned or duplicated "# dcd" markers. Replace that with persist_path(): write a single sentinel-delimited block (# >>> dcd installer >>> ... # <<< dcd installer <<<) and, before writing, strip any prior block AND legacy bare "# dcd" markers + export lines, then trim trailing blank lines. Re-runs now converge to exactly one block and existing orphaned markers are cleaned up on the next install. The rc is rewritten in place (truncate + write) so its inode and permissions are preserved. Co-Authored-By: Claude Opus 4.8 --- install.sh | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index 81b2b96..ac94e43 100755 --- a/install.sh +++ b/install.sh @@ -59,6 +59,46 @@ rc_file() { esac } +# Persist INSTALL_DIR onto PATH in the given rc file, idempotently. Anything a +# previous run wrote is stripped first — the current sentinel-delimited block, +# and the legacy bare "# dcd" marker with its export line — so re-running the +# installer never accumulates duplicate markers or export lines. Trailing blank +# lines are trimmed so the rc doesn't grow a gap on each run. Returns non-zero +# (leaving the rc untouched) if it can't be written. +persist_path() { + rc="$1" + begin='# >>> dcd installer >>>' + end='# <<< dcd installer <<<' + + if [ ! -e "$rc" ]; then + : >> "$rc" 2>/dev/null || return 1 + fi + [ -w "$rc" ] || return 1 + + cleaned=$(mktemp "${TMPDIR:-/tmp}/dcd-rc-XXXXXX") || return 1 + # Drop our managed block (between the sentinels) and any legacy lines we may + # have written before, then trim trailing blanks. buf[] preserves order. + awk -v dir="$INSTALL_DIR" -v b="$begin" -v e="$end" ' + $0 == b { skip = 1; next } + $0 == e { skip = 0; next } + skip { next } + $0 == "# dcd" { next } + $0 == "export PATH=\"" dir ":$PATH\"" { next } + { buf[++n] = $0 } + END { + while (n > 0 && buf[n] ~ /^[[:space:]]*$/) n-- + for (i = 1; i <= n; i++) print buf[i] + } + ' "$rc" > "$cleaned" || { rm -f "$cleaned"; return 1; } + + printf '\n%s\n%s\n%s\n' "$begin" "$path_line" "$end" >> "$cleaned" \ + || { rm -f "$cleaned"; return 1; } + + # Truncate-and-rewrite so the rc keeps its original inode and permissions. + cat "$cleaned" > "$rc" || { rm -f "$cleaned"; return 1; } + rm -f "$cleaned" +} + main() { DOWNLOAD_BASE="${DCD_DOWNLOAD_BASE:-https://get.devicecloud.dev}" INSTALL_DIR="${DCD_INSTALL_DIR:-$HOME/.dcd/bin}" @@ -143,12 +183,7 @@ main() { if [ "$on_path" -eq 0 ]; then rc=$(rc_file) - if [ -f "$rc" ] && grep -Fq "$INSTALL_DIR" "$rc" 2>/dev/null; then - # rc already references the dir (e.g. a re-install); don't duplicate it. - info "" - info " $INSTALL_DIR is already configured in $rc." - info " Restart your shell, or run: $path_line" - elif printf '\n# dcd\n%s\n' "$path_line" >> "$rc" 2>/dev/null; then + if persist_path "$rc"; then info "" info " Added $INSTALL_DIR to your PATH in $rc." info " Restart your shell, or run: $path_line"