A shepherd for your Appimages.
0

Configure Feed

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

support install from url

Aly Raffauf (Jun 17, 2026, 11:23 PM EDT) 18eb007f c2581160

+87 -6
+3 -1
README.md
··· 11 11 ## Features 12 12 13 13 - **Shows up like a real app.** Lands in your application menu with its real name and icon. 14 + - **Installs from a file or a URL.** Point it at a local AppImage or paste a download link — appherder fetches and installs it. 14 15 - **Uninstalls cleanly.** Remove an app and its launcher and icon go with it. No leftovers. 15 16 - **Upgrades replace instead of piling up.** appherder names an app by what's inside it, not the download's filename, so a newer version of `Foo` just replaces the old one. 16 17 - **Checks for updates.** Reads the update info baked into each AppImage and fetches the latest from GitHub, GitLab, zsync, or a static URL. `appherder upgrade --check` shows what's new; `appherder upgrade` downloads and installs it. ··· 50 51 ## Usage 51 52 52 53 ```bash 53 - appherder install ~/Downloads/Foo-x86_64.AppImage # install one 54 + appherder install ~/Downloads/Foo-x86_64.AppImage # install from a file 55 + appherder install https://example.com/Foo.AppImage # or download and install 54 56 appherder uninstall foo # remove one 55 57 appherder list # see what's installed 56 58 appherder sync # match your apps to what's in ~/AppImages
+21 -5
cmd/appherder/cli.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 + "net/url" 6 7 7 8 "github.com/alyraffauf/appherder/internal/appherder" 8 9 "github.com/spf13/cobra" ··· 33 34 34 35 func newInstallCommand(a appherder.App) *cobra.Command { 35 36 return &cobra.Command{ 36 - Use: "install APPIMAGE", 37 - Short: "Install an AppImage", 38 - Long: "Copies an AppImage into ~/AppImages and creates a launcher for it with the app's\nreal name and icon. You can delete the original download afterward.", 39 - Args: cobra.ExactArgs(1), 37 + Use: "install APPIMAGE|URL", 38 + Short: "Install an AppImage from a file or URL", 39 + Long: "Copies an AppImage into ~/AppImages and creates a launcher for it with the app's\n" + 40 + "real name and icon. You can delete the original download afterward.\n\n" + 41 + "Accepts a local file path or an HTTP/HTTPS URL to download directly.", 42 + Args: cobra.ExactArgs(1), 40 43 RunE: func(cmd *cobra.Command, args []string) error { 41 - name, err := a.Install(args[0]) 44 + arg := args[0] 45 + var name string 46 + var err error 47 + if isURL(arg) { 48 + fmt.Fprintf(cmd.OutOrStdout(), "downloading %s...\n", arg) 49 + name, err = a.InstallFromURL(cmd.Context(), arg) 50 + } else { 51 + name, err = a.Install(arg) 52 + } 42 53 if err != nil { 43 54 return err 44 55 } ··· 46 57 return nil 47 58 }, 48 59 } 60 + } 61 + 62 + func isURL(s string) bool { 63 + parsed, err := url.Parse(s) 64 + return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" 49 65 } 50 66 51 67 func newUninstallCommand(a appherder.App) *cobra.Command {
+21
internal/appherder/install.go
··· 1 1 package appherder 2 2 3 3 import ( 4 + "context" 4 5 "fmt" 5 6 "os" 6 7 "path/filepath" ··· 76 77 } 77 78 78 79 return appName, nil 80 + } 81 + 82 + // InstallFromURL downloads an AppImage from url and installs it. 83 + func (a App) InstallFromURL(ctx context.Context, url string) (string, error) { 84 + tmp, err := os.CreateTemp("", "appherder-install-*.appimage") 85 + if err != nil { 86 + return "", fmt.Errorf("create temporary file: %w", err) 87 + } 88 + tmpName := tmp.Name() 89 + defer os.Remove(tmpName) 90 + 91 + if err := download(ctx, url, tmp); err != nil { 92 + tmp.Close() 93 + return "", err 94 + } 95 + if err := tmp.Close(); err != nil { 96 + return "", fmt.Errorf("close download: %w", err) 97 + } 98 + 99 + return a.Install(tmpName) 79 100 } 80 101 81 102 // deriveAppName picks the canonical install name, matching GearLever so the
+42
internal/appherder/install_url_test.go
··· 1 + package appherder 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + "net/http/httptest" 7 + "os" 8 + "path/filepath" 9 + "testing" 10 + ) 11 + 12 + func TestInstallFromURLDownloadsAndCleansUp(t *testing.T) { 13 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 + w.Write([]byte("not an appimage")) 15 + })) 16 + defer srv.Close() 17 + 18 + home := t.TempDir() 19 + a := App{homeDir: func() (string, error) { return home, nil }} 20 + _, err := a.InstallFromURL(context.Background(), srv.URL+"/Foo.AppImage") 21 + if err == nil { 22 + t.Fatal("expected error for non-AppImage content") 23 + } 24 + 25 + matches, _ := filepath.Glob(filepath.Join(os.TempDir(), "appherder-install-*.appimage")) 26 + if len(matches) > 0 { 27 + t.Fatalf("temp files left behind: %v", matches) 28 + } 29 + } 30 + 31 + func TestInstallFromURLHandlesDownloadFailure(t *testing.T) { 32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 + w.WriteHeader(http.StatusNotFound) 34 + })) 35 + defer srv.Close() 36 + 37 + home := t.TempDir() 38 + a := App{homeDir: func() (string, error) { return home, nil }} 39 + if _, err := a.InstallFromURL(context.Background(), srv.URL+"/Foo.AppImage"); err == nil { 40 + t.Fatal("expected error for 404 download") 41 + } 42 + }