A shepherd for your Appimages.
0

Configure Feed

Select the types of activity you want to include in your feed.

add autoupgrade cli and units

Aly Raffauf (Jun 17, 2026, 11:45 PM EDT) 2f129706 8324f249

+152 -74
+2
README.md
··· 19 19 - **Tells you what it manages.** `appherder list` shows every app it installed, where it checks for updates, and which ones are missing their AppImage. 20 20 - **Quiet when nothing changed.** Re-installing an unchanged app does nothing. Drop your AppImages in one folder and `appherder sync` lines everything up. 21 21 - **Syncs automatically.** `appherder autosync` watches `~/AppImages` and installs or removes apps the moment you add or delete a file. 22 + - **Upgrades automatically.** `appherder autoupgrade` checks for and installs updates once a day. 22 23 23 24 ## Installation 24 25 ··· 58 59 appherder list # see what's installed 59 60 appherder sync # match your apps to what's in ~/AppImages 60 61 appherder autosync # auto-sync on file changes 62 + appherder autoupgrade # auto-upgrade once a day 61 63 appherder upgrade --check # see what's out of date 62 64 appherder upgrade # download and install updates 63 65 appherder migrate # adopt apps another tool set up
+8 -74
cmd/appherder/autosync.go
··· 1 1 package main 2 2 3 - import ( 4 - "embed" 5 - "fmt" 6 - "io/fs" 7 - "os" 8 - "os/exec" 9 - "path/filepath" 10 - "strings" 11 - ) 12 - 13 - //go:embed systemd/*.path systemd/*.service 14 - var unitTemplates embed.FS 3 + var syncUnits = []string{ 4 + "appherder-sync.path", 5 + "appherder-sync.service", 6 + } 15 7 16 - const autosyncUnitBase = "appherder-sync" 17 - 18 - // enableAutosync writes the systemd user units with the current binary path and 19 - // enables the watcher. 20 8 func enableAutosync() error { 21 - binPath, err := os.Executable() 22 - if err != nil { 23 - return fmt.Errorf("resolve appherder binary: %w", err) 24 - } 25 - if binPath, err = filepath.EvalSymlinks(binPath); err != nil { 26 - return fmt.Errorf("resolve appherder binary: %w", err) 27 - } 28 - 29 - userDir, err := systemdUserDir() 30 - if err != nil { 9 + if err := writeUnitFiles(syncUnits); err != nil { 31 10 return err 32 11 } 33 - if err := os.MkdirAll(userDir, 0o755); err != nil { 34 - return fmt.Errorf("create systemd user directory: %w", err) 35 - } 36 - 37 - entries, err := fs.ReadDir(unitTemplates, "systemd") 38 - if err != nil { 39 - return fmt.Errorf("read embedded units: %w", err) 40 - } 41 - for _, entry := range entries { 42 - data, err := unitTemplates.ReadFile("systemd/" + entry.Name()) 43 - if err != nil { 44 - return fmt.Errorf("read %s: %w", entry.Name(), err) 45 - } 46 - rendered := strings.ReplaceAll(string(data), "{{BIN}}", binPath) 47 - dest := filepath.Join(userDir, entry.Name()) 48 - if err := os.WriteFile(dest, []byte(rendered), 0o644); err != nil { 49 - return fmt.Errorf("write %s: %w", dest, err) 50 - } 51 - } 52 - 53 12 if err := runSystemctl("daemon-reload"); err != nil { 54 13 return err 55 14 } 56 - return runSystemctl("enable", "--now", autosyncUnitBase+".path") 15 + return runSystemctl("enable", "--now", "appherder-sync.path") 57 16 } 58 17 59 - // disableAutosync stops and removes the systemd user units. 60 18 func disableAutosync() error { 61 - if err := runSystemctl("disable", "--now", autosyncUnitBase+".path"); err != nil { 19 + if err := runSystemctl("disable", "--now", "appherder-sync.path"); err != nil { 62 20 return err 63 21 } 64 - userDir, err := systemdUserDir() 65 - if err != nil { 22 + if err := removeUnitFiles(syncUnits); err != nil { 66 23 return err 67 24 } 68 - entries, err := fs.ReadDir(unitTemplates, "systemd") 69 - if err != nil { 70 - return fmt.Errorf("read embedded units: %w", err) 71 - } 72 - for _, entry := range entries { 73 - _ = os.Remove(filepath.Join(userDir, entry.Name())) 74 - } 75 25 return runSystemctl("daemon-reload") 76 26 } 77 - 78 - func systemdUserDir() (string, error) { 79 - home, err := os.UserHomeDir() 80 - if err != nil { 81 - return "", fmt.Errorf("resolve home directory: %w", err) 82 - } 83 - return filepath.Join(home, ".config", "systemd", "user"), nil 84 - } 85 - 86 - func runSystemctl(args ...string) error { 87 - cmd := exec.Command("systemctl", append([]string{"--user"}, args...)...) 88 - if out, err := cmd.CombinedOutput(); err != nil { 89 - return fmt.Errorf("systemctl %s: %w\n%s", strings.Join(args, " "), err, out) 90 - } 91 - return nil 92 - }
+26
cmd/appherder/autoupgrade.go
··· 1 + package main 2 + 3 + var upgradeUnits = []string{ 4 + "appherder-upgrade.timer", 5 + "appherder-upgrade.service", 6 + } 7 + 8 + func enableAutoupgrade() error { 9 + if err := writeUnitFiles(upgradeUnits); err != nil { 10 + return err 11 + } 12 + if err := runSystemctl("daemon-reload"); err != nil { 13 + return err 14 + } 15 + return runSystemctl("enable", "--now", "appherder-upgrade.timer") 16 + } 17 + 18 + func disableAutoupgrade() error { 19 + if err := runSystemctl("disable", "--now", "appherder-upgrade.timer"); err != nil { 20 + return err 21 + } 22 + if err := removeUnitFiles(upgradeUnits); err != nil { 23 + return err 24 + } 25 + return runSystemctl("daemon-reload") 26 + }
+20
cmd/appherder/cli.go
··· 29 29 newMigrateCommand(a), 30 30 newUpgradeCommand(a), 31 31 newAutosyncCommand(), 32 + newAutoupgradeCommand(), 32 33 ) 33 34 return cmd 34 35 } ··· 189 190 cmd.Flags().BoolVar(&off, "off", false, "Disable and remove the autosync watcher") 190 191 return cmd 191 192 } 193 + 194 + func newAutoupgradeCommand() *cobra.Command { 195 + var off bool 196 + cmd := &cobra.Command{ 197 + Use: "autoupgrade", 198 + Short: "Enable or disable daily automatic upgrades", 199 + Long: "Installs a systemd user timer that checks for and installs AppImage updates\n" + 200 + "once a day. No root required.", 201 + Args: cobra.NoArgs, 202 + RunE: func(cmd *cobra.Command, args []string) error { 203 + if off { 204 + return disableAutoupgrade() 205 + } 206 + return enableAutoupgrade() 207 + }, 208 + } 209 + cmd.Flags().BoolVar(&off, "off", false, "Disable and remove the upgrade timer") 210 + return cmd 211 + }
+79
cmd/appherder/systemd.go
··· 1 + package main 2 + 3 + import ( 4 + "embed" 5 + "fmt" 6 + "os" 7 + "os/exec" 8 + "path/filepath" 9 + "strings" 10 + ) 11 + 12 + //go:embed systemd/* 13 + var unitTemplates embed.FS 14 + 15 + func binaryPath() (string, error) { 16 + bin, err := os.Executable() 17 + if err != nil { 18 + return "", fmt.Errorf("resolve appherder binary: %w", err) 19 + } 20 + if bin, err = filepath.EvalSymlinks(bin); err != nil { 21 + return "", fmt.Errorf("resolve appherder binary: %w", err) 22 + } 23 + return bin, nil 24 + } 25 + 26 + // writeUnitFiles renders the named templates with the appherder binary path 27 + // and writes them to the systemd user directory. 28 + func writeUnitFiles(names []string) error { 29 + bin, err := binaryPath() 30 + if err != nil { 31 + return err 32 + } 33 + userDir, err := systemdUserDir() 34 + if err != nil { 35 + return err 36 + } 37 + if err := os.MkdirAll(userDir, 0o755); err != nil { 38 + return fmt.Errorf("create systemd user directory: %w", err) 39 + } 40 + for _, name := range names { 41 + data, err := unitTemplates.ReadFile("systemd/" + name) 42 + if err != nil { 43 + return fmt.Errorf("read %s: %w", name, err) 44 + } 45 + rendered := strings.ReplaceAll(string(data), "{{BIN}}", bin) 46 + dest := filepath.Join(userDir, name) 47 + if err := os.WriteFile(dest, []byte(rendered), 0o644); err != nil { 48 + return fmt.Errorf("write %s: %w", dest, err) 49 + } 50 + } 51 + return nil 52 + } 53 + 54 + func removeUnitFiles(names []string) error { 55 + userDir, err := systemdUserDir() 56 + if err != nil { 57 + return err 58 + } 59 + for _, name := range names { 60 + _ = os.Remove(filepath.Join(userDir, name)) 61 + } 62 + return nil 63 + } 64 + 65 + func systemdUserDir() (string, error) { 66 + home, err := os.UserHomeDir() 67 + if err != nil { 68 + return "", fmt.Errorf("resolve home directory: %w", err) 69 + } 70 + return filepath.Join(home, ".config", "systemd", "user"), nil 71 + } 72 + 73 + func runSystemctl(args ...string) error { 74 + cmd := exec.Command("systemctl", append([]string{"--user"}, args...)...) 75 + if out, err := cmd.CombinedOutput(); err != nil { 76 + return fmt.Errorf("systemctl %s: %w\n%s", strings.Join(args, " "), err, out) 77 + } 78 + return nil 79 + }
+6
cmd/appherder/systemd/appherder-upgrade.service
··· 1 + [Unit] 2 + Description=Download and install available AppImage updates 3 + 4 + [Service] 5 + Type=oneshot 6 + ExecStart={{BIN}} upgrade
+11
cmd/appherder/systemd/appherder-upgrade.timer
··· 1 + [Unit] 2 + Description=Check for AppImage updates daily 3 + 4 + [Timer] 5 + # Run once a day, starting from boot so laptops that don't stay on still get it. 6 + OnBootSec=5min 7 + OnUnitActiveSec=24h 8 + Persistent=true 9 + 10 + [Install] 11 + WantedBy=timers.target