Skip to content

feat(install.sh): uninstall + rc-file hygiene (#741)#63

Draft
LukasWodka wants to merge 1 commit into
developfrom
fix/install-journey-741-rc-hygiene
Draft

feat(install.sh): uninstall + rc-file hygiene (#741)#63
LukasWodka wants to merge 1 commit into
developfrom
fix/install-journey-741-rc-hygiene

Conversation

@LukasWodka

Copy link
Copy Markdown
Contributor

Closes #741.

Stacks on #61 (branched off fix/install-path-persist). The diff below includes #61's commit (fix(install.sh): persist PATH to the shell rc) until #61 merges — that's expected for a stacked PR. Rebase onto develop after #61 lands and the diff collapses to just this change. Review the second commit (feat(install.sh): add --uninstall + harden rc-file mutation) for the #741 work.

Summary

#61 made the installer mutate the user's shell rc — it appends a marked PATH block (# Added by the tracebloc CLI installer + an export PATH=… / fish_add_path line) on the unprivileged ~/.local/bin fallback. That was the right fix, but mutating dotfiles creates obligations the installer didn't yet meet. This PR closes them: an uninstall path, and safety around managed/symlinked/read-only rc files.

What changed (scripts/install.sh)

  • --uninstall flag — removes the tracebloc binary from the prefix (and the ~/.local/bin fallback, so it cleans up regardless of which path install took) and strips only our marked block from the rc, restoring it byte-identical. Takes no network.
    • Portable editing — temp file + mv + awk (the repo's existing sync-schema.sh convention). No sed -i, whose -i semantics differ between GNU and BSD/macOS.
    • Removes the leading blank-line separator too. fix(install.sh): persist PATH to the shell rc (parity with install.ps1) #61's append writes \n + marker + path line, so the block is effectively three lines. Stripping only the two visible lines orphans a blank line — the round-trip bats test caught this. The awk uses one-line lookbehind to drop a single preceding blank separator (only if it is blank — real content is kept).
    • Multi-rc scan — uninstall checks every candidate rc (.zshrc, .bashrc, .bash_profile, fish config, .profile), so it finds the block even if the user installed under a different shell or has since switched shells.
    • Idempotent — running it on an already-clean system is a no-op that still exits 0.
  • Hardened the append path (the part fix(install.sh): persist PATH to the shell rc (parity with install.ps1) #61 added):
    • Symlinked / read-only / managed rc (chezmoi, dotfiles repo, Nix home-manager) is detected — the installer advises the user with the exact line to add instead of writing through the link or failing silently. Non-fatal.
    • $SHELL unset / exotic (csh, tcsh, nu) now falls back to advice rather than guessing ~/.profile (which a fresh non-login bash never reads).
  • Refactor — rc routing (rc_for_shell), the PATH line (path_line_for_shell), the managed-rc probe (rc_is_managed), append (append_path_to_rc), and strip (strip_rc_block) are now shared, named functions, and the marker string is a single RC_MARKER constant. Install and uninstall cannot drift on the marker or the target file. Existing install idempotency (fix(install.sh): persist PATH to the shell rc (parity with install.ps1) #61's "skip if rc already references the dir") is preserved.

Why

Per #741: mutating dotfiles without an uninstall leaves the line orphaned; and an append that writes through a symlink or a read-only/managed rc can fail silently or pollute version control. Both are addressed. The original report behind #61 came from a field deployment on Ubuntu 24.04.

Test plan

New scripts/tests/install.bats (bats-core) — 17 tests, all green:

  • append → --uninstall round-trips the rc byte-identical (with trailing newline, with a trailing blank line, and for an empty file). The one documented non-identical case — an rc whose last line lacks a trailing newline gains one (POSIX-correct, harmless) — is asserted explicitly.
  • read-only rc → advises, file untouched.
  • symlinked rc → advises on both append and strip, link target untouched, symlink not clobbered.
  • unset / exotic $SHELL → generic advice, no wrong-file write; rc_for_shell returns non-zero; known shells route correctly.
  • idempotency — append x3 → one block; strip x2 → clean, exit 0; strip on a never-touched rc leaves it identical.
  • end-to-end --uninstall via the real script — removes binary + block, clean no-op on a clean system, finds the block in a non-routed rc.

The suite sources install.sh behind a TRACEBLOC_INSTALL_SH_SOURCE_ONLY gate so the pure rc functions run without any download; the end-to-end tests execute the real script (uninstall takes no network).

Verified locally / Needs CI

Verified locally:

  • bash -n scripts/install.sh — OK
  • sh -n scripts/install.sh — OK (POSIX /bin/sh, no bashisms, matching the script's contract)
  • shellcheck -s sh scripts/install.sh — clean (shellcheck 0.11.0)
  • bats scripts/tests/install.bats — 17/17

Needs CI: build.yml only exercises the Go code — there is no shell-lint or bats step today, so neither #61's append nor this change is covered by CI. A small job (shellcheck + bats scripts/tests/) would guard the installer going forward. Not added here to avoid bundling CI infra into a behavior PR — happy to follow up if wanted.

Notes

  • POSIX /bin/sh throughout (the script is piped via curl … | sh on distros that may lack bash).
  • install.ps1 (Windows) already persists PATH and is out of scope; only install.sh mutates a Unix rc, so only it needs the uninstall/hygiene work.
  • bats is required to run the suite (brew install bats-core / apt install bats).

@LukasWodka

Copy link
Copy Markdown
Contributor Author

👋 Heads-up — Code review queue is at 17 / 8

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

cli#61 made the installer mutate the user's shell rc (appends a marked
PATH block on the ~/.local/bin fallback). That created hygiene
obligations the installer didn't meet — this closes them.

- --uninstall: removes the tracebloc binary from the prefix (and the
  ~/.local/bin fallback) and strips ONLY our marked block from the rc,
  restoring it byte-identical. Portable editing (temp file + mv + awk;
  no sed -i, whose semantics differ GNU vs BSD/macOS). Idempotent.
  Scans all candidate rc files so it finds the block even if the user
  installed under a different shell.
- Harden the append path: a symlinked / read-only / managed rc (chezmoi,
  dotfiles repo, Nix home-manager) is detected and the user is ADVISED
  with the exact line to add, instead of writing through the link or
  failing. $SHELL unset / exotic (csh, nu) now falls back to advice
  rather than guessing ~/.profile (which a non-login bash never reads).
- Refactor the rc routing + path-line + append logic into shared,
  testable functions so install and uninstall can never drift on the
  marker string or target file.
- Tests: scripts/tests/install.bats (new) — round-trip byte-identity,
  read-only/symlink advice, re-run idempotency, multi-rc uninstall.

Verified locally: bash -n, sh -n, shellcheck -s sh (clean), bats 17/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka LukasWodka force-pushed the fix/install-journey-741-rc-hygiene branch from 8e1b8ee to 284439e Compare June 8, 2026 18:17
@LukasWodka

Copy link
Copy Markdown
Contributor Author

Rebased against current develop (2026-06-08).

What changed during rebase:

Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants