diff --git a/.gitignore b/.gitignore index d6280be..4f14390 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ world-cli .DS_Store # Jetbrains -.idea \ No newline at end of file +.idea + +# Test +/starter-game \ No newline at end of file diff --git a/cmd/cardinal/cardinal.go b/cmd/cardinal/cardinal.go index 9e5cda7..aefa27b 100644 --- a/cmd/cardinal/cardinal.go +++ b/cmd/cardinal/cardinal.go @@ -8,9 +8,11 @@ import ( func init() { // Register subcommands - `world cardinal [subcommand]` - BaseCmd.AddCommand(createCmd, startCmd, restartCmd, purgeCmd, stopCmd) + BaseCmd.AddCommand(createCmd, startCmd, devCmd, restartCmd, purgeCmd, stopCmd) } +// BaseCmd is the base command for the cardinal subcommand +// Usage: `world cardinal` var BaseCmd = &cobra.Command{ Use: "cardinal", Short: "Manage your Cardinal game shard project", diff --git a/cmd/cardinal/create.go b/cmd/cardinal/create.go index 620e224..afdb19e 100644 --- a/cmd/cardinal/create.go +++ b/cmd/cardinal/create.go @@ -4,7 +4,6 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" - "io" "pkg.world.dev/world-cli/common/tea_cmd" "pkg.world.dev/world-cli/tea/component/steps" "pkg.world.dev/world-cli/tea/style" @@ -21,6 +20,8 @@ var CreateDeps = []tea_cmd.Dependency{ // Cobra Setup // ///////////////// +// createCmd creates a new World Engine project based on starter-game-template +// Usage: `world cardinal create [directory_name]` var createCmd = &cobra.Command{ Use: "create [directory_name]", Short: "Create a World Engine game shard from scratch", @@ -144,12 +145,7 @@ func (m WorldCreateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea_cmd.GitCloneFinishMsg: // If there is an error, log stderr then mark step as failed if msg.Err != nil { - stderrBytes, err := io.ReadAll(msg.ErrBuf) - if err != nil { - m.logs = append(m.logs, style.CrossIcon.Render()+"Error occurred while reading stderr") - } else { - m.logs = append(m.logs, style.CrossIcon.Render()+string(stderrBytes)) - } + m.logs = append(m.logs, style.CrossIcon.Render()+msg.Err.Error()) return m, m.steps.CompleteStepCmd(msg.Err) } diff --git a/cmd/cardinal/dev.go b/cmd/cardinal/dev.go index ac028e7..c081908 100644 --- a/cmd/cardinal/dev.go +++ b/cmd/cardinal/dev.go @@ -1,34 +1,143 @@ package cardinal import ( - tea "github.com/charmbracelet/bubbletea" + "fmt" + "github.com/magefile/mage/sh" "github.com/spf13/cobra" - "pkg.world.dev/world-cli/common" - "pkg.world.dev/world-cli/tea/component" + "os" + "os/exec" + "os/signal" + "pkg.world.dev/world-cli/tea/style" + "syscall" +) + +const ( + CardinalPort = "3333" + RedisPort = "6379" + WebdisPort = "7379" ) ///////////////// // Cobra Setup // ///////////////// +// devCmd runs Cardinal in development mode +// Usage: `world cardinal dev` var devCmd = &cobra.Command{ Use: "dev", Short: "TODO", Long: `TODO`, RunE: func(cmd *cobra.Command, args []string) error { - //total width/height doesn't matter here as soon as you put it into the bubbletea framework everything will resize to fit window. - lowerLeftBox := component.NewServerStatusApp() - lowerLeftBoxInfo := component.CreateBoxInfo(lowerLeftBox, 50, 30, component.WithBorder) - triLayout := component.BuildTriLayoutHorizontal(0, 0, nil, lowerLeftBoxInfo, nil) - _, _, _, err := common.RunShellCommandReturnBuffers("cd cardinal && go run .", 1024) + err := os.Chdir("cardinal") + if err != nil { + return err + } + + // Run Redis container + err = sh.Run("docker", "run", "-d", "-p", fmt.Sprintf("%s:%s", RedisPort, RedisPort), "-e", "LOCAL_REDIS=true", "--name", "cardinal-dev-redis", "redis") if err != nil { return err } - p := tea.NewProgram(triLayout, tea.WithAltScreen()) - _, err = p.Run() + + // Run Webdis container - this provides a REST wrapper around Redis + err = sh.Run("docker", "run", "-d", "-p", fmt.Sprintf("%s:%s", WebdisPort, WebdisPort), "--link", "cardinal-dev-redis:redis", "--name", "cardinal-dev-webdis", "anapsix/webdis") if err != nil { return err } - return nil + + // Run Cardinal + execCmd, err := runCardinal() + if err != nil { + return err + } + + // Create a channel to receive termination signals + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + + // Create a channel to receive errors from the command + cmdErr := make(chan error, 1) + + go func() { + err := execCmd.Wait() + cmdErr <- err + }() + + select { + case <-signalCh: + // Shutdown signal received, attempt to gracefully stop the command + errCleanup := cleanup() + if errCleanup != nil { + return errCleanup + } + + err = execCmd.Process.Signal(syscall.SIGTERM) + if err != nil { + return err + } + + return nil + + case err := <-cmdErr: + fmt.Println(err) + errCleanup := cleanup() + if errCleanup != nil { + return errCleanup + } + return nil + } }, } + +// runCardinal runs cardinal in dev mode. +// We run cardinal without docker to make it easier to debug and skip the docker image build step +func runCardinal() (*exec.Cmd, error) { + fmt.Print(style.CLIHeader("Cardinal", "Running Cardinal in dev mode"), "\n") + fmt.Println(style.BoldText.Render("Press Ctrl+C to stop")) + fmt.Println() + fmt.Println(fmt.Sprintf("Redis: localhost:%s", RedisPort)) + fmt.Println(fmt.Sprintf("Webdis: localhost:%s", WebdisPort)) + fmt.Println(fmt.Sprintf("Cardinal: localhost:%s", CardinalPort)) + fmt.Println() + + env := map[string]string{ + "REDIS_MODE": "normal", + "CARDINAL_PORT": CardinalPort, + "REDIS_ADDR": fmt.Sprintf("localhost:%s", RedisPort), + "DEPLOY_MODE": "development", + } + + cmd := exec.Command("go", "run", ".") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + for k, v := range env { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) + } + + err := cmd.Start() + if err != nil { + return cmd, err + } + + return cmd, nil +} + +// cleanup stops and removes the Redis and Webdis containers +func cleanup() error { + err := sh.Run("docker", "rm", "-f", "cardinal-dev-redis") + if err != nil { + fmt.Println("Failed to delete Redis container automatically") + fmt.Println("Please delete it manually with `docker rm -f cardinal-dev-redis`") + return err + } + + err = sh.Run("docker", "rm", "-f", "cardinal-dev-webdis") + if err != nil { + fmt.Println("Failed to delete Webdis container automatically") + fmt.Println("Please delete it manually with `docker rm -f cardinal-dev-webdis`") + return err + } + + return nil +} diff --git a/cmd/cardinal/purge.go b/cmd/cardinal/purge.go index 05d11e4..b35f12b 100644 --- a/cmd/cardinal/purge.go +++ b/cmd/cardinal/purge.go @@ -9,6 +9,8 @@ import ( // Cobra Setup // ///////////////// +// purgeCmd stops and resets the state of your Cardinal game shard +// Usage: `world cardinal purge` var purgeCmd = &cobra.Command{ Use: "purge", Short: "Stop and reset the state of your Cardinal game shard", diff --git a/cmd/cardinal/restart.go b/cmd/cardinal/restart.go index 538731b..f2355cc 100644 --- a/cmd/cardinal/restart.go +++ b/cmd/cardinal/restart.go @@ -9,6 +9,8 @@ import ( // Cobra Setup // ///////////////// +// restartCmd restarts your Cardinal game shard stack +// Usage: `world cardinal restart` var restartCmd = &cobra.Command{ Use: "restart", Short: "Restart your Cardinal game shard stack", diff --git a/cmd/cardinal/start.go b/cmd/cardinal/start.go index bda3641..8af79e5 100644 --- a/cmd/cardinal/start.go +++ b/cmd/cardinal/start.go @@ -16,6 +16,8 @@ func init() { startCmd.Flags().String("mode", "", "Run with special mode [detach/integration-test]") } +// startCmd starts your Cardinal game shard stack +// Usage: `world cardinal start` var startCmd = &cobra.Command{ Use: "start", Short: "Start your Cardinal game shard stack", diff --git a/cmd/cardinal/stop.go b/cmd/cardinal/stop.go index 387d216..2d11afe 100644 --- a/cmd/cardinal/stop.go +++ b/cmd/cardinal/stop.go @@ -9,6 +9,8 @@ import ( // Cobra Setup // ///////////////// +// stopCmd stops your Cardinal game shard stack +// Usage: `world cardinal stop` var stopCmd = &cobra.Command{ Use: "stop", Short: "Stop your Cardinal game shard stack", diff --git a/cmd/doctor.go b/cmd/doctor.go index 6165448..bebd48a 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -19,6 +19,8 @@ var DoctorDeps = []tea_cmd.Dependency{ // Cobra Setup // ///////////////// +// doctorCmd checks that required dependencies are installed +// Usage: `world doctor` var doctorCmd = &cobra.Command{ Use: "doctor", Short: "Check that required dependencies are installed", diff --git a/cmd/root.go b/cmd/root.go index 1044063..e5beb90 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,8 @@ func init() { rootCmd.AddCommand(cardinal.BaseCmd) } -// rootCmd represents the base command when called without any subcommands +// rootCmd represents the base command +// Usage: `world` var rootCmd = &cobra.Command{ Use: "world", Short: "A swiss army knife for World Engine projects", @@ -35,6 +36,5 @@ var rootCmd = &cobra.Command{ func Execute() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) if err := rootCmd.Execute(); err != nil { - log.Fatal().Err(err).Msg("Failed to execute root command") } } diff --git a/cmd/version.go b/cmd/version.go index 9564477..f9d770f 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -7,6 +7,8 @@ import ( "github.com/spf13/cobra" ) +// versionCmd print the version number of World CLI +// Usage: `world version` var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of World CLI", diff --git a/common/tea_cmd/docker.go b/common/tea_cmd/docker.go index 622bab4..c02b086 100644 --- a/common/tea_cmd/docker.go +++ b/common/tea_cmd/docker.go @@ -86,10 +86,10 @@ func DockerCmd(action DockerCmdArgs) tea.Cmd { // DockerBuild builds all docker images func DockerBuild() error { - if err := prepareDirs("cardinal", "nakama"); err != nil { + if err := prepareDirs("cardinal"); err != nil { return err } - if err := sh.RunV("docker", "compose", "build"); err != nil { + if err := sh.Run("docker", "compose", "build"); err != nil { return err } return nil @@ -100,7 +100,7 @@ func DockerStart(build bool, services []DockerService) error { if services == nil { return fmt.Errorf("no service names provided") } - if err := prepareDirs("cardinal", "nakama"); err != nil { + if err := prepareDirs("cardinal"); err != nil { return err } if build { @@ -120,10 +120,10 @@ func DockerStartTest() error { if err := DockerPurge(); err != nil { return err } - if err := prepareDirs("testsuite", "cardinal", "nakama"); err != nil { + if err := prepareDirs("testsuite", "cardinal"); err != nil { return err } - if err := sh.RunV("docker", "compose", "up", "--build", "--abort-on-container-exit", "--exit-code-from", "testsuite", "--attach", "testsuite"); err != nil { + if err := sh.Run("docker", "compose", "up", "--build", "--abort-on-container-exit", "--exit-code-from", "testsuite", "--attach", "testsuite"); err != nil { return err } return nil @@ -132,10 +132,10 @@ func DockerStartTest() error { // DockerStartDebug starts Nakama and Cardinal in debug mode with Cardinal debugger listening on port 40000 // Note: Cardinal server will not run until a debugger is attached port 40000 func DockerStartDebug() error { - if err := prepareDirs("cardinal", "nakama"); err != nil { + if err := prepareDirs("cardinal"); err != nil { return err } - if err := sh.RunV("docker", "compose", "-f", "docker-compose-debug.yml", "up", "--build", "cardinal", "nakama"); err != nil { + if err := sh.Run("docker", "compose", "-f", "docker-compose-debug.yml", "up", "--build", "cardinal", "nakama"); err != nil { return err } return nil @@ -143,10 +143,10 @@ func DockerStartDebug() error { // DockerStartDetach starts Nakama and Cardinal with detach and wait-timeout 60s (useful for CI workflow) func DockerStartDetach() error { - if err := prepareDirs("cardinal", "nakama"); err != nil { + if err := prepareDirs("cardinal"); err != nil { return err } - if err := sh.RunV("docker", "compose", "up", "--detach", "--wait", "--wait-timeout", "60"); err != nil { + if err := sh.Run("docker", "compose", "up", "--detach", "--wait", "--wait-timeout", "60"); err != nil { return err } return nil @@ -187,7 +187,7 @@ func DockerStop(services []DockerService) error { // DockerPurge stops and deletes all docker containers and data volumes // This will completely wipe the state, if you only want to stop the containers, use DockerStop func DockerPurge() error { - return sh.RunV("docker", "compose", "down", "--volumes") + return sh.Run("docker", "compose", "down", "--volumes") } // dockerArgs converts a string of docker args and slice of DockerService to a single slice of strings. diff --git a/common/tea_cmd/git.go b/common/tea_cmd/git.go index f36f849..2615c29 100644 --- a/common/tea_cmd/git.go +++ b/common/tea_cmd/git.go @@ -3,65 +3,55 @@ package tea_cmd import ( "bytes" tea "github.com/charmbracelet/bubbletea" + "github.com/magefile/mage/sh" "os" - "os/exec" ) type GitCloneFinishMsg struct { - ErrBuf *bytes.Buffer - Err error + Err error } -func Run(cmd *exec.Cmd) (*bytes.Buffer, error) { +func git(args ...string) error { var outBuff, errBuff bytes.Buffer - cmd.Stdout = &outBuff - cmd.Stderr = &errBuff - - err := cmd.Run() + _, err := sh.Exec(nil, &outBuff, &errBuff, "git", args...) if err != nil { - return &errBuff, err + return err } - - return nil, nil + return nil } func GitCloneCmd(url string, targetDir string, initMsg string) tea.Cmd { return func() tea.Msg { - cmd := exec.Command("git", "clone", url, targetDir) - errBuf, err := Run(cmd) + err := git("clone", url, targetDir) if err != nil { - return GitCloneFinishMsg{ErrBuf: errBuf, Err: err} + return GitCloneFinishMsg{Err: err} } err = os.Chdir(targetDir) if err != nil { - return GitCloneFinishMsg{ErrBuf: nil, Err: err} + return GitCloneFinishMsg{Err: err} } - cmd = exec.Command("rm", "-rf", ".git") - errBuf, err = Run(cmd) + err = sh.Run("rm", "-rf", ".git") if err != nil { - return GitCloneFinishMsg{ErrBuf: errBuf, Err: err} + return GitCloneFinishMsg{Err: err} } - cmd = exec.Command("git", "init") - errBuf, err = Run(cmd) + err = git("init") if err != nil { - return GitCloneFinishMsg{ErrBuf: errBuf, Err: err} + return GitCloneFinishMsg{Err: err} } - cmd = exec.Command("git", "add", "-A") - errBuf, err = Run(cmd) + err = git("add", "-A") if err != nil { - return GitCloneFinishMsg{ErrBuf: errBuf, Err: err} + return GitCloneFinishMsg{Err: err} } - cmd = exec.Command("git", "commit", "-m", initMsg) - errBuf, err = Run(cmd) + err = git("commit", "-m", initMsg) if err != nil { - return GitCloneFinishMsg{ErrBuf: errBuf, Err: err} + return GitCloneFinishMsg{Err: err} } - return GitCloneFinishMsg{ErrBuf: nil, Err: nil} + return GitCloneFinishMsg{Err: nil} } }