Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to MageBox will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **Self-Healing Web-UI Commands** - `phpmyadmin`, `elasticvue`, and `mailpit` now start their container on demand instead of dead-ending when it is stopped (which happens routinely after a Docker Desktop or machine restart). `phpmyadmin open` and `elasticvue open` start the container when the service is enabled but stopped, then open the browser; `mailpit open` always starts it (Mailpit is always on). `phpmyadmin enable` and `elasticvue enable` are now idempotent — when already enabled but stopped, they restart the container instead of printing "already enabled" and doing nothing. Only the requested UI container is started (no full `global start`, no databases dragged in), and a clear message is shown when Docker itself is not running. `status` now points to the relevant `open` command. Shared helpers (`isContainerRunning`, `openInBrowser`, `ensureGlobalServiceRunning`, `decideServiceUI`) de-duplicate the logic across the three commands.

## [1.18.2] - 2026-06-23

### Fixed
Expand Down
134 changes: 74 additions & 60 deletions cmd/magebox/elasticvue.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"fmt"
"os/exec"
"runtime"
"strings"

"github.com/spf13/cobra"
Expand All @@ -15,12 +14,16 @@ import (
"qoliber/magebox/internal/project"
)

const elasticvueDefaultPort = "8090"
const (
elasticvueDefaultPort = "8090"
elasticvueContainer = "magebox-elasticvue"
elasticvueService = "elasticvue"
)

