@@ -73,6 +73,7 @@ NODE_PATH=""
7373APP_ROOT=" " # OUR_ROOT — the app folder, e.g. .../.ru-fork
7474APP_BIN=" " # the command / wrapper name
7575APP_DIR_NAME=" " # basename(APP_ROOT), e.g. .ru-fork — drives rc cleanup
76+ LEGACY_ROOT=" " # Linux: orphaned {home}/.<app> to delete after relocation
7677NODE_OK=" " # 1 if node is compatible (gates the old app's `stop`)
7778BIN_DIR=" " # APP_ROOT/bin
7879PREFLIGHT_STATUS=1 # 0 only when the environment is compatible
@@ -132,11 +133,15 @@ resolve_bundle_dir() {
132133
133134remove_source_dir () {
134135 [ -n " $SOURCE_DIR " ] || return 0
135- [ " $KEEP_SOURCE " = false ] || { info " Исходный каталог сохранён: $SOURCE_DIR " ; return 0; }
136- case " $SOURCE_DIR " in
137- /|/home|/Users|" $HOME " ) warn " Отказ удалять: $SOURCE_DIR " ; return 0 ;;
136+ # Capture + clear up front so this is idempotent: the success path and the
137+ # on_exit trap (and the failure dies) all call it, but it acts exactly once.
138+ local dir=" $SOURCE_DIR "
139+ SOURCE_DIR=" "
140+ [ " $KEEP_SOURCE " = false ] || { info " Исходный каталог сохранён: $dir " ; return 0; }
141+ case " $dir " in
142+ /|/home|/Users|" $HOME " ) warn " Отказ удалять: $dir " ; return 0 ;;
138143 esac
139- rm -rf " $SOURCE_DIR " 2> /dev/null && info " Исходный каталог удалён: $SOURCE_DIR " || true
144+ rm -rf " $dir " 2> /dev/null && info " Исходный каталог удалён: $dir " || true
140145}
141146
142147# ---------------------------------------------------------------------------
@@ -161,11 +166,17 @@ run_preflight() {
161166 PREFLIGHT_STATUS=0
162167 out=$( " $NODE_PATH " " $script " ) || PREFLIGHT_STATUS=$?
163168
164- APP_ROOT=$( printf ' %s\n' " $out " | grep ' ^OUR_ROOT=' | head -n 1 | cut -d' =' -f2-)
165- APP_BIN=$( printf ' %s\n' " $out " | grep ' ^APP_BIN=' | head -n 1 | cut -d' =' -f2-)
166- NODE_OK=$( printf ' %s\n' " $out " | grep ' ^NODE_OK=' | head -n 1 | cut -d' =' -f2-)
169+ # `|| true` on every extraction: a no-match grep (the preflight failed and
170+ # emitted no keys, or LEGACY_ROOT is absent without a relocation) must not trip
171+ # set -e/pipefail. Empty values are fine — the empty-APP_ROOT check below then
172+ # reports a clean error and rolls back.
173+ APP_ROOT=$( printf ' %s\n' " $out " | grep ' ^OUR_ROOT=' | head -n 1 | cut -d' =' -f2- || true)
174+ APP_BIN=$( printf ' %s\n' " $out " | grep ' ^APP_BIN=' | head -n 1 | cut -d' =' -f2- || true)
175+ NODE_OK=$( printf ' %s\n' " $out " | grep ' ^NODE_OK=' | head -n 1 | cut -d' =' -f2- || true)
176+ LEGACY_ROOT=$( printf ' %s\n' " $out " | grep ' ^LEGACY_ROOT=' | head -n 1 | cut -d' =' -f2- || true)
167177
168178 APP_ROOT=" ${APP_ROOT// \\ // } "
179+ LEGACY_ROOT=" ${LEGACY_ROOT// \\ // } "
169180 [ -n " $INSTALL_DIR " ] && APP_ROOT=" ${INSTALL_DIR// \\ // } "
170181 [ -n " $APP_ROOT " ] || { remove_source_dir; die " preflight: путь установки не получен." ; }
171182
@@ -218,13 +229,22 @@ clean_rc() {
218229 done < <( rc_files)
219230}
220231
221- # Stop the old app via node on its installed cli.js — only when node is
222- # compatible (it can't run otherwise). No-op if there is no old install.
223- stop_old () {
232+ # Stop a running daemon via node on the cli.js in bin dir $1 — only when node is
233+ # compatible (it can't run otherwise). No-op if that cli.js is absent.
234+ stop_daemon_at () {
235+ local bin=" $1 "
224236 [ " $NODE_OK " = " 1" ] || return 0
225- [ -f " $BIN_DIR /cli.js" ] || return 0
226- info " Остановка приложения..."
227- " $NODE_PATH " $NODE_FLAGS " $BIN_DIR /cli.js" stop > /dev/null 2>&1 || true
237+ [ -n " $bin " ] && [ -f " $bin /cli.js" ] || return 0
238+ info " Остановка приложения: $bin "
239+ " $NODE_PATH " $NODE_FLAGS " $bin /cli.js" stop > /dev/null 2>&1 || true
240+ }
241+
242+ # Stop both the new install and (after a relocation) the old home install.
243+ # (uses `if`, not `&&`, so an empty LEGACY_ROOT can't make the function return
244+ # non-zero and trip set -e in the caller.)
245+ stop_old () {
246+ stop_daemon_at " $BIN_DIR "
247+ if [ -n " ${LEGACY_ROOT:- } " ]; then stop_daemon_at " $LEGACY_ROOT /bin" ; fi
228248}
229249
230250remove_bin () {
@@ -239,6 +259,25 @@ remove_bin() {
239259 rm -rf " $BIN_DIR "
240260}
241261
262+ # Remove the orphaned home install after the resolver relocated OUR_ROOT to the
263+ # preferred dir (LEGACY_ROOT, Linux only). On install this runs only AFTER verify
264+ # (so a failed relocation never wipes a working old install); do_uninstall calls
265+ # it too. Guarded: must exist, end in the app dir name, not a dangerous path.
266+ remove_legacy_root () {
267+ [ -n " ${LEGACY_ROOT:- } " ] && [ -d " $LEGACY_ROOT " ] || return 0
268+ # Without the app dir name the */<appdir> guard below can't be trusted — skip.
269+ [ -n " $APP_DIR_NAME " ] || { warn " Отказ удалять: имя каталога приложения не определено" ; return 0; }
270+ case " $LEGACY_ROOT " in
271+ /|/home|/Users|" $HOME " ) warn " Отказ удалять: $LEGACY_ROOT " ; return 0 ;;
272+ esac
273+ case " $LEGACY_ROOT " in
274+ * /" $APP_DIR_NAME " ) ;;
275+ * ) warn " Отказ удалять подозрительный LEGACY_ROOT: $LEGACY_ROOT " ; return 0 ;;
276+ esac
277+ info " Удаление старой установки в домашнем каталоге: $LEGACY_ROOT "
278+ rm -rf " $LEGACY_ROOT " 2> /dev/null || true
279+ }
280+
242281# Full teardown — used by the crash rollback (no stop; the new install never
243282# fully started).
244283tear_down () {
@@ -322,13 +361,31 @@ WRAPPER
322361 chmod +x " $BIN_DIR /$APP_BIN "
323362}
324363
364+ # Convert a "C:/path" (drive-letter, forward slashes) to MSYS "/c/path" so it can
365+ # live in Git Bash's colon-separated PATH — the drive colon would otherwise split
366+ # the entry into "C" + "/path". Non-Windows paths pass straight through.
367+ to_msys_path () {
368+ local p=" $1 "
369+ if [[ " $p " =~ ^([A-Za-z]):/(.* )$ ]]; then
370+ # tr (not bash-4 ${x,,}) so this also works on the bash 3.2 macOS ships.
371+ local drive; drive=$( printf ' %s' " ${BASH_REMATCH[1]} " | tr ' A-Z' ' a-z' )
372+ printf ' /%s/%s' " $drive " " ${BASH_REMATCH[2]} "
373+ else
374+ printf ' %s' " $p "
375+ fi
376+ }
377+
325378# Append a fresh PATH line. clean_rc already removed any prior ones, so it can
326379# never duplicate.
327380add_path () {
328- local line=" export PATH=\" ${BIN_DIR} :\$ PATH\" " rc added=false
381+ # Git Bash PATH is colon-separated, so a Windows "C:/..." entry splits at the
382+ # drive colon. Use MSYS form "/c/..." on Windows; pass-through elsewhere.
383+ local bin_for_path=" $BIN_DIR "
384+ [ " $OS " = " windows" ] && bin_for_path=" $( to_msys_path " $BIN_DIR " ) "
385+ local line=" export PATH=\" ${bin_for_path} :\$ PATH\" " rc added=false
329386 while IFS= read -r rc; do
330387 [ -f " $rc " ] || continue
331- # A read-only rc (e.g. corp- managed .bash_profile) must NOT abort the install
388+ # A read-only rc (e.g. a managed/locked .bash_profile) must NOT abort the install
332389 # under `set -e`: catch the failed redirect, warn, and try the next file.
333390 if printf ' %s\n' " $line " >> " $rc " 2> /dev/null; then
334391 added=true
@@ -338,7 +395,7 @@ add_path() {
338395 fi
339396 done < <( rc_files)
340397 [ " $added " = true ] || die " Не удалось записать PATH ни в один файл оболочки. Добавьте вручную: $line "
341- export PATH=" $BIN_DIR :$PATH "
398+ export PATH=" $bin_for_path :$PATH "
342399}
343400
344401verify () {
@@ -351,6 +408,7 @@ do_uninstall() {
351408 run_preflight
352409 stop_old
353410 remove_bin
411+ remove_legacy_root
354412 clean_rc
355413 info " Удалено."
356414}
@@ -363,9 +421,14 @@ do_uninstall() {
363421on_exit () {
364422 local status=$?
365423 [ -n " ${TEMP_DIR:- } " ] && rm -rf " $TEMP_DIR " 2> /dev/null || true
424+ # The cloned installer dir is ALWAYS removed on exit (success or fail); only
425+ # --keep-source spares it (checked inside remove_source_dir). Closes the gap
426+ # where early failures (before BIN_DIR was set) left the clone behind.
427+ remove_source_dir
428+ # The freshly-laid bin is torn down only on a real failure (needs BIN_DIR for
429+ # its app-dir-name safety guard); a successful install keeps it.
366430 if [ " $status " -ne 0 ] && [ -n " ${BIN_DIR:- } " ]; then
367431 tear_down
368- remove_source_dir
369432 fi
370433 return " $status "
371434}
@@ -501,6 +564,7 @@ main() {
501564 create_wrapper
502565 add_path
503566 verify
567+ remove_legacy_root
504568 remove_source_dir
505569 # Banner to stderr so it lands in order with the rest of the output.
506570 print_summary >&2
0 commit comments