diff --git a/README.md b/README.md index 0b04361..41a5cd6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ curl -sSL https://shelltime.xyz/i | bash 3. **Optional: Enable daemon mode** (recommended for optimal performance): ```bash - sudo shelltime daemon install + shelltime daemon install ``` ## Configuration @@ -274,20 +274,20 @@ shelltime daemon ``` **Subcommands:** -- `install`: Install the daemon service (requires sudo) -- `uninstall`: Remove the daemon service (requires sudo) -- `reinstall`: Reinstall the daemon service (requires sudo) +- `install`: Install the daemon service +- `uninstall`: Remove the daemon service +- `reinstall`: Reinstall the daemon service **Examples:** ```bash # Install daemon for better performance -sudo shelltime daemon install +shelltime daemon install # Remove daemon service -sudo shelltime daemon uninstall +shelltime daemon uninstall # Reinstall (useful for updates) -sudo shelltime daemon reinstall +shelltime daemon reinstall ``` #### `shelltime hooks` @@ -413,7 +413,7 @@ Default synchronization behavior and expected latencies: For optimal performance and minimal shell latency, enable daemon mode: ```bash -sudo ~/.shelltime/bin/shelltime daemon install +~/.shelltime/bin/shelltime daemon install ``` **Key Benefits:** @@ -423,7 +423,7 @@ sudo ~/.shelltime/bin/shelltime daemon install - **Resilient Delivery**: Automatic retry and buffering during network issues **Technical Implementation:** -- Operates as a system-level service +- Operates as a user-level service - Manages all network synchronization operations - Implements intelligent command buffering - Provides automatic retry mechanisms for failed synchronizations @@ -500,7 +500,7 @@ ShellTime implements a hybrid RSA/AES-GCM encryption scheme: Remove the daemon service when no longer needed: ```bash -sudo ~/.shelltime/bin/shelltime daemon uninstall +~/.shelltime/bin/shelltime daemon uninstall ``` **Uninstallation Process:** diff --git a/commands/daemon.install.go b/commands/daemon.install.go index 58e7782..43a04af 100644 --- a/commands/daemon.install.go +++ b/commands/daemon.install.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "os" + "os/user" "path/filepath" "github.com/gookit/color" @@ -17,20 +18,17 @@ var DaemonInstallCommand *cli.Command = &cli.Command{ } func commandDaemonInstall(c *cli.Context) error { - color.Yellow.Println("âš ī¸ Warning: This daemon service is currently not ready for use. Please proceed with caution.") - - // Check if running as root - if os.Geteuid() != 0 { - return fmt.Errorf("this command must be run as root (sudo shelltime daemon install)") - } color.Yellow.Println("🔍 Detecting system architecture...") - // TODO: the username is not stable in multiple user system - baseFolder, username, err := model.SudoGetBaseFolder() + // Get current user's home directory and username + currentUser, err := user.Current() if err != nil { - return err + return fmt.Errorf("failed to get current user: %w", err) } + baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime") + username := currentUser.Username + installer, err := model.NewDaemonInstaller(baseFolder, username) if err != nil { return err @@ -59,15 +57,8 @@ func commandDaemonInstall(c *cli.Context) error { return nil } - // Copy to final location - binaryPath := "/usr/local/bin/shelltime-daemon" - - if _, err := os.Stat(binaryPath); err != nil { - color.Yellow.Println("🔍 Creating daemon symlink...") - if err := os.Symlink(filepath.Join(baseFolder, "bin/shelltime-daemon"), binaryPath); err != nil { - return fmt.Errorf("failed to create daemon symlink: %w", err) - } - } + // User-level installation - no system-wide symlink needed + color.Yellow.Println("🔍 Setting up user-level daemon installation...") if err := installer.InstallService(username); err != nil { return err diff --git a/commands/daemon.reinstall.go b/commands/daemon.reinstall.go index 3a5cbc7..60b84fb 100644 --- a/commands/daemon.reinstall.go +++ b/commands/daemon.reinstall.go @@ -15,7 +15,7 @@ func commandDaemonReinstall(c *cli.Context) error { color.Yellow.Println("🔄 Starting daemon service reinstallation...") // First, uninstall the existing service - color.Yellow.Println("🗑 Uninstalling existing daemon service...") + color.Yellow.Println("🗑 Uninstalling existing daemon service...") if err := commandDaemonUninstall(c); err != nil { return err } @@ -28,4 +28,4 @@ func commandDaemonReinstall(c *cli.Context) error { color.Green.Println("✅ Daemon service has been successfully reinstalled!") return nil -} \ No newline at end of file +} diff --git a/commands/daemon.uninstall.go b/commands/daemon.uninstall.go index be034e9..38caa45 100644 --- a/commands/daemon.uninstall.go +++ b/commands/daemon.uninstall.go @@ -2,7 +2,8 @@ package commands import ( "fmt" - "os" + "os/user" + "path/filepath" "github.com/gookit/color" "github.com/malamtime/cli/model" @@ -16,19 +17,17 @@ var DaemonUninstallCommand = &cli.Command{ } func commandDaemonUninstall(c *cli.Context) error { - // Check if running as root - if os.Geteuid() != 0 { - return fmt.Errorf("this command must be run as root (sudo shelltime daemon uninstall)") - } - color.Yellow.Println("🔍 Starting daemon service uninstallation...") - // TODO: the username is not stable in multiple user system - baseFolder, username, err := model.SudoGetBaseFolder() + // Get current user's home directory and username + currentUser, err := user.Current() if err != nil { - return err + return fmt.Errorf("failed to get current user: %w", err) } + baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime") + username := currentUser.Username + installer, err := model.NewDaemonInstaller(baseFolder, username) if err != nil { return err @@ -39,14 +38,8 @@ func commandDaemonUninstall(c *cli.Context) error { return fmt.Errorf("failed to unregister service: %w", err) } - // Remove symlink from /usr/local/bin - binaryPath := "/usr/local/bin/shelltime-daemon" - if _, err := os.Stat(binaryPath); err == nil { - color.Yellow.Println("🗑 Removing daemon symlink...") - if err := os.Remove(binaryPath); err != nil { - return fmt.Errorf("failed to remove daemon symlink: %w", err) - } - } + // No need to remove system-wide symlink for user-level installation + color.Yellow.Println("🗑 User-level daemon service cleanup completed...") color.Green.Println("✅ Daemon service has been successfully uninstalled!") // color.Yellow.Println("â„šī¸ Note: Your commands will now be synced to shelltime.xyz on the next login") diff --git a/model/daemon-installer.darwin.go b/model/daemon-installer.darwin.go index e2ab7a3..2e50366 100644 --- a/model/daemon-installer.darwin.go +++ b/model/daemon-installer.darwin.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "os/user" "path/filepath" "text/template" @@ -31,7 +32,7 @@ func NewMacDaemonInstaller(baseFolder, user string) *MacDaemonInstaller { } func (m *MacDaemonInstaller) Check() error { - cmd := exec.Command("launchctl", "print", "system/"+m.serviceName) + cmd := exec.Command("launchctl", "print", "user/"+fmt.Sprintf("%d", os.Getuid())+"/"+m.serviceName) if err := cmd.Run(); err == nil { return nil } @@ -41,13 +42,16 @@ func (m *MacDaemonInstaller) Check() error { func (m *MacDaemonInstaller) CheckAndStopExistingService() error { color.Yellow.Println("🔍 Checking if service is running...") - if err := m.Check(); err != nil { - return err - } - - color.Yellow.Println("🛑 Stopping existing service...") - if err := exec.Command("launchctl", "unload", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run(); err != nil { - return fmt.Errorf("failed to stop existing service: %w", err) + if err := m.Check(); err == nil { + color.Yellow.Println("🛑 Stopping existing service...") + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName)) + if err := exec.Command("launchctl", "unload", agentPath).Run(); err != nil { + return fmt.Errorf("failed to stop existing service: %w", err) + } } return nil } @@ -62,6 +66,12 @@ func (m *MacDaemonInstaller) InstallService(username string) error { return fmt.Errorf("failed to create daemon directory: %w", err) } + // Create logs directory if not exists + logsPath := filepath.Join(m.baseFolder, "logs") + if err := os.MkdirAll(logsPath, 0755); err != nil { + return fmt.Errorf("failed to create logs directory: %w", err) + } + plistPath := filepath.Join(daemonPath, fmt.Sprintf("%s.plist", m.serviceName)) if _, err := os.Stat(plistPath); err == nil { if err := os.Remove(plistPath); err != nil { @@ -84,7 +94,19 @@ func (m *MacDaemonInstaller) RegisterService() error { if m.baseFolder == "" { return fmt.Errorf("base folder is not set") } - plistPath := fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName) + + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + + // Create LaunchAgents directory if it doesn't exist + launchAgentsDir := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents") + if err := os.MkdirAll(launchAgentsDir, 0755); err != nil { + return fmt.Errorf("failed to create LaunchAgents directory: %w", err) + } + + plistPath := filepath.Join(launchAgentsDir, fmt.Sprintf("%s.plist", m.serviceName)) if _, err := os.Stat(plistPath); err != nil { sourceFile := filepath.Join(m.baseFolder, fmt.Sprintf("daemon/%s.plist", m.serviceName)) if err := os.Symlink(sourceFile, plistPath); err != nil { @@ -96,7 +118,14 @@ func (m *MacDaemonInstaller) RegisterService() error { func (m *MacDaemonInstaller) StartService() error { color.Yellow.Println("🚀 Starting service...") - if err := exec.Command("launchctl", "load", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run(); err != nil { + + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + + agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName)) + if err := exec.Command("launchctl", "load", agentPath).Run(); err != nil { return fmt.Errorf("failed to start service: %w", err) } return nil @@ -106,14 +135,22 @@ func (m *MacDaemonInstaller) UnregisterService() error { if m.baseFolder == "" { return fmt.Errorf("base folder is not set") } + + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + + agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName)) + color.Yellow.Println("🛑 Stopping service if running...") // Try to stop the service first - _ = exec.Command("launchctl", "unload", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run() + _ = exec.Command("launchctl", "unload", agentPath).Run() - color.Yellow.Println("🗑 Removing service files...") - // Remove symlink from LaunchDaemons - if err := os.Remove(fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove launch daemon plist: %w", err) + color.Yellow.Println("🗑 Removing service files...") + // Remove symlink from LaunchAgents + if err := os.Remove(agentPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove launch agent plist: %w", err) } color.Green.Println("✅ Service unregistered successfully") @@ -126,7 +163,8 @@ func (m *MacDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.Bu return } err = tmpl.Execute(&buf, map[string]string{ - "UserName": username, + "UserName": username, + "BaseFolder": m.baseFolder, }) return } diff --git a/model/daemon-installer.linux.go b/model/daemon-installer.linux.go index 185188c..d55dd80 100644 --- a/model/daemon-installer.linux.go +++ b/model/daemon-installer.linux.go @@ -117,7 +117,7 @@ func (l *LinuxDaemonInstaller) UnregisterService() error { _ = exec.Command("systemctl", "stop", "shelltime").Run() _ = exec.Command("systemctl", "disable", "shelltime").Run() - color.Yellow.Println("🗑 Removing service files...") + color.Yellow.Println("🗑 Removing service files...") // Remove symlink from systemd if err := os.Remove("/etc/systemd/system/shelltime.service"); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove systemd service symlink: %w", err) diff --git a/model/sys-desc/xyz.shelltime.daemon.plist b/model/sys-desc/xyz.shelltime.daemon.plist index 9ed44af..d30eefc 100644 --- a/model/sys-desc/xyz.shelltime.daemon.plist +++ b/model/sys-desc/xyz.shelltime.daemon.plist @@ -6,16 +6,16 @@ xyz.shelltime.daemon ProgramArguments - /usr/local/bin/shelltime-daemon + {{.BaseFolder}}/bin/shelltime-daemon RunAtLoad KeepAlive StandardErrorPath - /var/log/shelltime-daemon.err + {{.BaseFolder}}/logs/shelltime-daemon.err StandardOutPath - /var/log/shelltime-daemon.log + {{.BaseFolder}}/logs/shelltime-daemon.log EnvironmentVariables HOSTING_USER