Skip to content

fix(daemon): prefer Homebrew binary over curl-installer location#277

Merged
AnnatarHe merged 1 commit into
mainfrom
fix/daemon-prefer-homebrew-binary
May 17, 2026
Merged

fix(daemon): prefer Homebrew binary over curl-installer location#277
AnnatarHe merged 1 commit into
mainfrom
fix/daemon-prefer-homebrew-binary

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

Summary

  • Reorder ResolveDaemonBinaryPath to prefer PATH/Homebrew over ~/.shelltime/bin/shelltime-daemon, so brew upgrade shelltime drives the running daemon instead of a stale curl-installer copy.
  • On shelltime daemon install, remove the stale ~/.shelltime/bin/shelltime-daemon when a system-managed binary is chosen (keeps shelltime.bak from the CLI upgrade flow intact).
  • Guard against PATH resolving back to the curl-installer location so step 3 stays the only branch that returns it.
  • Extract Homebrew search dirs into a package-level var so tests can isolate from the host environment.
  • Adds TestResolveDaemonBinaryPath covering five resolution cases.

Background

A user with both a curl-installed daemon (~/.shelltime/bin/shelltime-daemon, Apr 7) and a Homebrew install (/opt/homebrew/bin/shelltime-daemon, today) found the plist still pointed at the April binary. The old resolution order preferred ~/.shelltime/bin/ first.

Test plan

  • go test ./model/... -run TestResolveDaemonBinaryPath (5 subtests pass)
  • go test -timeout 3m ./model/... (no regressions)
  • go build ./... (whole module builds)
  • End-to-end on a machine that had both binaries: rebuilt CLI, ran shelltime daemon install — output shows Found daemon binary at: /opt/homebrew/bin/shelltime-daemon and Removed stale curl-installer daemon at .... The plist now contains /opt/homebrew/bin/shelltime-daemon and pgrep -lf shelltime-daemon shows the Homebrew-managed process running.
  • Re-verify on Linux/systemd (the resolution change is cross-platform; cleanup runs the same way).

🤖 Generated with Claude Code

Reorder ResolveDaemonBinaryPath to check PATH/Homebrew before
~/.shelltime/bin/shelltime-daemon, so that brew-managed installs
actually drive the running daemon. On install, remove the stale
curl-installer daemon when a system-managed binary is being used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

❌ Patch coverage is 58.82353% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
commands/daemon.install.go 0.00% 7 Missing ⚠️
Flag Coverage Δ
unittests 39.89% <58.82%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
model/path.go 96.55% <100.00%> (+25.86%) ⬆️
commands/daemon.install.go 0.00% <0.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80c00a16e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread model/path.go
// 2. Explicit Homebrew/Linuxbrew fallback paths, in case PATH was stripped.
for _, dir := range daemonHomebrewSearchPaths {
p := filepath.Join(dir, binaryName)
if info, err := os.Stat(p); err == nil && !info.IsDir() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-executable fallback binaries

ResolveDaemonBinaryPath now checks hard-coded Homebrew/Linuxbrew locations before the curl-installer fallback, but this branch only uses os.Stat and treats any non-directory file as valid. In environments where PATH is empty/stripped (the exact case this fallback is meant to handle), a non-executable file at /opt/homebrew/bin/shelltime-daemon (for example after permission drift or a partial upgrade) will be selected and written into the service config, causing daemon startup to fail even when a valid ~/.shelltime/bin/shelltime-daemon exists. This lookup should enforce executability before returning the path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the daemon binary resolution logic to prioritize system-managed installations (such as Homebrew or those on the system PATH) over the legacy curl-installer location. It also introduces a cleanup mechanism to remove stale curl-installer binaries and adds comprehensive unit tests for the resolution logic. The review feedback suggests improving path comparison by using os.SameFile instead of string equality to robustly handle symlinks and prevent the accidental deletion of active binaries.

Comment thread model/path.go
Comment on lines 143 to 147
if path, err := exec.LookPath(binaryName); err == nil {
return path, nil
if resolved, _ := filepath.Abs(path); resolved != curlPath {
return path, nil
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current check resolved != curlPath only performs a string comparison, which may fail to identify the curl-installer binary if symlinks are involved (e.g., if a path in PATH is a symlink to the .shelltime/bin location). Using os.SameFile is a more robust way to ensure that the 'system-managed' binary isn't actually the one we're trying to avoid in this step. Additionally, returning the absolute path ensures consistency across all resolution branches.

	if path, err := exec.LookPath(binaryName); err == nil {
		if absPath, err := filepath.Abs(path); err == nil {
			if info, err := os.Stat(absPath); err == nil {
				curlInfo, err := os.Stat(curlPath)
				if err != nil || !os.SameFile(info, curlInfo) {
					return absPath, nil
				}
			}
		}
	}

Comment on lines +56 to +64
if daemonBinPath != curlDaemonPath {
if info, statErr := os.Stat(curlDaemonPath); statErr == nil && !info.IsDir() {
if rmErr := os.Remove(curlDaemonPath); rmErr == nil {
color.Yellow.Printf("🧹 Removed stale curl-installer daemon at %s\n", curlDaemonPath)
} else {
color.Yellow.Printf("⚠️ Could not remove stale daemon at %s: %v\n", curlDaemonPath, rmErr)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Comparing daemonBinPath and curlDaemonPath via string equality might be insufficient if one is a symlink to the other. If daemonBinPath is a symlink pointing to curlDaemonPath, deleting the latter will break the installation as the binary will be missing. It is safer to use os.SameFile to verify they are distinct physical files before attempting removal.

Suggested change
if daemonBinPath != curlDaemonPath {
if info, statErr := os.Stat(curlDaemonPath); statErr == nil && !info.IsDir() {
if rmErr := os.Remove(curlDaemonPath); rmErr == nil {
color.Yellow.Printf("🧹 Removed stale curl-installer daemon at %s\n", curlDaemonPath)
} else {
color.Yellow.Printf("⚠️ Could not remove stale daemon at %s: %v\n", curlDaemonPath, rmErr)
}
}
}
curlDaemonPath := model.GetCurlInstallerDaemonPath()
if info, err := os.Stat(curlDaemonPath); err == nil && !info.IsDir() {
// Ensure we don't delete the binary if the resolved path is just a symlink to it
if binInfo, err := os.Stat(daemonBinPath); err == nil && !os.SameFile(info, binInfo) {
if rmErr := os.Remove(curlDaemonPath); rmErr == nil {
color.Yellow.Printf("🧹 Removed stale curl-installer daemon at %s\n", curlDaemonPath)
} else {
color.Yellow.Printf("⚠️ Could not remove stale daemon at %s: %v\n", curlDaemonPath, rmErr)
}
}
}

@AnnatarHe AnnatarHe merged commit 5351071 into main May 17, 2026
8 checks passed
@AnnatarHe AnnatarHe deleted the fix/daemon-prefer-homebrew-binary branch May 17, 2026 00:55
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.

1 participant