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 ", 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 ", 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 ", 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 ", 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 ", 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 ", 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) }