302 lines
8.6 KiB
Go
302 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/platform"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/run"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/tui"
|
|
)
|
|
|
|
var (
|
|
flagDemo bool
|
|
flagPR int
|
|
flagEnv []string
|
|
flagDetach bool
|
|
flagBuildPR int
|
|
runMain = tui.RunDev
|
|
)
|
|
|
|
func main() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "homarr",
|
|
Short: "Launch and manage Homarr Docker instances",
|
|
SilenceUsage: true,
|
|
Long: `Launch and manage Homarr Docker containers.
|
|
|
|
Start a local image, a PR test build from ghcr.io, browse open PRs,
|
|
or manage running instances from an interactive dashboard.
|
|
|
|
Examples:
|
|
homarr run dev # launch local homarr:dev image
|
|
homarr run --pr 6390 # launch remote PR test image
|
|
homarr run --pr 6390 --demo # PR + demo mode
|
|
homarr run -e FOO=bar dev # extra env vars
|
|
homarr run --detach dev # start in background
|
|
homarr dash # interactive instance dashboard
|
|
homarr dev # browse remote PRs and local images
|
|
homarr build feature # build this checkout as homarr:feature
|
|
homarr build --pr 6390 # build PR #6390 locally as homarr:pr-6390
|
|
homarr list # script-friendly instance list
|
|
homarr logs homarr_pr_6390 # follow logs
|
|
homarr doctor # check Docker and optional integrations`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runMain()
|
|
},
|
|
}
|
|
|
|
var dashCmd = &cobra.Command{
|
|
Use: "dash",
|
|
Short: "Interactive dashboard of running instances",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return tui.RunDashboard()
|
|
},
|
|
}
|
|
|
|
var runCmd = &cobra.Command{
|
|
Use: "run [tag]",
|
|
Short: "Launch a local image or remote pull-request image",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runLaunch,
|
|
}
|
|
|
|
var devCmd = &cobra.Command{
|
|
Use: "dev",
|
|
Short: "Browse remote PRs and local development images",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return tui.RunDev()
|
|
},
|
|
}
|
|
|
|
var buildCmd = &cobra.Command{
|
|
Use: "build [name]",
|
|
Short: "Build a local Homarr image from this checkout or a pull request",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
tag := ""
|
|
if len(args) > 0 {
|
|
tag = args[0]
|
|
}
|
|
if flagBuildPR < 0 {
|
|
return fmt.Errorf("PR number must be positive")
|
|
}
|
|
if flagBuildPR > 0 {
|
|
if tag == "" {
|
|
tag = fmt.Sprintf("pr-%d", flagBuildPR)
|
|
}
|
|
return run.BuildPRImage(flagBuildPR, tag)
|
|
}
|
|
if tag == "" {
|
|
return fmt.Errorf("image name is required when --pr is not set")
|
|
}
|
|
return run.BuildCurrentImage(tag, ".")
|
|
},
|
|
}
|
|
|
|
var rebuildCmd = &cobra.Command{
|
|
Use: "rebuild <name>",
|
|
Short: "Rebuild a local image from its recorded checkout or pull request",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
images, err := docker.ListLocalImages(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, image := range images {
|
|
if image.Tag == args[0] || image.Reference() == args[0] {
|
|
return run.RebuildImage(image)
|
|
}
|
|
}
|
|
return fmt.Errorf("local image homarr:%s was not found", strings.TrimPrefix(args[0], "homarr:"))
|
|
},
|
|
}
|
|
|
|
var logsCmd = &cobra.Command{
|
|
Use: "logs <container>",
|
|
Short: "Follow logs of a running instance",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return docker.FollowLogs(args[0])
|
|
},
|
|
}
|
|
|
|
var stopCmd = &cobra.Command{
|
|
Use: "stop <container>",
|
|
Short: "Stop a running instance",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return docker.Stop(args[0])
|
|
},
|
|
}
|
|
|
|
var restartCmd = &cobra.Command{
|
|
Use: "restart <container>",
|
|
Short: "Restart a running instance",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return docker.Restart(args[0])
|
|
},
|
|
}
|
|
|
|
var removeCmd = &cobra.Command{
|
|
Use: "remove <container>",
|
|
Short: "Force-remove an instance",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return docker.Remove(args[0])
|
|
},
|
|
}
|
|
|
|
var openCmd = &cobra.Command{
|
|
Use: "open <container>",
|
|
Short: "Open a running instance in the browser",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
containers, err := docker.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, container := range containers {
|
|
if container.Name != args[0] {
|
|
continue
|
|
}
|
|
if !container.Running() {
|
|
return fmt.Errorf("container %q is not running", args[0])
|
|
}
|
|
port := container.HostPort()
|
|
if port == "" {
|
|
return fmt.Errorf("container %q does not publish the Homarr port", args[0])
|
|
}
|
|
return platform.OpenURL("http://localhost:" + port)
|
|
}
|
|
return fmt.Errorf("container %q was not found", args[0])
|
|
},
|
|
}
|
|
|
|
var listCmd = &cobra.Command{
|
|
Use: "list",
|
|
Aliases: []string{"ls"},
|
|
Short: "List Homarr instances without opening the dashboard",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
containers, err := docker.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(containers) == 0 {
|
|
fmt.Println("No Homarr instances found.")
|
|
return nil
|
|
}
|
|
fmt.Printf("%-28s %-10s %-7s %s\n", "NAME", "STATE", "PORT", "IMAGE")
|
|
for _, container := range containers {
|
|
fmt.Printf("%-28s %-10s %-7s %s\n", container.Name, container.State, container.HostPort(), container.Image)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorCmd = &cobra.Command{
|
|
Use: "doctor",
|
|
Short: "Check Docker and optional developer CLI integrations",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
failed := false
|
|
output, err := exec.Command("docker", "info", "--format", "{{.ServerVersion}}").CombinedOutput()
|
|
if err != nil {
|
|
failed = true
|
|
fmt.Printf("✗ Docker daemon: %s\n", strings.TrimSpace(string(output)))
|
|
} else {
|
|
fmt.Printf("✓ Docker daemon: %s\n", strings.TrimSpace(string(output)))
|
|
}
|
|
|
|
if _, err := exec.LookPath("gh"); err != nil {
|
|
fmt.Println("- GitHub CLI not found (optional; required for pull-request features)")
|
|
} else if output, err := exec.Command("gh", "auth", "status").CombinedOutput(); err != nil {
|
|
fmt.Printf("- GitHub CLI is not authenticated (optional; run `gh auth login` for pull-request features): %s\n", strings.TrimSpace(string(output)))
|
|
} else {
|
|
fmt.Println("✓ GitHub CLI authentication")
|
|
}
|
|
|
|
if _, err := exec.LookPath("homarr"); err != nil {
|
|
fmt.Println("- homarr is not on PATH (optional; use `pnpm dev:cli -- ...` or run `pnpm dev:cli:install`)")
|
|
} else {
|
|
fmt.Println("✓ homarr is on PATH")
|
|
}
|
|
if failed {
|
|
return fmt.Errorf("one or more checks failed")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
runCmd.Flags().BoolVarP(&flagDemo, "demo", "m", false, "Demo mode (mock integrations)")
|
|
runCmd.Flags().IntVarP(&flagPR, "pr", "p", 0, "Launch PR test image from ghcr.io")
|
|
runCmd.Flags().StringArrayVarP(&flagEnv, "env", "e", nil, "Extra env vars (repeatable, KEY=VALUE)")
|
|
runCmd.Flags().BoolVar(&flagDetach, "detach", false, "Start in the background")
|
|
buildCmd.Flags().IntVarP(&flagBuildPR, "pr", "p", 0, "Build a pull request in a temporary checkout")
|
|
|
|
rootCmd.AddCommand(runCmd, dashCmd, devCmd, buildCmd, rebuildCmd, listCmd, logsCmd, stopCmd, restartCmd, removeCmd, openCmd, doctorCmd)
|
|
}
|
|
|
|
func runLaunch(cmd *cobra.Command, args []string) error {
|
|
tag := ""
|
|
if len(args) > 0 {
|
|
tag = args[0]
|
|
}
|
|
if flagPR < 0 {
|
|
return fmt.Errorf("PR number must be positive")
|
|
}
|
|
if flagPR > 0 && tag != "" {
|
|
return fmt.Errorf("tag and --pr cannot be used together")
|
|
}
|
|
if flagPR == 0 && tag == "" {
|
|
if err := cmd.Usage(); err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("provide a tag or --pr")
|
|
}
|
|
|
|
plan, err := run.BuildPlan(run.Options{
|
|
Context: cmd.Context(),
|
|
PR: flagPR,
|
|
Tag: tag,
|
|
Demo: flagDemo,
|
|
Env: flagEnv,
|
|
FetchTitle: flagPR > 0,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Starting Homarr (%s)\n", plan.Label)
|
|
fmt.Printf(" Image : %s\n", plan.Image)
|
|
fmt.Printf(" Container : %s\n", plan.Name)
|
|
fmt.Printf(" Volume : %s\n", plan.Volume)
|
|
fmt.Printf(" Port : %d:7575\n", plan.HostPort)
|
|
fmt.Printf(" URL : http://localhost:%d\n", plan.HostPort)
|
|
if plan.PRNumber > 0 {
|
|
fmt.Printf(" PR : https://github.com/homarr-labs/homarr/pull/%d\n", plan.PRNumber)
|
|
}
|
|
fmt.Println()
|
|
|
|
if flagDetach {
|
|
if err := run.StartDetached(plan); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Started in background: http://localhost:%d\n", plan.HostPort)
|
|
return nil
|
|
}
|
|
return run.StartForeground(plan)
|
|
}
|