Skip to content

Commit 8e0b3ea

Browse files
authored
Merge pull request #272 from shelltime/feat/homebrew-path-support
feat(cli): support Homebrew binary paths for daemon and hooks
2 parents 915c2ad + 519ea35 commit 8e0b3ea

20 files changed

Lines changed: 313 additions & 49 deletions

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ The Go module path is `github.com/malamtime/cli`. That naming mismatch is intent
99

1010
## Install
1111

12+
### Homebrew (macOS and Linux)
13+
14+
```bash
15+
brew install shelltime/tap/shelltime
16+
```
17+
18+
### curl installer
19+
1220
```bash
1321
curl -sSL https://shelltime.xyz/i | bash
1422
```

commands/daemon.install.go

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,32 @@ func commandDaemonInstall(c *cli.Context) error {
2929
baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
3030
username := currentUser.Username
3131

32-
installer, err := model.NewDaemonInstaller(baseFolder, username)
33-
if err != nil {
34-
return err
35-
}
36-
37-
installer.CheckAndStopExistingService()
38-
39-
// check latest file exist or not
40-
if _, err := os.Stat(filepath.Join(baseFolder, "bin/shelltime-daemon.bak")); err == nil {
32+
// Handle .bak upgrade for curl-installer users
33+
bakPath := filepath.Join(baseFolder, "bin/shelltime-daemon.bak")
34+
if _, err := os.Stat(bakPath); err == nil {
4135
color.Yellow.Println("🔄 Found latest daemon file, restoring...")
42-
// try to remove old file
4336
_ = os.Remove(filepath.Join(baseFolder, "bin/shelltime-daemon"))
44-
// rename .bak to original
45-
if err := os.Rename(
46-
filepath.Join(baseFolder, "bin/shelltime-daemon.bak"),
47-
filepath.Join(baseFolder, "bin/shelltime-daemon"),
48-
); err != nil {
37+
if err := os.Rename(bakPath, filepath.Join(baseFolder, "bin/shelltime-daemon")); err != nil {
4938
return fmt.Errorf("failed to restore latest daemon: %w", err)
5039
}
5140
}
5241

53-
// check shelltime-daemon
54-
if _, err := os.Stat(filepath.Join(baseFolder, "bin/shelltime-daemon")); err != nil {
55-
color.Yellow.Println("⚠️ shelltime-daemon not found, please reinstall the CLI first:")
56-
color.Yellow.Println("curl -sSL https://raw.githubusercontent.com/malamtime/installation/master/install.bash | bash")
42+
// Resolve daemon binary (curl-installer, Homebrew, or PATH)
43+
daemonBinPath, err := model.ResolveDaemonBinaryPath()
44+
if err != nil {
45+
color.Yellow.Println("⚠️ shelltime-daemon not found.")
46+
color.Yellow.Println("Install via Homebrew: brew install shelltime/tap/shelltime")
47+
color.Yellow.Println("Or via curl installer: curl -sSL https://shelltime.xyz/i | bash")
5748
return nil
5849
}
50+
color.Green.Printf("✅ Found daemon binary at: %s\n", daemonBinPath)
51+
52+
installer, err := model.NewDaemonInstaller(baseFolder, username, daemonBinPath)
53+
if err != nil {
54+
return err
55+
}
56+
57+
installer.CheckAndStopExistingService()
5958

6059
// User-level installation - no system-wide symlink needed
6160
color.Yellow.Println("🔍 Setting up user-level daemon installation...")

commands/daemon.status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func commandDaemonStatus(c *cli.Context) error {
5353
}
5454

5555
// Check 3: Service manager status
56-
installer, installerErr := model.NewDaemonInstaller("", "")
56+
installer, installerErr := model.NewDaemonInstaller("", "", "")
5757
if installerErr == nil {
5858
if err := installer.Check(); err == nil {
5959
printSuccess("Service is registered and running")

commands/daemon.uninstall.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func commandDaemonUninstall(c *cli.Context) error {
2828
baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
2929
username := currentUser.Username
3030

31-
installer, err := model.NewDaemonInstaller(baseFolder, username)
31+
installer, err := model.NewDaemonInstaller(baseFolder, username, "")
3232
if err != nil {
3333
return err
3434
}

commands/doctor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func commandDoctor(c *cli.Context) error {
9191

9292
// 5. Check daemon process
9393
printSectionHeader("Daemon Process")
94-
daemonInstaller, err := model.NewDaemonInstaller("", "")
94+
daemonInstaller, err := model.NewDaemonInstaller("", "", "")
9595
if err != nil {
9696
printError(fmt.Sprintf("Error checking daemon installer: %v", err))
9797
return err

commands/hooks.install.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67

78
"github.com/gookit/color"
89
"github.com/malamtime/cli/model"
@@ -17,9 +18,12 @@ var HooksInstallCommand = &cli.Command{
1718

1819
func commandHooksInstall(c *cli.Context) error {
1920
binFolder := os.ExpandEnv(fmt.Sprintf("$HOME/%s/bin", model.COMMAND_BASE_STORAGE_FOLDER))
20-
if _, err := os.Stat(binFolder); os.IsNotExist(err) {
21-
color.Red.Println("📁 cannot find bin folder at", binFolder)
22-
color.Red.Println("Please run 'curl -sSL https://raw.githubusercontent.com/malamtime/installation/master/install.bash | bash' first")
21+
_, binFolderErr := os.Stat(binFolder)
22+
_, lookPathErr := exec.LookPath("shelltime")
23+
if os.IsNotExist(binFolderErr) && lookPathErr != nil {
24+
color.Red.Println("📁 shelltime binary not found.")
25+
color.Red.Println("Install via Homebrew: brew install shelltime/tap/shelltime")
26+
color.Red.Println("Or via curl installer: curl -sSL https://shelltime.xyz/i | bash")
2327
return nil
2428
}
2529

model/daemon-installer.darwin.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@ var daemonMacServiceDesc []byte
1818

1919
// MacDaemonInstaller implements DaemonInstaller for macOS systems
2020
type MacDaemonInstaller struct {
21-
baseFolder string
22-
serviceName string
23-
user string
21+
baseFolder string
22+
serviceName string
23+
user string
24+
daemonBinPath string
2425
}
2526

26-
func NewMacDaemonInstaller(baseFolder, user string) *MacDaemonInstaller {
27+
func NewMacDaemonInstaller(baseFolder, user, daemonBinPath string) *MacDaemonInstaller {
2728
return &MacDaemonInstaller{
28-
baseFolder: baseFolder,
29-
user: user,
30-
serviceName: "xyz.shelltime.daemon",
29+
baseFolder: baseFolder,
30+
user: user,
31+
serviceName: "xyz.shelltime.daemon",
32+
daemonBinPath: daemonBinPath,
3133
}
3234
}
3335

@@ -163,8 +165,9 @@ func (m *MacDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.Bu
163165
return
164166
}
165167
err = tmpl.Execute(&buf, map[string]string{
166-
"UserName": username,
167-
"BaseFolder": m.baseFolder,
168+
"UserName": username,
169+
"BaseFolder": m.baseFolder,
170+
"DaemonBinPath": m.daemonBinPath,
168171
})
169172
return
170173
}

model/daemon-installer.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ type DaemonInstaller interface {
1818
}
1919

2020
// Factory function to create appropriate installer based on OS
21-
func NewDaemonInstaller(baseFolder, username string) (DaemonInstaller, error) {
21+
func NewDaemonInstaller(baseFolder, username, daemonBinPath string) (DaemonInstaller, error) {
2222
switch runtime.GOOS {
2323
case "linux":
24-
return NewLinuxDaemonInstaller(baseFolder, username), nil
24+
return NewLinuxDaemonInstaller(baseFolder, username, daemonBinPath), nil
2525
case "darwin":
26-
return NewMacDaemonInstaller(baseFolder, username), nil
26+
return NewMacDaemonInstaller(baseFolder, username, daemonBinPath), nil
2727
default:
2828
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
2929
}

model/daemon-installer.linux.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ var daemonLinuxServiceDesc []byte
1919

2020
// LinuxDaemonInstaller implements DaemonInstaller for Linux systems
2121
type LinuxDaemonInstaller struct {
22-
baseFolder string
23-
user string
22+
baseFolder string
23+
user string
24+
daemonBinPath string
2425
}
2526

26-
func NewLinuxDaemonInstaller(baseFolder, user string) *LinuxDaemonInstaller {
27-
return &LinuxDaemonInstaller{baseFolder: baseFolder, user: user}
27+
func NewLinuxDaemonInstaller(baseFolder, user, daemonBinPath string) *LinuxDaemonInstaller {
28+
return &LinuxDaemonInstaller{baseFolder: baseFolder, user: user, daemonBinPath: daemonBinPath}
2829
}
2930

3031
// getXDGRuntimeDir returns the XDG_RUNTIME_DIR path for the current user
@@ -221,8 +222,9 @@ func (l *LinuxDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.
221222
return
222223
}
223224
err = tmpl.Execute(&buf, map[string]string{
224-
"UserName": username,
225-
"BaseFolder": l.baseFolder,
225+
"UserName": username,
226+
"BaseFolder": l.baseFolder,
227+
"DaemonBinPath": l.daemonBinPath,
226228
})
227229
return
228230
}

model/hooks/bash.bash

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/bin/bash
2+
3+
# Source bash-preexec.sh if it exists
4+
if [ -f "bash-preexec.sh" ]; then
5+
source "bash-preexec.sh"
6+
else
7+
# Attempt to find bash-preexec.sh in the same directory as this script
8+
_SHELLTIME_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
if [ -f "$_SHELLTIME_HOOK_DIR/bash-preexec.sh" ]; then
10+
source "$_SHELLTIME_HOOK_DIR/bash-preexec.sh"
11+
else
12+
echo "Warning: bash-preexec.sh not found. Pre-execution hooks will not work."
13+
return 1
14+
fi
15+
fi
16+
17+
# Check if shelltime CLI exists
18+
if ! command -v shelltime &> /dev/null
19+
then
20+
echo "Warning: shelltime CLI not found. Please install it to enable time tracking."
21+
else
22+
shelltime gc
23+
fi
24+
25+
# Create a timestamp for the session when the shell starts
26+
SESSION_ID=$(date +%Y%m%d%H%M%S)
27+
LAST_COMMAND=""
28+
29+
# Function to be executed before each command
30+
preexec_invoke_cmd() {
31+
local CMD="$1"
32+
LAST_COMMAND="$CMD"
33+
# Check if command starts with exit, logout, or reboot
34+
if [[ "$CMD" =~ ^(exit|logout|reboot) ]]; then
35+
return
36+
fi
37+
38+
# Avoid tracking shelltime commands themselves to prevent loops
39+
if [[ "$CMD" =~ ^shelltime ]]; then
40+
return
41+
fi
42+
43+
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null
44+
}
45+
46+
# Function to be executed after each command (before prompt)
47+
precmd_invoke_cmd() {
48+
local LAST_RESULT=$?
49+
# BASH_COMMAND in precmd is the *previous* command
50+
local CMD="$LAST_COMMAND"
51+
# Check if command starts with exit, logout, or reboot
52+
if [[ "$CMD" =~ ^(exit|logout|reboot) ]]; then
53+
return
54+
fi
55+
56+
# Avoid tracking shelltime commands themselves to prevent loops
57+
if [[ "$CMD" =~ ^shelltime ]]; then
58+
return
59+
fi
60+
61+
# Ensure CMD is not empty or the precmd_invoke_cmd itself
62+
if [ -z "$CMD" ] || [ "$CMD" == "precmd_invoke_cmd" ]; then
63+
return
64+
fi
65+
66+
shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
67+
}
68+
69+
# Set the functions for bash-preexec
70+
preexec_functions+=(preexec_invoke_cmd)
71+
precmd_functions+=(precmd_invoke_cmd)

0 commit comments

Comments
 (0)