update.go
1 package cli 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 "github.com/spf13/cobra" 13 "github.com/TransformerOS/kamaji-go/internal/style" 14 ) 15 16 var updateCmd = &cobra.Command{ 17 Use: "update", 18 Short: "Update Kamaji to latest version", 19 Long: "Update Kamaji by pulling latest changes and rebuilding", 20 RunE: runUpdate, 21 } 22 23 func init() { 24 updateCmd.Flags().BoolP("verbose", "v", false, "Show detailed output during update") 25 } 26 27 func runUpdate(cmd *cobra.Command, args []string) error { 28 verbose, _ := cmd.Flags().GetBool("verbose") 29 30 // Get source path (current working directory or find git root) 31 sourcePath, err := findGitRoot() 32 if err != nil { 33 return fmt.Errorf("failed to find git repository: %w", err) 34 } 35 36 fmt.Printf("š Kamaji source location: %s\n", sourcePath) 37 38 // Check if it's a git repository 39 if !isGitRepo(sourcePath) { 40 fmt.Printf("%s\n", style.Error("ā Error: Source directory is not a git repository")) 41 fmt.Printf(" Location: %s\n", sourcePath) 42 fmt.Printf("\nTo enable updates, install Kamaji from a git clone:\n") 43 fmt.Printf(" git clone https://github.com/TransformerOS/kamaji-go.git\n") 44 fmt.Printf(" cd kamaji-go/go\n") 45 fmt.Printf(" make build\n") 46 return fmt.Errorf("not a git repository") 47 } 48 49 fmt.Printf("%s\n", style.Info("š” Checking for updates...")) 50 51 // Check current branch 52 branch, err := getCurrentBranch(sourcePath) 53 if err != nil { 54 return fmt.Errorf("failed to get current branch: %w", err) 55 } 56 fmt.Printf("š Current branch: %s\n", branch) 57 58 // Check current commit 59 commit, err := getCurrentCommit(sourcePath) 60 if err != nil { 61 return fmt.Errorf("failed to get current commit: %w", err) 62 } 63 fmt.Printf("š Current commit: %s\n", commit) 64 65 // Fetch updates 66 fmt.Printf("\n%s\n", style.Info("š Fetching updates from remote...")) 67 if err := fetchUpdates(sourcePath, verbose); err != nil { 68 return fmt.Errorf("failed to fetch updates: %w", err) 69 } 70 71 // Check if updates are available 72 commitsBehind, err := getCommitsBehind(sourcePath, branch) 73 if err != nil { 74 return fmt.Errorf("failed to check for updates: %w", err) 75 } 76 77 if commitsBehind == 0 { 78 fmt.Printf("%s\n", style.Success("ā Already up to date!")) 79 return nil 80 } 81 82 fmt.Printf("š¦ %d update(s) available\n", commitsBehind) 83 84 // Show what will be updated 85 fmt.Printf("\n%s\n", style.Info("š Changes:")) 86 if err := showChanges(sourcePath, branch, commitsBehind); err != nil { 87 fmt.Printf("%s\n", style.Warning("ā ļø Could not show changes")) 88 } 89 90 // Ask for confirmation 91 fmt.Printf("\nā Update Kamaji?\n") 92 fmt.Printf(" Type 'yes' to continue: ") 93 94 reader := bufio.NewReader(os.Stdin) 95 response, err := reader.ReadString('\n') 96 if err != nil { 97 return fmt.Errorf("failed to read input: %w", err) 98 } 99 100 response = strings.TrimSpace(strings.ToLower(response)) 101 if response != "yes" { 102 fmt.Printf("%s\n", style.Warning("ā Update cancelled")) 103 return nil 104 } 105 106 // Pull updates 107 fmt.Printf("\n%s\n", style.Info("ā¬ļø Pulling updates...")) 108 if err := pullUpdates(sourcePath, branch, verbose); err != nil { 109 return fmt.Errorf("failed to pull updates: %w", err) 110 } 111 112 // Rebuild 113 fmt.Printf("\n%s\n", style.Info("šØ Rebuilding Kamaji...")) 114 if err := rebuildKamaji(sourcePath, verbose); err != nil { 115 return fmt.Errorf("failed to rebuild: %w", err) 116 } 117 118 fmt.Printf("\n%s\n", style.Success("ā Update complete!")) 119 fmt.Printf("š Kamaji has been updated to the latest version\n") 120 121 return nil 122 } 123 124 func findGitRoot() (string, error) { 125 // Start from current directory and walk up to find .git 126 dir, err := os.Getwd() 127 if err != nil { 128 return "", err 129 } 130 131 for { 132 gitDir := filepath.Join(dir, ".git") 133 if _, err := os.Stat(gitDir); err == nil { 134 return dir, nil 135 } 136 137 parent := filepath.Dir(dir) 138 if parent == dir { 139 break // Reached root 140 } 141 dir = parent 142 } 143 144 return "", fmt.Errorf("not in a git repository") 145 } 146 147 func isGitRepo(path string) bool { 148 gitDir := filepath.Join(path, ".git") 149 info, err := os.Stat(gitDir) 150 return err == nil && info.IsDir() 151 } 152 153 func getCurrentBranch(path string) (string, error) { 154 cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") 155 cmd.Dir = path 156 output, err := cmd.Output() 157 if err != nil { 158 return "", err 159 } 160 return strings.TrimSpace(string(output)), nil 161 } 162 163 func getCurrentCommit(path string) (string, error) { 164 cmd := exec.Command("git", "rev-parse", "--short", "HEAD") 165 cmd.Dir = path 166 output, err := cmd.Output() 167 if err != nil { 168 return "", err 169 } 170 return strings.TrimSpace(string(output)), nil 171 } 172 173 func fetchUpdates(path string, verbose bool) error { 174 cmd := exec.Command("git", "fetch", "origin") 175 cmd.Dir = path 176 177 if verbose { 178 cmd.Stdout = os.Stdout 179 cmd.Stderr = os.Stderr 180 } 181 182 return cmd.Run() 183 } 184 185 func getCommitsBehind(path, branch string) (int, error) { 186 cmd := exec.Command("git", "rev-list", fmt.Sprintf("HEAD..origin/%s", branch), "--count") 187 cmd.Dir = path 188 output, err := cmd.Output() 189 if err != nil { 190 return 0, err 191 } 192 193 count, err := strconv.Atoi(strings.TrimSpace(string(output))) 194 if err != nil { 195 return 0, err 196 } 197 198 return count, nil 199 } 200 201 func showChanges(path, branch string, commitsBehind int) error { 202 cmd := exec.Command("git", "log", fmt.Sprintf("HEAD..origin/%s", branch), "--oneline", "--no-decorate") 203 cmd.Dir = path 204 output, err := cmd.Output() 205 if err != nil { 206 return err 207 } 208 209 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 210 maxLines := 5 211 if len(lines) > maxLines { 212 lines = lines[:maxLines] 213 } 214 215 for _, line := range lines { 216 if line != "" { 217 fmt.Printf(" ⢠%s\n", line) 218 } 219 } 220 221 if commitsBehind > maxLines { 222 fmt.Printf(" ... and %d more\n", commitsBehind-maxLines) 223 } 224 225 return nil 226 } 227 228 func pullUpdates(path, branch string, verbose bool) error { 229 cmd := exec.Command("git", "pull", "origin", branch) 230 cmd.Dir = path 231 232 if verbose { 233 cmd.Stdout = os.Stdout 234 cmd.Stderr = os.Stderr 235 } 236 237 return cmd.Run() 238 } 239 240 func rebuildKamaji(path string, verbose bool) error { 241 // Look for go directory or Makefile 242 goDir := filepath.Join(path, "go") 243 if _, err := os.Stat(goDir); err == nil { 244 // Build from go directory 245 cmd := exec.Command("make", "build") 246 cmd.Dir = goDir 247 248 if verbose { 249 cmd.Stdout = os.Stdout 250 cmd.Stderr = os.Stderr 251 } 252 253 return cmd.Run() 254 } 255 256 // Try building from current directory 257 cmd := exec.Command("make", "build") 258 cmd.Dir = path 259 260 if verbose { 261 cmd.Stdout = os.Stdout 262 cmd.Stderr = os.Stderr 263 } 264 265 return cmd.Run() 266 }