// getElasticvueURL returns the Elasticvue URL, reading the actual port from the running container.
// Falls back to the default port if the container is not running.
func getElasticvueURL() string {
portCmd := exec.Command("docker", "port", "magebox-elasticvue", "8080")
portCmd := exec.Command("docker", "port", elasticvueContainer, "8080")
output, err := portCmd.Output()
if err == nil {
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
Expand Down Expand Up @@ -64,7 +67,7 @@ var elasticvueStatusCmd = &cobra.Command{
var elasticvueOpenCmd = &cobra.Command{
Use: "open",
Short: "Open Elasticvue in browser",
Long: "Opens the Elasticvue web UI in the default browser",
Long: "Opens the Elasticvue web UI in the default browser, starting it if needed",
RunE: runElasticvueOpen,
}

Expand All @@ -87,48 +90,55 @@ func runElasticvueEnable(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to load global config: %w", err)
}

if globalCfg.Elasticvue {
cli.PrintInfo("Elasticvue is already enabled")
switch decideServiceUI(globalCfg.Elasticvue, isContainerRunning(elasticvueContainer)) {
case decisionProceed:
cli.PrintInfo("Elasticvue is already enabled and running")
fmt.Println()
fmt.Printf(" Web UI: %s\n", cli.Highlight(getElasticvueURL()))
return nil
}

cli.PrintTitle("Enabling Elasticvue")
fmt.Println()
case decisionStart:
cli.PrintTitle("Starting Elasticvue")
fmt.Println()
fmt.Print("Elasticvue is enabled but stopped, starting... ")
if err := ensureGlobalServiceRunning(p, elasticvueService); err != nil {
fmt.Println(cli.Error("failed"))
cli.PrintError("%v", err)
return nil
}
fmt.Println(cli.Success("done"))
fmt.Println()
cli.PrintSuccess("Elasticvue started!")
fmt.Println()
fmt.Printf(" Web UI: %s\n", cli.Highlight(getElasticvueURL()))
return nil

globalCfg.Elasticvue = true
if err := config.SaveGlobalConfig(p.HomeDir, globalCfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
default: // decisionNotEnabled
cli.PrintTitle("Enabling Elasticvue")
fmt.Println()

// Regenerate docker-compose and start
fmt.Print("Starting Elasticvue container... ")
composeGen := docker.NewComposeGenerator(p)
globalCfg.Elasticvue = true
if err := config.SaveGlobalConfig(p.HomeDir, globalCfg); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

// Discover all project configs to regenerate docker-compose
configs := discoverAllConfigs(p)
if err := composeGen.GenerateGlobalServices(configs); err != nil {
fmt.Println(cli.Error("failed"))
return fmt.Errorf("failed to generate docker-compose: %w", err)
}
fmt.Print("Starting Elasticvue container... ")
if err := ensureGlobalServiceRunning(p, elasticvueService); err != nil {
fmt.Println(cli.Error("failed"))
cli.PrintError("%v", err)
return nil
}
fmt.Println(cli.Success("done"))

dockerCtrl := docker.NewDockerController(composeGen.ComposeFilePath())
if err := dockerCtrl.StartService("elasticvue"); err != nil {
fmt.Println(cli.Error("failed"))
return fmt.Errorf("failed to start Elasticvue: %w", err)
fmt.Println()
cli.PrintSuccess("Elasticvue enabled!")
fmt.Println()
fmt.Printf(" Web UI: %s\n", cli.Highlight(getElasticvueURL()))
fmt.Println()
cli.PrintInfo("Add your cluster in the Elasticvue UI using http://localhost:<port>")
cli.PrintInfo("Check 'magebox status' or the ports reference for your search engine's port")
return nil
}
fmt.Println(cli.Success("done"))

fmt.Println()
cli.PrintSuccess("Elasticvue enabled!")
fmt.Println()
fmt.Printf(" Web UI: %s\n", cli.Highlight(getElasticvueURL()))
fmt.Println()
cli.PrintInfo("Add your cluster in the Elasticvue UI using http://localhost:<port>")
cli.PrintInfo("Check 'magebox status' or the ports reference for your search engine's port")

return nil
}

func runElasticvueDisable(cmd *cobra.Command, args []string) error {
Expand All @@ -154,7 +164,7 @@ func runElasticvueDisable(cmd *cobra.Command, args []string) error {
fmt.Print("Stopping Elasticvue container... ")
composeGen := docker.NewComposeGenerator(p)
dockerCtrl := docker.NewDockerController(composeGen.ComposeFilePath())
if err := dockerCtrl.StopService("elasticvue"); err != nil {
if err := dockerCtrl.StopService(elasticvueService); err != nil {
fmt.Println(cli.Warning("not running"))
} else {
fmt.Println(cli.Success("done"))
Expand All @@ -166,8 +176,7 @@ func runElasticvueDisable(cmd *cobra.Command, args []string) error {
}

// Regenerate docker-compose without Elasticvue
configs := discoverAllConfigs(p)
if err := composeGen.GenerateGlobalServices(configs); err != nil {
if err := composeGen.GenerateGlobalServices(discoverAllConfigs(p)); err != nil {
return fmt.Errorf("failed to update docker-compose: %w", err)
}

Expand Down Expand Up @@ -200,44 +209,49 @@ func runElasticvueStatus(cmd *cobra.Command, args []string) error {

fmt.Println("Enabled: " + cli.Success("yes"))

// Check if container is running
checkCmd := exec.Command("docker", "ps", "--filter", "name=magebox-elasticvue", "--filter", "status=running", "-q")
output, err := checkCmd.Output()
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
if isContainerRunning(elasticvueContainer) {
fmt.Println("Status: " + cli.Success("running"))
fmt.Printf("Web UI: %s\n", cli.Highlight(getElasticvueURL()))
} else {
fmt.Println("Status: " + cli.Warning("stopped"))
fmt.Println()
cli.PrintInfo("Start global services with: magebox global start")
cli.PrintInfo("Start with: magebox elasticvue open")
}

return nil
}

func runElasticvueOpen(cmd *cobra.Command, args []string) error {
// Check if container is running
checkCmd := exec.Command("docker", "ps", "--filter", "name=magebox-elasticvue", "--filter", "status=running", "-q")
output, err := checkCmd.Output()
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
cli.PrintError("Elasticvue is not running")
p, err := getPlatform()
if err != nil {
return err
}

globalCfg, err := config.LoadGlobalConfig(p.HomeDir)
if err != nil {
return fmt.Errorf("failed to load global config: %w", err)
}

switch decideServiceUI(globalCfg.Elasticvue, isContainerRunning(elasticvueContainer)) {
case decisionNotEnabled:
cli.PrintError("Elasticvue is not enabled")
fmt.Println()
cli.PrintInfo("Enable with: magebox elasticvue enable")
return nil

case decisionStart:
fmt.Print("Elasticvue is not running, starting... ")
if err := ensureGlobalServiceRunning(p, elasticvueService); err != nil {
fmt.Println(cli.Error("failed"))
cli.PrintError("%v", err)
return nil
}
fmt.Println(cli.Success("done"))
}

url := getElasticvueURL()
cli.PrintInfo("Opening %s", cli.URL(url))

var openCmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
openCmd = exec.Command("open", url)
default:
openCmd = exec.Command("xdg-open", url)
}

return openCmd.Start()
return openInBrowser(url)
}

// discoverAllConfigs loads configs from all registered MageBox projects
Expand Down
49 changes: 22 additions & 27 deletions cmd/magebox/mailpit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ package main
import (
"fmt"
"os/exec"
"runtime"
"strings"

"github.com/spf13/cobra"

"qoliber/magebox/internal/cli"
)

const mailpitDefaultPort = "8025"
const (
mailpitDefaultPort = "8025"
mailpitContainer = "magebox-mailpit"
mailpitService = "mailpit"
)

// getMailpitURL returns the Mailpit URL, reading the actual port from the running container.
// Falls back to the default port if the container is not running.
func getMailpitURL() string {
portCmd := exec.Command("docker", "port", "magebox-mailpit", "8025")
portCmd := exec.Command("docker", "port", mailpitContainer, "8025")
output, err := portCmd.Output()
if err == nil {
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
Expand All @@ -39,7 +42,7 @@ var mailpitCmd = &cobra.Command{
var mailpitOpenCmd = &cobra.Command{
Use: "open",
Short: "Open Mailpit in browser",
Long: "Opens the Mailpit web UI in the default browser",
Long: "Opens the Mailpit web UI in the default browser, starting it if needed",
RunE: runMailpitOpen,
}

Expand All @@ -57,28 +60,23 @@ func init() {
}

func runMailpitOpen(cmd *cobra.Command, args []string) error {
// Check if container is running
checkCmd := exec.Command("docker", "ps", "--filter", "name=magebox-mailpit", "--filter", "status=running", "-q")
output, err := checkCmd.Output()
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
cli.PrintError("Mailpit is not running")
fmt.Println()
cli.PrintInfo("Start global services with: magebox global start")
return nil
if !isContainerRunning(mailpitContainer) {
p, err := getPlatform()
if err != nil {
return err
}
fmt.Print("Mailpit is not running, starting... ")
if err := ensureGlobalServiceRunning(p, mailpitService); err != nil {
fmt.Println(cli.Error("failed"))
cli.PrintError("%v", err)
return nil
}
fmt.Println(cli.Success("done"))
}

url := getMailpitURL()
cli.PrintInfo("Opening %s", cli.URL(url))

var openCmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
openCmd = exec.Command("open", url)
default:
openCmd = exec.Command("xdg-open", url)
}

return openCmd.Start()
return openInBrowser(url)
}

func runMailpitStatus(cmd *cobra.Command, args []string) error {
Expand All @@ -88,17 +86,14 @@ func runMailpitStatus(cmd *cobra.Command, args []string) error {
// Mailpit is always enabled
fmt.Println("Enabled: " + cli.Success("yes (always on)"))

// Check if container is running
checkCmd := exec.Command("docker", "ps", "--filter", "name=magebox-mailpit", "--filter", "status=running", "-q")
output, err := checkCmd.Output()
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
if isContainerRunning(mailpitContainer) {
fmt.Println("Status: " + cli.Success("running"))
fmt.Printf("Web UI: %s\n", cli.Highlight(getMailpitURL()))
fmt.Printf("SMTP: %s\n", cli.Highlight("localhost:1025"))
} else {
fmt.Println("Status: " + cli.Warning("stopped"))
fmt.Println()
cli.PrintInfo("Start global services with: magebox global start")
cli.PrintInfo("Start with: magebox mailpit open")
}

return nil
Expand Down
Loading
Loading