CLI/TUI for drafting, repeating, and publishing daily standup updates as GitHub issues
github go cli golang management project tui daily
0
fork

Configure Feed

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

feat(cmd): add upgrade command and automatic update notices

+226 -3
+31 -1
cmd/root.go
··· 1 1 package cmd 2 2 3 - import "github.com/spf13/cobra" 3 + import ( 4 + "fmt" 5 + "os" 6 + 7 + "github.com/charmbracelet/lipgloss" 8 + "github.com/prefapp/pad/internal/version" 9 + "github.com/spf13/cobra" 10 + ) 4 11 5 12 func NewRootCmd(version string) *cobra.Command { 6 13 rootCmd := &cobra.Command{ ··· 9 16 SilenceUsage: true, 10 17 SilenceErrors: true, 11 18 Version: version, 19 + PersistentPostRun: func(cmd *cobra.Command, args []string) { 20 + showUpdateNotice() 21 + }, 12 22 } 13 23 14 24 rootCmd.SetVersionTemplate("{{.Version}}\n") ··· 19 29 rootCmd.AddCommand(newCreateCmd()) 20 30 rootCmd.AddCommand(newListCmd()) 21 31 rootCmd.AddCommand(newReportCmd()) 32 + rootCmd.AddCommand(newUpgradeCmd()) 22 33 23 34 return rootCmd 24 35 } 36 + 37 + func showUpdateNotice() { 38 + release, hasUpdate := version.CheckUpdate() 39 + if !hasUpdate { 40 + return 41 + } 42 + 43 + yellow := lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B")) 44 + cyan := lipgloss.NewStyle().Foreground(lipgloss.Color("#22D3EE")) 45 + 46 + fmt.Fprintln(os.Stderr) 47 + fmt.Fprintf( 48 + os.Stderr, 49 + "%s %s is available. Run %s to update.\n", 50 + yellow.Render("→"), 51 + cyan.Render(release.TagName), 52 + cyan.Render("pad upgrade"), 53 + ) 54 + }
+190
cmd/upgrade.go
··· 1 + package cmd 2 + 3 + import ( 4 + "archive/tar" 5 + "compress/gzip" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + "os" 10 + "path/filepath" 11 + "runtime" 12 + "strings" 13 + 14 + "github.com/charmbracelet/lipgloss" 15 + "github.com/prefapp/pad/internal/version" 16 + "github.com/spf13/cobra" 17 + ) 18 + 19 + func newUpgradeCmd() *cobra.Command { 20 + return &cobra.Command{ 21 + Use: "upgrade", 22 + Short: "Upgrade pad to the latest version", 23 + RunE: runUpgrade, 24 + } 25 + } 26 + 27 + func runUpgrade(cmd *cobra.Command, args []string) error { 28 + current := version.Current() 29 + 30 + fmt.Println("Checking for updates...") 31 + 32 + release, hasUpdate := version.CheckUpdate() 33 + if !hasUpdate { 34 + green := lipgloss.NewStyle().Foreground(lipgloss.Color("#22C55E")) 35 + fmt.Println(green.Render("You're already on the latest version!")) 36 + return nil 37 + } 38 + 39 + fmt.Printf("New version available: %s (current: %s)\n", release.TagName, current) 40 + fmt.Println("Downloading...") 41 + 42 + if err := downloadAndInstall(release.TagName); err != nil { 43 + return fmt.Errorf("upgrade failed: %w", err) 44 + } 45 + 46 + green := lipgloss.NewStyle().Foreground(lipgloss.Color("#22C55E")) 47 + fmt.Println(green.Render("✓ Upgrade successful! Restart pad to use the new version.")) 48 + 49 + return nil 50 + } 51 + 52 + func downloadAndInstall(tagName string) error { 53 + binPath, err := os.Executable() 54 + if err != nil { 55 + return fmt.Errorf("get executable path: %w", err) 56 + } 57 + 58 + realPath, err := filepath.EvalSymlinks(binPath) 59 + if err != nil { 60 + realPath = binPath 61 + } 62 + 63 + goos := runtime.GOOS 64 + goarch := runtime.GOARCH 65 + 66 + if goos == "darwin" { 67 + goos = "Darwin" 68 + } 69 + if goarch == "amd64" { 70 + goarch = "x86_64" 71 + } 72 + 73 + url := fmt.Sprintf( 74 + "https://github.com/prefapp/pad/releases/download/%s/pad_%s_%s.tar.gz", 75 + tagName, goos, goarch, 76 + ) 77 + 78 + tmpDir, err := os.MkdirTemp("", "pad-upgrade-*") 79 + if err != nil { 80 + return fmt.Errorf("create temp dir: %w", err) 81 + } 82 + defer os.RemoveAll(tmpDir) 83 + 84 + tarPath := filepath.Join(tmpDir, "pad.tar.gz") 85 + if err := downloadFile(url, tarPath); err != nil { 86 + return fmt.Errorf("download release: %w", err) 87 + } 88 + 89 + newBinPath := filepath.Join(tmpDir, "pad") 90 + if err := extractBinary(tarPath, newBinPath); err != nil { 91 + return fmt.Errorf("extract binary: %w", err) 92 + } 93 + 94 + backupPath := realPath + ".backup" 95 + if err := os.Rename(realPath, backupPath); err != nil { 96 + return fmt.Errorf("backup current binary: %w", err) 97 + } 98 + 99 + if err := copyFile(newBinPath, realPath); err != nil { 100 + os.Rename(backupPath, realPath) 101 + return fmt.Errorf("install new binary: %w", err) 102 + } 103 + 104 + os.Remove(backupPath) 105 + 106 + if err := os.Chmod(realPath, 0755); err != nil { 107 + return fmt.Errorf("set executable permissions: %w", err) 108 + } 109 + 110 + return nil 111 + } 112 + 113 + func downloadFile(url, dest string) error { 114 + resp, err := http.Get(url) 115 + if err != nil { 116 + return err 117 + } 118 + defer resp.Body.Close() 119 + 120 + if resp.StatusCode != http.StatusOK { 121 + return fmt.Errorf("HTTP %d", resp.StatusCode) 122 + } 123 + 124 + out, err := os.Create(dest) 125 + if err != nil { 126 + return err 127 + } 128 + defer out.Close() 129 + 130 + _, err = io.Copy(out, resp.Body) 131 + return err 132 + } 133 + 134 + func extractBinary(tarPath, dest string) error { 135 + file, err := os.Open(tarPath) 136 + if err != nil { 137 + return err 138 + } 139 + defer file.Close() 140 + 141 + gz, err := gzip.NewReader(file) 142 + if err != nil { 143 + return err 144 + } 145 + defer gz.Close() 146 + 147 + tr := tar.NewReader(gz) 148 + 149 + for { 150 + header, err := tr.Next() 151 + if err == io.EOF { 152 + break 153 + } 154 + if err != nil { 155 + return err 156 + } 157 + 158 + if header.Typeflag == tar.TypeReg && strings.Contains(header.Name, "pad") { 159 + out, err := os.Create(dest) 160 + if err != nil { 161 + return err 162 + } 163 + defer out.Close() 164 + 165 + if _, err := io.Copy(out, tr); err != nil { 166 + return err 167 + } 168 + return nil 169 + } 170 + } 171 + 172 + return fmt.Errorf("binary not found in archive") 173 + } 174 + 175 + func copyFile(src, dest string) error { 176 + source, err := os.Open(src) 177 + if err != nil { 178 + return err 179 + } 180 + defer source.Close() 181 + 182 + destination, err := os.Create(dest) 183 + if err != nil { 184 + return err 185 + } 186 + defer destination.Close() 187 + 188 + _, err = io.Copy(destination, source) 189 + return err 190 + }
+5 -2
main.go
··· 5 5 "os" 6 6 7 7 "github.com/prefapp/pad/cmd" 8 + "github.com/prefapp/pad/internal/version" 8 9 ) 9 10 10 - var version = "dev" 11 + var buildVersion = "dev" 11 12 12 13 func main() { 13 - if err := cmd.NewRootCmd(version).Execute(); err != nil { 14 + version.SetCurrent(buildVersion) 15 + 16 + if err := cmd.NewRootCmd(buildVersion).Execute(); err != nil { 14 17 fmt.Fprintln(os.Stderr, err) 15 18 os.Exit(1) 16 19 }