Skip to content

Commit aa0799e

Browse files
Install CLI to ~/.local/bin without sudo (#379)
* feat: install CLI to ~/.local/bin without sudo Drop the sudo-to-/usr/local/bin install path in favor of a userland install at ~/.local/bin. The installer creates the dir, drops the binary, and warns if ~/.local/bin isn't on PATH. README updated to match and to use the curl | bash form. * fix(upgrade): drop sudo prompt and warn about stale system binary Address PR review feedback: the install script writes to ~/.local/bin without sudo, so brev upgrade no longer gates on sudo and the prompt text reflects the new location. After a direct upgrade we also detect when the running binary lives in /usr/local/bin or /usr/bin (left over from a pre-~/.local/bin install) and tell the user to remove it so PATH resolution doesn't keep pinning them to the old version. --------- Co-authored-by: Pratik Patel <pratpatel@nvidia.com>
1 parent 8e255d8 commit aa0799e

5 files changed

Lines changed: 67 additions & 23 deletions

File tree

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ brew install brevdev/homebrew-brev/brev
1414
### Linux
1515

1616
```bash
17-
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/brevdev/brev-cli/main/bin/install-latest.sh)"
17+
curl -fsSL https://raw.githubusercontent.com/brevdev/brev-cli/main/bin/install-latest.sh | bash
18+
```
19+
20+
Installs to `~/.local/bin`. If it's not on your `PATH`, add this to your shell profile:
21+
22+
```bash
23+
export PATH="$HOME/.local/bin:$PATH"
1824
```
1925

2026
### Windows
@@ -27,10 +33,16 @@ Brev is supported on windows currently through the Windows Subsystem for Linux (
2733
- Virtualization enabled in your BIOS
2834
- Ubuntu >=22.04 installed from the Microsoft Store
2935

30-
Once you have WSL installed and configured, you can install Brev by running the following command in your terminal:
36+
Once WSL is set up, install Brev with:
3137

32-
```bash
33-
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/brevdev/brev-cli/main/bin/install-latest.sh)"
38+
```bash
39+
curl -fsSL https://raw.githubusercontent.com/brevdev/brev-cli/main/bin/install-latest.sh | bash
40+
```
41+
42+
Installs to `~/.local/bin`. If it's not on your `PATH`, add this to your shell profile:
43+
44+
```bash
45+
export PATH="$HOME/.local/bin:$PATH"
3446
```
3547

3648
### From conda-forge

bin/install-latest.sh

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,21 @@ trap 'rm -rf "${TMP_DIR}"' EXIT
3535
curl -sL "${DOWNLOAD_URL}" -o "${TMP_DIR}/brev.tar.gz"
3636
tar -xzf "${TMP_DIR}/brev.tar.gz" -C "${TMP_DIR}"
3737

38-
# Install the binary to system location
39-
sudo mv "${TMP_DIR}/brev" /usr/local/bin/brev
40-
sudo chmod +x /usr/local/bin/brev
38+
# Install the binary to the user's local bin
39+
INSTALL_DIR="${HOME}/.local/bin"
40+
mkdir -p "${INSTALL_DIR}"
41+
mv "${TMP_DIR}/brev" "${INSTALL_DIR}/brev"
42+
chmod +x "${INSTALL_DIR}/brev"
4143

42-
echo "Successfully installed brev CLI to /usr/local/bin/brev"
44+
echo "Successfully installed brev CLI to ${INSTALL_DIR}/brev"
45+
46+
case ":${PATH}:" in
47+
*":${INSTALL_DIR}:"*) ;;
48+
*)
49+
echo
50+
echo "Warning: ${INSTALL_DIR} is not in your PATH." >&2
51+
echo "Add it by appending the following line to your shell profile (e.g. ~/.bashrc, ~/.zshrc):" >&2
52+
echo " export PATH=\"\${HOME}/.local/bin:\${PATH}\"" >&2
53+
echo "Then restart your shell or run 'source' on the profile to pick up the change." >&2
54+
;;
55+
esac

pkg/cmd/upgrade/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (SystemUpgrader) UpgradeViaBrew(t *terminal.Terminal) error {
3333
return nil
3434
}
3535

36-
// UpgradeViaInstallScript runs the upstream install-latest.sh script via sudo.
36+
// UpgradeViaInstallScript runs the upstream install-latest.sh script.
3737
func (SystemUpgrader) UpgradeViaInstallScript(t *terminal.Terminal) error {
3838
t.Vprintf("Running: bash -c \"$(curl -fsSL %s)\"\n", installScriptURL)
3939
t.Vprint("")

pkg/cmd/upgrade/upgrade.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ package upgrade
44
import (
55
"fmt"
66
"os"
7+
"path/filepath"
78

89
"github.com/brevdev/brev-cli/pkg/cmd/agentskill"
910
"github.com/brevdev/brev-cli/pkg/cmd/register"
1011
"github.com/brevdev/brev-cli/pkg/cmd/version"
1112
"github.com/brevdev/brev-cli/pkg/store"
12-
"github.com/brevdev/brev-cli/pkg/sudo"
1313
"github.com/brevdev/brev-cli/pkg/terminal"
1414

1515
"github.com/spf13/cobra"
@@ -43,7 +43,6 @@ type upgradeDeps struct {
4343
detector Detector
4444
upgrader Upgrader
4545
confirmer terminal.Confirmer
46-
gater sudo.Gater
4746
skillInstaller SkillInstaller
4847
}
4948

@@ -52,7 +51,6 @@ func defaultUpgradeDeps() upgradeDeps {
5251
detector: SystemDetector{},
5352
upgrader: SystemUpgrader{},
5453
confirmer: register.TerminalPrompter{},
55-
gater: sudo.Default,
5654
skillInstaller: defaultSkillInstaller{},
5755
}
5856
}
@@ -146,11 +144,12 @@ func upgradeViaBrew(t *terminal.Terminal, deps upgradeDeps) (bool, error) {
146144

147145
func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) (bool, error) {
148146
t.Vprint("Detected install method: direct binary install")
149-
t.Vprint("This will download the latest release and install it to /usr/local/bin/brev")
147+
t.Vprint("This will download the latest release and install it to ~/.local/bin/brev.")
150148
t.Vprint("")
151149

152-
if err := deps.gater.Gate(t, deps.confirmer, "Upgrade", false); err != nil {
153-
return false, fmt.Errorf("sudo issue: %w", err)
150+
if !deps.confirmer.ConfirmYesNo("Proceed with upgrade?") {
151+
t.Vprint("Upgrade canceled.")
152+
return false, nil
154153
}
155154

156155
t.Vprint("")
@@ -160,5 +159,33 @@ func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) (bool, error) {
160159

161160
t.Vprint("")
162161
t.Vprint(t.Green("Upgrade complete."))
162+
163+
if stale := detectStaleSystemInstall(); stale != "" {
164+
t.Vprint("")
165+
t.Vprintf(" The new brev binary was installed at ~/.local/bin/brev,\n")
166+
t.Vprintf(" but an older binary still exists at %s.\n", stale)
167+
t.Vprint(" If that path appears earlier in your PATH, your shell will keep running the old version.")
168+
t.Vprintf(" Remove it with: sudo rm %s\n", stale)
169+
}
163170
return true, nil
164171
}
172+
173+
// detectStaleSystemInstall returns the path of the currently-running binary
174+
// when it lives in a system bin directory left over from a pre-~/.local/bin
175+
// install. Returns "" if the running binary is already in ~/.local/bin or in
176+
// any other non-system location.
177+
func detectStaleSystemInstall() string {
178+
exe, err := os.Executable()
179+
if err != nil {
180+
return ""
181+
}
182+
resolved, err := filepath.EvalSymlinks(exe)
183+
if err != nil {
184+
resolved = exe
185+
}
186+
switch filepath.Dir(resolved) {
187+
case "/usr/local/bin", "/usr/bin":
188+
return resolved
189+
}
190+
return ""
191+
}

pkg/cmd/upgrade/upgrade_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/brevdev/brev-cli/pkg/cmd/version"
88
"github.com/brevdev/brev-cli/pkg/store"
9-
"github.com/brevdev/brev-cli/pkg/sudo"
109
"github.com/brevdev/brev-cli/pkg/terminal"
1110
)
1211

@@ -65,7 +64,6 @@ func Test_runUpgrade_AlreadyUpToDate(t *testing.T) {
6564
detector: mockDetector{method: InstallMethodBrew},
6665
upgrader: upgrader,
6766
confirmer: mockConfirmer{confirm: true},
68-
gater: sudo.CachedGater{},
6967
skillInstaller: skill,
7068
}
7169

@@ -94,7 +92,6 @@ func Test_runUpgrade_BrewPath(t *testing.T) {
9492
detector: mockDetector{method: InstallMethodBrew},
9593
upgrader: upgrader,
9694
confirmer: mockConfirmer{confirm: true},
97-
gater: sudo.CachedGater{},
9895
skillInstaller: skill,
9996
}
10097

@@ -126,7 +123,6 @@ func Test_runUpgrade_DirectPath(t *testing.T) {
126123
detector: mockDetector{method: InstallMethodDirect},
127124
upgrader: upgrader,
128125
confirmer: mockConfirmer{confirm: true},
129-
gater: sudo.CachedGater{},
130126
skillInstaller: skill,
131127
}
132128

@@ -158,7 +154,6 @@ func Test_runUpgrade_UserCancels(t *testing.T) {
158154
detector: mockDetector{method: InstallMethodBrew},
159155
upgrader: upgrader,
160156
confirmer: mockConfirmer{confirm: false},
161-
gater: sudo.CachedGater{},
162157
skillInstaller: skill,
163158
}
164159

@@ -181,7 +176,6 @@ func Test_runUpgrade_VersionCheckFails(t *testing.T) {
181176
detector: mockDetector{method: InstallMethodBrew},
182177
upgrader: &mockUpgrader{},
183178
confirmer: mockConfirmer{confirm: true},
184-
gater: sudo.CachedGater{},
185179
skillInstaller: &mockSkillInstaller{},
186180
}
187181

@@ -204,7 +198,6 @@ func Test_runUpgrade_UpgraderFails(t *testing.T) {
204198
detector: mockDetector{method: InstallMethodBrew},
205199
upgrader: upgrader,
206200
confirmer: mockConfirmer{confirm: true},
207-
gater: sudo.CachedGater{},
208201
skillInstaller: skill,
209202
}
210203

@@ -230,7 +223,6 @@ func Test_runUpgrade_SkillInstallFailure_DoesNotFailUpgrade(t *testing.T) {
230223
detector: mockDetector{method: InstallMethodBrew},
231224
upgrader: upgrader,
232225
confirmer: mockConfirmer{confirm: true},
233-
gater: sudo.CachedGater{},
234226
skillInstaller: skill,
235227
}
236228

0 commit comments

Comments
 (0)