diff --git a/README.md b/README.md
index 6e5c42d..fb7b74c 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,14 @@ The Go module path is `github.com/malamtime/cli`. That naming mismatch is intent
## Install
+### Homebrew (macOS and Linux)
+
+```bash
+brew install shelltime/tap/shelltime
+```
+
+### curl installer
+
```bash
curl -sSL https://shelltime.xyz/i | bash
```
diff --git a/commands/daemon.install.go b/commands/daemon.install.go
index 43a04af..35a3150 100644
--- a/commands/daemon.install.go
+++ b/commands/daemon.install.go
@@ -29,33 +29,32 @@ func commandDaemonInstall(c *cli.Context) error {
baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
username := currentUser.Username
- installer, err := model.NewDaemonInstaller(baseFolder, username)
- if err != nil {
- return err
- }
-
- installer.CheckAndStopExistingService()
-
- // check latest file exist or not
- if _, err := os.Stat(filepath.Join(baseFolder, "bin/shelltime-daemon.bak")); err == nil {
+ // Handle .bak upgrade for curl-installer users
+ bakPath := filepath.Join(baseFolder, "bin/shelltime-daemon.bak")
+ if _, err := os.Stat(bakPath); err == nil {
color.Yellow.Println("🔄 Found latest daemon file, restoring...")
- // try to remove old file
_ = os.Remove(filepath.Join(baseFolder, "bin/shelltime-daemon"))
- // rename .bak to original
- if err := os.Rename(
- filepath.Join(baseFolder, "bin/shelltime-daemon.bak"),
- filepath.Join(baseFolder, "bin/shelltime-daemon"),
- ); err != nil {
+ if err := os.Rename(bakPath, filepath.Join(baseFolder, "bin/shelltime-daemon")); err != nil {
return fmt.Errorf("failed to restore latest daemon: %w", err)
}
}
- // check shelltime-daemon
- if _, err := os.Stat(filepath.Join(baseFolder, "bin/shelltime-daemon")); err != nil {
- color.Yellow.Println("⚠️ shelltime-daemon not found, please reinstall the CLI first:")
- color.Yellow.Println("curl -sSL https://raw.githubusercontent.com/malamtime/installation/master/install.bash | bash")
+ // Resolve daemon binary (curl-installer, Homebrew, or PATH)
+ daemonBinPath, err := model.ResolveDaemonBinaryPath()
+ if err != nil {
+ color.Yellow.Println("⚠️ shelltime-daemon not found.")
+ color.Yellow.Println("Install via Homebrew: brew install shelltime/tap/shelltime")
+ color.Yellow.Println("Or via curl installer: curl -sSL https://shelltime.xyz/i | bash")
return nil
}
+ color.Green.Printf("✅ Found daemon binary at: %s\n", daemonBinPath)
+
+ installer, err := model.NewDaemonInstaller(baseFolder, username, daemonBinPath)
+ if err != nil {
+ return err
+ }
+
+ installer.CheckAndStopExistingService()
// User-level installation - no system-wide symlink needed
color.Yellow.Println("🔍 Setting up user-level daemon installation...")
diff --git a/commands/daemon.status.go b/commands/daemon.status.go
index 9231017..e3572eb 100644
--- a/commands/daemon.status.go
+++ b/commands/daemon.status.go
@@ -53,7 +53,7 @@ func commandDaemonStatus(c *cli.Context) error {
}
// Check 3: Service manager status
- installer, installerErr := model.NewDaemonInstaller("", "")
+ installer, installerErr := model.NewDaemonInstaller("", "", "")
if installerErr == nil {
if err := installer.Check(); err == nil {
printSuccess("Service is registered and running")
diff --git a/commands/daemon.uninstall.go b/commands/daemon.uninstall.go
index 38caa45..21cb3a1 100644
--- a/commands/daemon.uninstall.go
+++ b/commands/daemon.uninstall.go
@@ -28,7 +28,7 @@ func commandDaemonUninstall(c *cli.Context) error {
baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
username := currentUser.Username
- installer, err := model.NewDaemonInstaller(baseFolder, username)
+ installer, err := model.NewDaemonInstaller(baseFolder, username, "")
if err != nil {
return err
}
diff --git a/commands/doctor.go b/commands/doctor.go
index 2efb275..617d336 100644
--- a/commands/doctor.go
+++ b/commands/doctor.go
@@ -91,7 +91,7 @@ func commandDoctor(c *cli.Context) error {
// 5. Check daemon process
printSectionHeader("Daemon Process")
- daemonInstaller, err := model.NewDaemonInstaller("", "")
+ daemonInstaller, err := model.NewDaemonInstaller("", "", "")
if err != nil {
printError(fmt.Sprintf("Error checking daemon installer: %v", err))
return err
diff --git a/commands/hooks.install.go b/commands/hooks.install.go
index 22dfea4..25441cb 100644
--- a/commands/hooks.install.go
+++ b/commands/hooks.install.go
@@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os"
+ "os/exec"
"github.com/gookit/color"
"github.com/malamtime/cli/model"
@@ -17,9 +18,12 @@ var HooksInstallCommand = &cli.Command{
func commandHooksInstall(c *cli.Context) error {
binFolder := os.ExpandEnv(fmt.Sprintf("$HOME/%s/bin", model.COMMAND_BASE_STORAGE_FOLDER))
- if _, err := os.Stat(binFolder); os.IsNotExist(err) {
- color.Red.Println("📁 cannot find bin folder at", binFolder)
- color.Red.Println("Please run 'curl -sSL https://raw.githubusercontent.com/malamtime/installation/master/install.bash | bash' first")
+ _, binFolderErr := os.Stat(binFolder)
+ _, lookPathErr := exec.LookPath("shelltime")
+ if os.IsNotExist(binFolderErr) && lookPathErr != nil {
+ color.Red.Println("📁 shelltime binary not found.")
+ color.Red.Println("Install via Homebrew: brew install shelltime/tap/shelltime")
+ color.Red.Println("Or via curl installer: curl -sSL https://shelltime.xyz/i | bash")
return nil
}
diff --git a/model/daemon-installer.darwin.go b/model/daemon-installer.darwin.go
index 206d253..94aa7ba 100644
--- a/model/daemon-installer.darwin.go
+++ b/model/daemon-installer.darwin.go
@@ -18,16 +18,18 @@ var daemonMacServiceDesc []byte
// MacDaemonInstaller implements DaemonInstaller for macOS systems
type MacDaemonInstaller struct {
- baseFolder string
- serviceName string
- user string
+ baseFolder string
+ serviceName string
+ user string
+ daemonBinPath string
}
-func NewMacDaemonInstaller(baseFolder, user string) *MacDaemonInstaller {
+func NewMacDaemonInstaller(baseFolder, user, daemonBinPath string) *MacDaemonInstaller {
return &MacDaemonInstaller{
- baseFolder: baseFolder,
- user: user,
- serviceName: "xyz.shelltime.daemon",
+ baseFolder: baseFolder,
+ user: user,
+ serviceName: "xyz.shelltime.daemon",
+ daemonBinPath: daemonBinPath,
}
}
@@ -163,8 +165,9 @@ func (m *MacDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.Bu
return
}
err = tmpl.Execute(&buf, map[string]string{
- "UserName": username,
- "BaseFolder": m.baseFolder,
+ "UserName": username,
+ "BaseFolder": m.baseFolder,
+ "DaemonBinPath": m.daemonBinPath,
})
return
}
diff --git a/model/daemon-installer.go b/model/daemon-installer.go
index ae5d711..516bde2 100644
--- a/model/daemon-installer.go
+++ b/model/daemon-installer.go
@@ -18,12 +18,12 @@ type DaemonInstaller interface {
}
// Factory function to create appropriate installer based on OS
-func NewDaemonInstaller(baseFolder, username string) (DaemonInstaller, error) {
+func NewDaemonInstaller(baseFolder, username, daemonBinPath string) (DaemonInstaller, error) {
switch runtime.GOOS {
case "linux":
- return NewLinuxDaemonInstaller(baseFolder, username), nil
+ return NewLinuxDaemonInstaller(baseFolder, username, daemonBinPath), nil
case "darwin":
- return NewMacDaemonInstaller(baseFolder, username), nil
+ return NewMacDaemonInstaller(baseFolder, username, daemonBinPath), nil
default:
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
diff --git a/model/daemon-installer.linux.go b/model/daemon-installer.linux.go
index 1d468f8..db9f27c 100644
--- a/model/daemon-installer.linux.go
+++ b/model/daemon-installer.linux.go
@@ -19,12 +19,13 @@ var daemonLinuxServiceDesc []byte
// LinuxDaemonInstaller implements DaemonInstaller for Linux systems
type LinuxDaemonInstaller struct {
- baseFolder string
- user string
+ baseFolder string
+ user string
+ daemonBinPath string
}
-func NewLinuxDaemonInstaller(baseFolder, user string) *LinuxDaemonInstaller {
- return &LinuxDaemonInstaller{baseFolder: baseFolder, user: user}
+func NewLinuxDaemonInstaller(baseFolder, user, daemonBinPath string) *LinuxDaemonInstaller {
+ return &LinuxDaemonInstaller{baseFolder: baseFolder, user: user, daemonBinPath: daemonBinPath}
}
// getXDGRuntimeDir returns the XDG_RUNTIME_DIR path for the current user
@@ -221,8 +222,9 @@ func (l *LinuxDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.
return
}
err = tmpl.Execute(&buf, map[string]string{
- "UserName": username,
- "BaseFolder": l.baseFolder,
+ "UserName": username,
+ "BaseFolder": l.baseFolder,
+ "DaemonBinPath": l.daemonBinPath,
})
return
}
diff --git a/model/hooks/bash.bash b/model/hooks/bash.bash
new file mode 100644
index 0000000..2f4e196
--- /dev/null
+++ b/model/hooks/bash.bash
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+# Source bash-preexec.sh if it exists
+if [ -f "bash-preexec.sh" ]; then
+ source "bash-preexec.sh"
+else
+ # Attempt to find bash-preexec.sh in the same directory as this script
+ _SHELLTIME_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ if [ -f "$_SHELLTIME_HOOK_DIR/bash-preexec.sh" ]; then
+ source "$_SHELLTIME_HOOK_DIR/bash-preexec.sh"
+ else
+ echo "Warning: bash-preexec.sh not found. Pre-execution hooks will not work."
+ return 1
+ fi
+fi
+
+# Check if shelltime CLI exists
+if ! command -v shelltime &> /dev/null
+then
+ echo "Warning: shelltime CLI not found. Please install it to enable time tracking."
+else
+ shelltime gc
+fi
+
+# Create a timestamp for the session when the shell starts
+SESSION_ID=$(date +%Y%m%d%H%M%S)
+LAST_COMMAND=""
+
+# Function to be executed before each command
+preexec_invoke_cmd() {
+ local CMD="$1"
+ LAST_COMMAND="$CMD"
+ # Check if command starts with exit, logout, or reboot
+ if [[ "$CMD" =~ ^(exit|logout|reboot) ]]; then
+ return
+ fi
+
+ # Avoid tracking shelltime commands themselves to prevent loops
+ if [[ "$CMD" =~ ^shelltime ]]; then
+ return
+ fi
+
+ shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null
+}
+
+# Function to be executed after each command (before prompt)
+precmd_invoke_cmd() {
+ local LAST_RESULT=$?
+ # BASH_COMMAND in precmd is the *previous* command
+ local CMD="$LAST_COMMAND"
+ # Check if command starts with exit, logout, or reboot
+ if [[ "$CMD" =~ ^(exit|logout|reboot) ]]; then
+ return
+ fi
+
+ # Avoid tracking shelltime commands themselves to prevent loops
+ if [[ "$CMD" =~ ^shelltime ]]; then
+ return
+ fi
+
+ # Ensure CMD is not empty or the precmd_invoke_cmd itself
+ if [ -z "$CMD" ] || [ "$CMD" == "precmd_invoke_cmd" ]; then
+ return
+ fi
+
+ shelltime track -s=bash -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
+}
+
+# Set the functions for bash-preexec
+preexec_functions+=(preexec_invoke_cmd)
+precmd_functions+=(precmd_invoke_cmd)
diff --git a/model/hooks/fish.fish b/model/hooks/fish.fish
new file mode 100644
index 0000000..da25227
--- /dev/null
+++ b/model/hooks/fish.fish
@@ -0,0 +1,33 @@
+#!/usr/bin/env fish
+
+# Check if shelltime CLI exists
+if not command -v shelltime > /dev/null
+ echo "Warning: shelltime CLI not found. Please install it to enable time tracking."
+else
+ shelltime gc
+end
+
+# Create a timestamp for the session when the shell starts
+set -g SESSION_ID (date +%Y%m%d%H%M%S)
+
+# Capture parent process ID at shell startup (fish doesn't have native $PPID)
+set -g FISH_PPID (ps -o ppid= -p %self | string trim)
+
+# Define the preexec function
+function fish_preexec --on-event fish_preexec
+ if string match -q 'exit*' -- $argv; or string match -q 'logout*' -- $argv; or string match -q 'reboot*' -- $argv
+ return
+ end
+
+ shelltime track -s=fish -id=$SESSION_ID -cmd="$argv" -p=pre --ppid=$FISH_PPID > /dev/null
+end
+
+# Define the postexec function
+function fish_postexec --on-event fish_postexec
+ set -g LAST_RESULT (echo $status)
+ if string match -q 'exit*' -- $argv; or string match -q 'logout*' -- $argv; or string match -q 'reboot*' -- $argv
+ return
+ end
+ # This event is triggered before each prompt, which is after each command
+ shelltime track -s=fish -id=$SESSION_ID -cmd="$argv" -p=post -r=$LAST_RESULT --ppid=$FISH_PPID > /dev/null
+end
diff --git a/model/hooks/zsh.zsh b/model/hooks/zsh.zsh
new file mode 100644
index 0000000..fb89c05
--- /dev/null
+++ b/model/hooks/zsh.zsh
@@ -0,0 +1,34 @@
+#!/usr/bin/env zsh
+
+# Check if shelltime command exists
+if ! command -v shelltime &> /dev/null
+then
+ echo "Warning: shelltime command not found. Please install it to track shell usage."
+else
+ shelltime gc
+fi
+
+# Create a timestamp for the session when the shell starts
+SESSION_ID=$(date +%Y%m%d%H%M%S)
+
+# Define the preexec function
+preexec() {
+ local CMD=$1
+ # Check if command starts with exit, logout, or reboot
+ if [[ $CMD =~ ^(exit|logout|reboot) ]]; then
+ return
+ fi
+
+ shelltime track -s=zsh -id=$SESSION_ID -cmd="$CMD" -p=pre --ppid=$PPID &> /dev/null
+}
+
+# Define the postexec function (in zsh, it's called precmd)
+precmd() {
+ local LAST_RESULT=$?
+ local CMD=$(fc -ln -1)
+ # Check if command starts with exit, logout, or reboot
+ if [[ $CMD =~ ^(exit|logout|reboot) ]]; then
+ return
+ fi
+ shelltime track -s=zsh -id=$SESSION_ID -cmd="$CMD" -p=post -r=$LAST_RESULT --ppid=$PPID &> /dev/null
+}
diff --git a/model/path.go b/model/path.go
index 938beaf..cc52929 100644
--- a/model/path.go
+++ b/model/path.go
@@ -1,7 +1,9 @@
package model
import (
+ "fmt"
"os"
+ "os/exec"
"path/filepath"
)
@@ -111,3 +113,35 @@ func GetDaemonLogFilePath() string {
func GetDaemonErrFilePath() string {
return GetStoragePath("logs", "shelltime-daemon.err")
}
+
+// ResolveDaemonBinaryPath finds the shelltime-daemon binary.
+// It checks the curl-installer location first, then PATH (Homebrew),
+// then known Homebrew prefix paths as fallback.
+func ResolveDaemonBinaryPath() (string, error) {
+ const binaryName = "shelltime-daemon"
+
+ // 1. Check curl-installer location
+ curlPath := filepath.Join(GetBaseStoragePath(), "bin", binaryName)
+ if info, err := os.Stat(curlPath); err == nil && !info.IsDir() {
+ return curlPath, nil
+ }
+
+ // 2. Check PATH (covers Homebrew and other package managers)
+ if path, err := exec.LookPath(binaryName); err == nil {
+ return path, nil
+ }
+
+ // 3. Explicit Homebrew fallback paths
+ homebrewPaths := []string{
+ filepath.Join("/opt/homebrew/bin", binaryName),
+ filepath.Join("/usr/local/bin", binaryName),
+ filepath.Join("/home/linuxbrew/.linuxbrew/bin", binaryName),
+ }
+ for _, p := range homebrewPaths {
+ if info, err := os.Stat(p); err == nil && !info.IsDir() {
+ return p, nil
+ }
+ }
+
+ return "", fmt.Errorf("%s not found at %s, on PATH, or in standard Homebrew locations", binaryName, curlPath)
+}
diff --git a/model/shell.bash.go b/model/shell.bash.go
index 4410d9b..3ca7f8d 100644
--- a/model/shell.bash.go
+++ b/model/shell.bash.go
@@ -2,12 +2,48 @@ package model
import (
"fmt"
+ "io"
+ "net/http"
"os"
+ "path/filepath"
"strings"
+ "time"
"github.com/gookit/color"
)
+const bashPreexecURL = "https://raw.githubusercontent.com/rcaloras/bash-preexec/master/bash-preexec.sh"
+
+// ensureBashPreexec downloads bash-preexec.sh if it doesn't exist in the hooks directory.
+func ensureBashPreexec(hooksDir string) error {
+ preexecPath := filepath.Join(hooksDir, "bash-preexec.sh")
+ if _, err := os.Stat(preexecPath); err == nil {
+ return nil // already exists
+ }
+
+ client := &http.Client{Timeout: 1 * time.Minute}
+ resp, err := client.Get(bashPreexecURL)
+ if err != nil {
+ return fmt.Errorf("failed to download bash-preexec.sh: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("failed to download bash-preexec.sh: HTTP %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return fmt.Errorf("failed to read bash-preexec.sh response: %w", err)
+ }
+
+ if err := os.WriteFile(preexecPath, body, 0644); err != nil {
+ return fmt.Errorf("failed to write bash-preexec.sh: %w", err)
+ }
+
+ return nil
+}
+
type BashHookService struct {
BaseHookService
shellName string
@@ -38,10 +74,11 @@ func (s *BashHookService) ShellName() string {
func (s *BashHookService) Install() error {
hookFilePath := os.ExpandEnv(fmt.Sprintf("$HOME/%s/hooks/bash.bash", COMMAND_BASE_STORAGE_FOLDER))
- if _, err := os.Stat(hookFilePath); os.IsNotExist(err) {
- color.Red.Println("hook file not found at", hookFilePath)
- color.Red.Println("Please run 'curl -sSL https://shelltime.xyz/i | bash' first")
- return err
+ if err := ensureHookFile(hookFilePath, EmbeddedBashHook); err != nil {
+ return fmt.Errorf("failed to ensure bash hook file: %w", err)
+ }
+ if err := ensureBashPreexec(filepath.Dir(hookFilePath)); err != nil {
+ color.Yellow.Printf("Warning: failed to set up bash-preexec: %v\n", err)
}
if _, err := os.Stat(s.configPath); os.IsNotExist(err) {
diff --git a/model/shell.fish.go b/model/shell.fish.go
index abde4be..7d1ba0d 100644
--- a/model/shell.fish.go
+++ b/model/shell.fish.go
@@ -42,6 +42,11 @@ func (s *FishHookService) ShellName() string {
}
func (s *FishHookService) Install() error {
+ hookFilePath := os.ExpandEnv(fmt.Sprintf("$HOME/%s/hooks/fish.fish", COMMAND_BASE_STORAGE_FOLDER))
+ if err := ensureHookFile(hookFilePath, EmbeddedFishHook); err != nil {
+ return fmt.Errorf("failed to ensure fish hook file: %w", err)
+ }
+
if _, err := os.Stat(s.configPath); os.IsNotExist(err) {
return fmt.Errorf("fish config file not found at %s. Please run 'shelltime hooks install'", s.configPath)
}
diff --git a/model/shell.go b/model/shell.go
index ac7b3ce..68ff890 100644
--- a/model/shell.go
+++ b/model/shell.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
+ "path/filepath"
"strings"
"time"
)
@@ -18,6 +19,22 @@ type ShellHookService interface {
ShellName() string
}
+// ensureHookFile writes the embedded hook script to disk if it doesn't already exist.
+// This allows Homebrew-installed users to get hook files without the curl installer.
+func ensureHookFile(hookFilePath string, embeddedContent []byte) error {
+ if _, err := os.Stat(hookFilePath); err == nil {
+ return nil // already exists
+ }
+ hookDir := filepath.Dir(hookFilePath)
+ if err := os.MkdirAll(hookDir, 0755); err != nil {
+ return fmt.Errorf("failed to create hooks directory: %w", err)
+ }
+ if err := os.WriteFile(hookFilePath, embeddedContent, 0644); err != nil {
+ return fmt.Errorf("failed to write hook file: %w", err)
+ }
+ return nil
+}
+
// Common utilities for hook services
type BaseHookService struct{}
diff --git a/model/shell.zsh.go b/model/shell.zsh.go
index c3c40f6..502d39c 100644
--- a/model/shell.zsh.go
+++ b/model/shell.zsh.go
@@ -38,6 +38,11 @@ func (s *ZshHookService) ShellName() string {
}
func (s *ZshHookService) Install() error {
+ hookFilePath := os.ExpandEnv(fmt.Sprintf("$HOME/%s/hooks/zsh.zsh", COMMAND_BASE_STORAGE_FOLDER))
+ if err := ensureHookFile(hookFilePath, EmbeddedZshHook); err != nil {
+ return fmt.Errorf("failed to ensure zsh hook file: %w", err)
+ }
+
if _, err := os.Stat(s.configPath); os.IsNotExist(err) {
return fmt.Errorf("zsh config file not found at %s. Please run 'shelltime hooks install'", s.configPath)
}
diff --git a/model/shell_hooks_embed.go b/model/shell_hooks_embed.go
new file mode 100644
index 0000000..6988bba
--- /dev/null
+++ b/model/shell_hooks_embed.go
@@ -0,0 +1,12 @@
+package model
+
+import _ "embed"
+
+//go:embed hooks/bash.bash
+var EmbeddedBashHook []byte
+
+//go:embed hooks/zsh.zsh
+var EmbeddedZshHook []byte
+
+//go:embed hooks/fish.fish
+var EmbeddedFishHook []byte
diff --git a/model/sys-desc/shelltime.service b/model/sys-desc/shelltime.service
index 3a95f77..9493864 100644
--- a/model/sys-desc/shelltime.service
+++ b/model/sys-desc/shelltime.service
@@ -4,7 +4,7 @@ After=network.target
[Service]
Type=simple
-ExecStart=/bin/sh -c 'exec $(getent passwd $(id -un) | cut -d: -f7) -l -c "{{.BaseFolder}}/bin/shelltime-daemon"'
+ExecStart=/bin/sh -c 'exec $(getent passwd $(id -un) | cut -d: -f7) -l -c "{{.DaemonBinPath}}"'
Restart=always
# Resource limits
diff --git a/model/sys-desc/xyz.shelltime.daemon.plist b/model/sys-desc/xyz.shelltime.daemon.plist
index 3d9bbe6..a936882 100644
--- a/model/sys-desc/xyz.shelltime.daemon.plist
+++ b/model/sys-desc/xyz.shelltime.daemon.plist
@@ -8,7 +8,7 @@
/bin/sh
-c
- exec $SHELL -l -c '{{.BaseFolder}}/bin/shelltime-daemon'
+ exec $SHELL -l -c '{{.DaemonBinPath}}'
RunAtLoad