@@ -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,173 @@ 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+ if beta {
936+ return fmt .Errorf ("no beta release available for greyproxy yet — try 'greywall update' for the latest stable" )
937+ }
938+ return fmt .Errorf ("failed to fetch latest greyproxy tag: %w" , err )
939+ }
940+ greywallTag , err := proxy .CheckLatestTagFor ("GreyhavenHQ" , "greywall" , beta )
941+ if err != nil {
942+ if beta {
943+ return fmt .Errorf ("no beta release available for greywall yet — try 'greywall update' for the latest stable" )
944+ }
945+ return fmt .Errorf ("failed to fetch latest greywall tag: %w" , err )
946+ }
947+
948+ channel := "stable"
949+ if beta {
950+ channel = "beta"
951+ }
952+ fmt .Printf ("Latest %s: greywall %s, greyproxy %s\n \n " , channel , greywallTag , greyproxyTag )
953+
954+ // Detect how greywall was installed (resolve symlinks first)
955+ selfPath , err := os .Executable ()
956+ if err != nil {
957+ selfPath = ""
958+ } else if resolved , err := filepath .EvalSymlinks (selfPath ); err == nil {
959+ selfPath = resolved
960+ }
961+
962+ switch detectInstallMethod (selfPath ) {
963+ case installMethodBrew :
964+ fmt .Printf ("greywall is managed by Homebrew. To update both tools, run:\n " )
965+ fmt .Printf (" brew upgrade greywall greyproxy\n " )
966+ return nil
967+
968+ case installMethodSource :
969+ fmt .Printf ("greywall was installed from source. To update manually:\n \n " )
970+ fmt .Printf (" greywall:\n " )
971+ fmt .Printf (" git clone --branch %s https://github.com/GreyhavenHQ/greywall.git\n " , greywallTag )
972+ fmt .Printf (" cd greywall && make build && cp greywall ~/.local/bin/\n \n " )
973+ fmt .Printf (" greyproxy:\n " )
974+ fmt .Printf (" git clone --branch %s https://github.com/GreyhavenHQ/greyproxy.git\n " , greyproxyTag )
975+ fmt .Printf (" cd greyproxy && go build -o greyproxy ./cmd/greyproxy && greyproxy install --force\n " )
976+ return nil // exit 0 — not broken, just can't automate
977+
978+ default : // installMethodScript
979+ // Update greyproxy via binary download
980+ fmt .Println ("==> Updating greyproxy..." )
981+ greyproxyStatus := proxy .Detect ()
982+ if ! proxy .IsOlderVersion (greyproxyStatus .Version , greyproxyTag ) {
983+ fmt .Printf ("greyproxy is already up to date (%s)\n " , greyproxyTag )
984+ } else if err := proxy .Install (proxy.InstallOptions {
985+ Output : os .Stderr ,
986+ Tag : greyproxyTag ,
987+ }); err != nil {
988+ return fmt .Errorf ("failed to update greyproxy: %w" , err )
989+ }
990+
991+ // Update greywall via binary download
992+ fmt .Println ("\n ==> Updating greywall..." )
993+ if ! proxy .IsOlderVersion (version , greywallTag ) {
994+ fmt .Printf ("greywall is already up to date (%s)\n " , greywallTag )
995+ } else if err := updateSelf (greywallTag , selfPath , os .Stderr ); err != nil {
996+ return fmt .Errorf ("failed to update greywall: %w" , err )
997+ }
998+ fmt .Printf ("\n greywall and greyproxy are up to date on %s channel.\n " , channel )
999+ return nil
1000+ }
1001+ }
1002+
1003+ // updateSelf downloads the greywall release binary and replaces the running binary at execPath.
1004+ func updateSelf (tag , execPath string , output io.Writer ) error {
1005+ _ , _ = fmt .Fprintf (output , "Downloading greywall %s...\n " , tag )
1006+
1007+ binPath , cleanup , err := proxy .DownloadGreywallBinary (tag )
1008+ if err != nil {
1009+ return err
1010+ }
1011+ defer cleanup ()
1012+
1013+ _ , _ = fmt .Fprintf (output , "Replacing %s...\n " , execPath )
1014+ if err := os .Rename (binPath , execPath ); err != nil {
1015+ // Rename across filesystems fails; copy instead
1016+ if err2 := copyFileTo (binPath , execPath ); err2 != nil {
1017+ return fmt .Errorf ("failed to replace binary: %w (copy also failed: %v)" , err , err2 )
1018+ }
1019+ }
1020+
1021+ _ , _ = fmt .Fprintf (output , "greywall updated to %s\n " , tag )
1022+ return nil
1023+ }
1024+
1025+ // copyFileTo copies src to dst atomically (write to temp, then rename).
1026+ func copyFileTo (src , dst string ) error {
1027+ in , err := os .Open (src ) //nolint:gosec // src is a path we control
1028+ if err != nil {
1029+ return err
1030+ }
1031+ defer func () { _ = in .Close () }()
1032+
1033+ tmpDst := dst + ".new"
1034+ out , err := os .OpenFile (tmpDst , os .O_CREATE | os .O_WRONLY | os .O_TRUNC , 0o755 ) //nolint:gosec
1035+ if err != nil {
1036+ return err
1037+ }
1038+ defer func () { _ = os .Remove (tmpDst ) }()
1039+
1040+ if _ , err := io .Copy (out , in ); err != nil {
1041+ _ = out .Close ()
1042+ return err
1043+ }
1044+ if err := out .Close (); err != nil {
1045+ return err
1046+ }
1047+ return os .Rename (tmpDst , dst )
1048+ }
1049+
8811050// newCompletionCmd creates the completion subcommand for shell completions.
8821051func newCompletionCmd (rootCmd * cobra.Command ) * cobra.Command {
8831052 cmd := & cobra.Command {
0 commit comments