package main import ( "bufio" "flag" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "git.davoryn.de/calic/syncwarden/internal/config" st "git.davoryn.de/calic/syncwarden/internal/syncthing" ) func main() { uninstall := flag.Bool("uninstall", false, "Remove installed files and autostart entry") flag.Parse() if *uninstall { runUninstall() } else { runInstall() } } func installDir() string { if runtime.GOOS == "windows" { return filepath.Join(os.Getenv("LOCALAPPDATA"), "syncwarden") } home, _ := os.UserHomeDir() return filepath.Join(home, ".local", "bin") } func binaryNames() []string { if runtime.GOOS == "windows" { return []string{"syncwarden.exe", "syncwarden-panel.exe"} } return []string{"syncwarden", "syncwarden-panel"} } func sourceDir() string { exe, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "Error: cannot determine executable path: %v\n", err) os.Exit(1) } return filepath.Dir(exe) } func findSourceBinary(dir, name string) string { p := filepath.Join(dir, name) if _, err := os.Stat(p); err == nil { return p } // Try without "syncwarden-" prefix for local builds bare := strings.TrimPrefix(name, "syncwarden-") if bare != name { p = filepath.Join(dir, bare) if _, err := os.Stat(p); err == nil { return p } } return "" } func askYesNo(prompt string) bool { reader := bufio.NewReader(os.Stdin) fmt.Printf("%s [Y/n] ", prompt) answer, _ := reader.ReadString('\n') answer = strings.TrimSpace(strings.ToLower(answer)) return answer == "" || answer == "y" || answer == "yes" } func runInstall() { fmt.Println("SyncWarden Setup") fmt.Println(strings.Repeat("=", 40)) fmt.Println() src := sourceDir() dst := installDir() var errors []string fmt.Printf("Install directory: %s\n\n", dst) // Create install directory if err := os.MkdirAll(dst, 0o755); err != nil { msg := "Could not create install directory: " + err.Error() fmt.Fprintln(os.Stderr, msg) showMessage("SyncWarden — Setup", "Something went wrong.\n\n"+msg, true) os.Exit(1) } // Copy binaries fmt.Println("Installing binaries...") binaries := binaryNames() installed := 0 for _, name := range binaries { srcPath := findSourceBinary(src, name) dstPath := filepath.Join(dst, name) if srcPath == "" { fmt.Printf(" SKIP %s (not found in source directory)\n", name) continue } err := copyFile(srcPath, dstPath) if err != nil { if runtime.GOOS == "windows" && isFileInUse(err) { if isInteractive() { fmt.Printf(" BUSY %s — please close the app and press Enter to retry: ", name) bufio.NewReader(os.Stdin).ReadString('\n') err = copyFile(srcPath, dstPath) } } if err != nil { errors = append(errors, fmt.Sprintf("%s: %v", name, err)) fmt.Fprintf(os.Stderr, " FAIL %s: %v\n", name, err) continue } } fmt.Printf(" OK %s\n", name) installed++ } fmt.Println() if installed == 0 { msg := "No binaries found next to setup. Make sure setup is in the same directory as the binaries." fmt.Fprintln(os.Stderr, msg) showMessage("SyncWarden — Setup", "Something went wrong.\n\n"+msg, true) os.Exit(1) } // Autostart enableAutostart := true if isInteractive() { enableAutostart = askYesNo("Enable autostart for SyncWarden?") } if enableAutostart { if err := createAutostart(dst); err != nil { errors = append(errors, "autostart: "+err.Error()) fmt.Fprintf(os.Stderr, " Warning: could not set up autostart: %v\n", err) } else { fmt.Println(" Autostart enabled.") } } fmt.Println() // Config directory cfgDir := config.ConfigDir() if err := os.MkdirAll(cfgDir, 0o755); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not create config directory: %v\n", err) } // Launch on Windows trayName := "syncwarden" if runtime.GOOS == "windows" { trayName = "syncwarden.exe" } trayPath := filepath.Join(dst, trayName) if runtime.GOOS == "windows" { exec.Command(trayPath).Start() } // Summary fmt.Println(strings.Repeat("-", 40)) fmt.Println("Installation complete!") fmt.Println() fmt.Printf(" Binaries: %s\n", dst) fmt.Printf(" Config: %s\n", cfgDir) fmt.Println() fmt.Printf("Launch SyncWarden: %s\n", trayPath) // Check if Syncthing is installed if !st.IsInstalled() { fmt.Println() fmt.Println("Note: Syncthing is not installed.") fmt.Printf("Download it from: %s\n", st.DownloadURL) if isInteractive() { openURL(st.DownloadURL) } } if len(errors) > 0 { msg := "Setup completed with warnings:\n\n" + strings.Join(errors, "\n") + "\n\nSuccessfully installed " + fmt.Sprint(installed) + " binaries to:\n" + dst if !st.IsInstalled() { msg += "\n\nNote: Syncthing is not installed.\nDownload it from: " + st.DownloadURL } showMessage("SyncWarden — Setup", msg, true) } else { msg := "Setup complete!\n\nSyncWarden is running.\n\nIt will auto-discover your Syncthing API key." if !st.IsInstalled() { msg += "\n\nNote: Syncthing is not installed.\nDownload it from: " + st.DownloadURL } showMessage("SyncWarden — Setup", msg, false) } } func runUninstall() { fmt.Println("SyncWarden Uninstall") fmt.Println(strings.Repeat("=", 40)) fmt.Println() dst := installDir() var errors []string // Stop running instance fmt.Println("Stopping SyncWarden...") if runtime.GOOS == "windows" { exec.Command("taskkill", "/F", "/IM", "syncwarden.exe").Run() exec.Command("taskkill", "/F", "/IM", "syncwarden-panel.exe").Run() } else { exec.Command("pkill", "-f", "syncwarden").Run() } fmt.Println() // Remove binaries fmt.Println("Removing binaries...") for _, name := range binaryNames() { p := filepath.Join(dst, name) if _, err := os.Stat(p); os.IsNotExist(err) { continue } if err := os.Remove(p); err != nil { if runtime.GOOS == "windows" && isFileInUse(err) { if isInteractive() { fmt.Printf(" BUSY %s — please close the app and press Enter to retry: ", name) bufio.NewReader(os.Stdin).ReadString('\n') err = os.Remove(p) } } if err != nil { errors = append(errors, fmt.Sprintf("%s: %v", name, err)) fmt.Fprintf(os.Stderr, " FAIL %s: %v\n", name, err) continue } } fmt.Printf(" Removed %s\n", name) } fmt.Println() // Remove autostart fmt.Println("Removing autostart entry...") if err := removeAutostart(); err != nil { errors = append(errors, "autostart: "+err.Error()) fmt.Fprintf(os.Stderr, " Warning: %v\n", err) } else { fmt.Println(" Autostart removed.") } fmt.Println() fmt.Println("Uninstall complete.") if len(errors) > 0 { showMessage("SyncWarden — Uninstall", "Uninstall completed with warnings:\n\n"+strings.Join(errors, "\n"), true) } else { showMessage("SyncWarden — Uninstall", "Uninstall complete.\n\nAll binaries and autostart entry have been removed.", false) } } func isInteractive() bool { if runtime.GOOS != "windows" { return true } fi, err := os.Stdin.Stat() if err != nil { return false } return fi.Mode()&os.ModeCharDevice != 0 } func copyFile(src, dst string) error { data, err := os.ReadFile(src) if err != nil { return err } return os.WriteFile(dst, data, 0o755) } func isFileInUse(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "Access is denied") || strings.Contains(msg, "being used by another process") } func createAutostart(installDir string) error { if runtime.GOOS == "windows" { return createAutostartWindows(installDir) } return createAutostartLinux(installDir) } func createAutostartWindows(dir string) error { startupDir := filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup") lnkPath := filepath.Join(startupDir, "SyncWarden.lnk") target := filepath.Join(dir, "syncwarden.exe") script := fmt.Sprintf( `$s = (New-Object -COM WScript.Shell).CreateShortcut('%s'); $s.TargetPath = '%s'; $s.WorkingDirectory = '%s'; $s.Save()`, lnkPath, target, dir, ) cmd := exec.Command("powershell", "-NoProfile", "-Command", script) cmd.Stderr = os.Stderr return cmd.Run() } func createAutostartLinux(dir string) error { autostartDir := filepath.Join(os.Getenv("HOME"), ".config", "autostart") if err := os.MkdirAll(autostartDir, 0o755); err != nil { return err } desktopEntry := fmt.Sprintf(`[Desktop Entry] Type=Application Name=SyncWarden Exec=%s Terminal=false X-GNOME-Autostart-enabled=true `, filepath.Join(dir, "syncwarden")) return os.WriteFile(filepath.Join(autostartDir, "syncwarden.desktop"), []byte(desktopEntry), 0o644) } func removeAutostart() error { if runtime.GOOS == "windows" { return removeAutostartWindows() } return removeAutostartLinux() } func removeAutostartWindows() error { lnkPath := filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "SyncWarden.lnk") if _, err := os.Stat(lnkPath); os.IsNotExist(err) { return nil } return os.Remove(lnkPath) } func openURL(url string) { var cmd *exec.Cmd switch runtime.GOOS { case "windows": cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) case "darwin": cmd = exec.Command("open", url) default: cmd = exec.Command("xdg-open", url) } _ = cmd.Start() } func removeAutostartLinux() error { desktopPath := filepath.Join(os.Getenv("HOME"), ".config", "autostart", "syncwarden.desktop") if _, err := os.Stat(desktopPath); os.IsNotExist(err) { return nil } return os.Remove(desktopPath) }