Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions cmd/restart.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var restartCmd = &cobra.Command{
Use: "restart <name>",
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)
}
121 changes: 109 additions & 12 deletions cmd/status.go
Original file line number Diff line number Diff line change
@@ -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 <name>",
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 ""
}
49 changes: 37 additions & 12 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>",
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!")
}
18 changes: 8 additions & 10 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
82 changes: 82 additions & 0 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package project
import (
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -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 {
Expand Down