diff --git a/cmd/restart.go b/cmd/restart.go index 19726b5..6bccf35 100644 --- a/cmd/restart.go +++ b/cmd/restart.go @@ -1,26 +1,22 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" ) var restartCmd = &cobra.Command{ - Use: "restart ", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("restart called") - }, + Use: "restart", + Short: "Restart Umono", + Long: `Restart the Umono application in the current directory. Equivalent to running 'down' followed by 'up'.`, + Run: runRestart, } func init() { + restartCmd.Flags().BoolVarP(&detach, "detach", "d", false, "Run in background") rootCmd.AddCommand(restartCmd) } + +func runRestart(cmd *cobra.Command, args []string) { + runDown(cmd, args) + runUp(cmd, args) +} diff --git a/cmd/status.go b/cmd/status.go index 885ce10..ee5d466 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,26 +1,123 @@ package cmd import ( + "bufio" "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" "github.com/spf13/cobra" ) var statusCmd = &cobra.Command{ - Use: "status ", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("status called") - }, + Use: "status", + Short: "Show Umono status", + Long: `Show the running status of Umono application in the current directory.`, + Run: runStatus, } func init() { rootCmd.AddCommand(statusCmd) } + +func runStatus(cmd *cobra.Command, args []string) { + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err) + os.Exit(1) + } + + umonoPath := filepath.Join(cwd, "umono") + umonoExists := true + if _, err := os.Stat(umonoPath); os.IsNotExist(err) { + umonoExists = false + } + + if !umonoExists { + fmt.Println("⚠️ Not an Umono project (no umono executable found)") + return + } + + port := readPortFromEnv(cwd) + + pidPath := filepath.Join(cwd, ".PID") + pidData, err := os.ReadFile(pidPath) + + if os.IsNotExist(err) { + fmt.Println("⏹️ Umono is stopped") + if port != "" { + fmt.Printf(" Port: %s\n", port) + } + return + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to read .PID file: %v\n", err) + os.Exit(1) + } + + pidStr := strings.TrimSpace(string(pidData)) + pid, err := strconv.Atoi(pidStr) + if err != nil { + fmt.Println("⚠️ Invalid .PID file") + return + } + + process, err := os.FindProcess(pid) + if err != nil { + fmt.Println("⏹️ Umono is stopped (stale .PID file)") + if port != "" { + fmt.Printf(" Port: %s\n", port) + } + return + } + + if err := process.Signal(syscall.Signal(0)); err != nil { + fmt.Println("⏹️ Umono is stopped (stale .PID file)") + if port != "" { + fmt.Printf(" Port: %s\n", port) + } + return + } + + fmt.Printf("✅ Umono is running (PID: %d)\n", pid) + if port != "" { + fmt.Printf(" Port: %s\n", port) + fmt.Printf(" URL: http://localhost:%s\n", port) + } +} + +func readPortFromEnv(dir string) string { + envPath := filepath.Join(dir, ".env") + file, err := os.Open(envPath) + if err != nil { + return "" + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + if key == "PORT" { + return value + } + } + + return "" +} diff --git a/cmd/upgrade.go b/cmd/upgrade.go index fceefd8..8d919fd 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -2,25 +2,50 @@ package cmd import ( "fmt" + "os" "github.com/spf13/cobra" + "github.com/umono-cms/cli/internal/project" ) var upgradeCmd = &cobra.Command{ - Use: "upgrade ", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("upgrade called") - }, + Use: "upgrade", + Short: "Upgrade Umono to the latest version", + Long: `Upgrade the current Umono installation to the latest release. + +This command will: + - Check for the latest Umono release + - Download the new binary for your platform + - Replace the existing binary while preserving your data + +Your database (umono.db) and configuration (.env) will be preserved. + +Example: + cd my-project + umono upgrade`, + Run: runUpgrade, } func init() { rootCmd.AddCommand(upgradeCmd) } + +func runUpgrade(cmd *cobra.Command, args []string) { + wd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err) + os.Exit(1) + } + + fmt.Println("🔄 Checking for updates...") + + err = project.Upgrade(wd) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + // TODO: Add feature that up to date. + + fmt.Println("✅ Upgrade completed successfully!") +} diff --git a/cmd/version.go b/cmd/version.go index e8700fc..ad3b39a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -6,20 +6,18 @@ import ( "github.com/spf13/cobra" ) +const version = "v0.1.0" + var versionCmd = &cobra.Command{ Use: "version", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("version called") - }, + Short: "Print the version number", + Run: runVersion, } func init() { rootCmd.AddCommand(versionCmd) } + +func runVersion(cmd *cobra.Command, args []string) { + fmt.Println(version) +} diff --git a/internal/project/project.go b/internal/project/project.go index cbe023b..f7c5077 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -3,6 +3,8 @@ package project import ( "encoding/base64" "fmt" + "io" + "os" "path/filepath" "github.com/spf13/cobra" @@ -61,6 +63,86 @@ func Create(cmd *cobra.Command, project Project) error { return nil } +func Upgrade(projectPath string) error { + binaryPath := findBinaryPath(projectPath) + if binaryPath == "" { + return fmt.Errorf("no Umono binary found in %s", projectPath) + } + + client := download.NewClient() + + releaseInfo, err := client.GetLatestRelease() + if err != nil { + return fmt.Errorf("failed to fetch release: %w", err) + } + + fmt.Printf("📦 Latest version: %s\n", releaseInfo.Version) + + tmpDir, err := os.MkdirTemp("", "umono-upgrade-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err := client.DownloadAndExtract(releaseInfo, tmpDir); err != nil { + return err + } + + newBinaryPath := findBinaryPath(tmpDir) + if newBinaryPath == "" { + return fmt.Errorf("no binary found in downloaded release") + } + + backupPath := binaryPath + ".backup" + if err := os.Rename(binaryPath, backupPath); err != nil { + return fmt.Errorf("failed to backup existing binary: %w", err) + } + + if err := copyFile(newBinaryPath, binaryPath); err != nil { + os.Rename(backupPath, binaryPath) + return fmt.Errorf("failed to install new binary: %w", err) + } + + os.Remove(backupPath) + + return nil +} + +func findBinaryPath(dir string) string { + candidates := []string{"umono", "umono-darwin-amd64", "umono-darwin-arm64", "umono-linux-amd64", "umono-linux-arm64"} + + for _, name := range candidates { + path := filepath.Join(dir, name) + if info, err := os.Stat(path); err == nil && !info.IsDir() { + return path + } + } + + return "" +} + +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + sourceInfo, err := sourceFile.Stat() + if err != nil { + return err + } + + destFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, sourceInfo.Mode()) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} + func hashData(data string) (string, error) { hashedData, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost) if err != nil {