@@ -4,6 +4,7 @@ package main
44import (
55 "encoding/json"
66 "fmt"
7+ "io"
78 "net/url"
89 "os"
910 "os/exec"
@@ -141,6 +142,7 @@ Configuration file format:
141142 rootCmd .AddCommand (newProfilesCmd ())
142143 rootCmd .AddCommand (newCheckCmd ())
143144 rootCmd .AddCommand (newSetupCmd ())
145+ rootCmd .AddCommand (newUpdateCmd ())
144146
145147 if err := rootCmd .Execute (); err != nil {
146148 fmt .Fprintf (os .Stderr , "Error: %v\n " , err )
@@ -878,6 +880,162 @@ func runSetup(_ *cobra.Command, _ []string) error {
878880 })
879881}
880882
883+ // newUpdateCmd creates the update subcommand for updating greywall and greyproxy.
884+ func newUpdateCmd () * cobra.Command {
885+ cmd := & cobra.Command {
886+ Use : "update" ,
887+ Short : "Update greywall and greyproxy to the latest release" ,
888+ Long : `Updates greywall and greyproxy by downloading the latest release binaries.
889+
890+ The update method depends on how greywall was originally installed:
891+ - Installed via install.sh → downloads and replaces binaries automatically
892+ - Installed via Homebrew → prints the brew upgrade command to run
893+ - Installed from source → prints manual instructions
894+
895+ Examples:
896+ greywall update # update to latest stable
897+ greywall update --beta # update to latest beta` ,
898+ Args : cobra .NoArgs ,
899+ RunE : runUpdate ,
900+ }
901+ cmd .Flags ().Bool ("beta" , false , "Update to the latest beta (pre-release) version" )
902+ return cmd
903+ }
904+
905+ // installMethod represents how greywall was originally installed.
906+ type installMethod int
907+
908+ const (
909+ installMethodScript installMethod = iota // install.sh → ~/.local/bin
910+ installMethodBrew // Homebrew
911+ installMethodSource // built from source / unknown
912+ )
913+
914+ // detectInstallMethod returns how the currently running greywall was installed.
915+ func detectInstallMethod (selfPath string ) installMethod {
916+ if proxy .IsBrewManaged (selfPath ) {
917+ return installMethodBrew
918+ }
919+ home , err := os .UserHomeDir ()
920+ if err == nil {
921+ localBin := filepath .Join (home , ".local" , "bin" )
922+ if filepath .Dir (selfPath ) == localBin {
923+ return installMethodScript
924+ }
925+ }
926+ return installMethodSource
927+ }
928+
929+ func runUpdate (cmd * cobra.Command , _ []string ) error {
930+ beta , _ := cmd .Flags ().GetBool ("beta" )
931+
932+ // Fetch latest tags for both tools
933+ greyproxyTag , err := proxy .CheckLatestTag (beta )
934+ if err != nil {
935+ return fmt .Errorf ("failed to fetch latest greyproxy tag: %w" , err )
936+ }
937+ greywallTag , err := proxy .CheckLatestTagFor ("GreyhavenHQ" , "greywall" , beta )
938+ if err != nil {
939+ return fmt .Errorf ("failed to fetch latest greywall tag: %w" , err )
940+ }
941+
942+ channel := "stable"
943+ if beta {
944+ channel = "beta"
945+ }
946+ fmt .Printf ("Latest %s: greywall %s, greyproxy %s\n \n " , channel , greywallTag , greyproxyTag )
947+
948+ // Detect how greywall was installed (resolve symlinks first)
949+ selfPath , err := os .Executable ()
950+ if err != nil {
951+ selfPath = ""
952+ } else if resolved , err := filepath .EvalSymlinks (selfPath ); err == nil {
953+ selfPath = resolved
954+ }
955+
956+ switch detectInstallMethod (selfPath ) {
957+ case installMethodBrew :
958+ fmt .Printf ("greywall is managed by Homebrew. To update both tools, run:\n " )
959+ fmt .Printf (" brew upgrade greywall greyproxy\n " )
960+ return nil
961+
962+ case installMethodSource :
963+ fmt .Printf ("greywall was installed from source. To update manually:\n \n " )
964+ fmt .Printf (" greywall:\n " )
965+ fmt .Printf (" git clone --branch %s https://github.com/GreyhavenHQ/greywall.git\n " , greywallTag )
966+ fmt .Printf (" cd greywall && make build && cp greywall ~/.local/bin/\n \n " )
967+ fmt .Printf (" greyproxy:\n " )
968+ fmt .Printf (" git clone --branch %s https://github.com/GreyhavenHQ/greyproxy.git\n " , greyproxyTag )
969+ fmt .Printf (" cd greyproxy && go build -o greyproxy ./cmd/greyproxy && greyproxy install --force\n " )
970+ return nil // exit 0 — not broken, just can't automate
971+
972+ default : // installMethodScript
973+ // Update greyproxy via binary download
974+ fmt .Println ("==> Updating greyproxy..." )
975+ if err := proxy .Install (proxy.InstallOptions {
976+ Output : os .Stderr ,
977+ Tag : greyproxyTag ,
978+ }); err != nil {
979+ return fmt .Errorf ("failed to update greyproxy: %w" , err )
980+ }
981+
982+ // Update greywall via binary download
983+ fmt .Println ("\n ==> Updating greywall..." )
984+ if err := updateSelf (greywallTag , selfPath , os .Stderr ); err != nil {
985+ return fmt .Errorf ("failed to update greywall: %w" , err )
986+ }
987+ fmt .Printf ("\n greywall and greyproxy updated to %s channel.\n " , channel )
988+ return nil
989+ }
990+ }
991+
992+ // updateSelf downloads the greywall release binary and replaces the running binary at execPath.
993+ func updateSelf (tag , execPath string , output io.Writer ) error {
994+ _ , _ = fmt .Fprintf (output , "Downloading greywall %s...\n " , tag )
995+
996+ binPath , cleanup , err := proxy .DownloadGreywallBinary (tag )
997+ if err != nil {
998+ return err
999+ }
1000+ defer cleanup ()
1001+
1002+ _ , _ = fmt .Fprintf (output , "Replacing %s...\n " , execPath )
1003+ if err := os .Rename (binPath , execPath ); err != nil {
1004+ // Rename across filesystems fails; copy instead
1005+ if err2 := copyFileTo (binPath , execPath ); err2 != nil {
1006+ return fmt .Errorf ("failed to replace binary: %w (copy also failed: %v)" , err , err2 )
1007+ }
1008+ }
1009+
1010+ _ , _ = fmt .Fprintf (output , "greywall updated to %s\n " , tag )
1011+ return nil
1012+ }
1013+
1014+ // copyFileTo copies src to dst atomically (write to temp, then rename).
1015+ func copyFileTo (src , dst string ) error {
1016+ in , err := os .Open (src ) //nolint:gosec // src is a path we control
1017+ if err != nil {
1018+ return err
1019+ }
1020+ defer func () { _ = in .Close () }()
1021+
1022+ tmpDst := dst + ".new"
1023+ out , err := os .OpenFile (tmpDst , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0o755 ) //nolint:gosec
1024+ if err != nil {
1025+ return err
1026+ }
1027+ defer func () { _ = os .Remove (tmpDst ) }()
1028+
1029+ if _ , err := io .Copy (out , in ); err != nil {
1030+ _ = out .Close ()
1031+ return err
1032+ }
1033+ if err := out .Close (); err != nil {
1034+ return err
1035+ }
1036+ return os .Rename (tmpDst , dst )
1037+ }
1038+
8811039// newCompletionCmd creates the completion subcommand for shell completions.
8821040func newCompletionCmd (rootCmd * cobra.Command ) * cobra.Command {
8831041 cmd := & cobra.Command {
0 commit comments