/ go / internal / cli / update.go
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  }