From cabc26878694eb372a4949f2321d39204ef2dd1b Mon Sep 17 00:00:00 2001
From: Jan Dubois <jan.dubois@suse.com>
Date: Sun, 3 Nov 2024 01:29:02 -0800
Subject: [PATCH] limactl shell: ask whether to start the instance if not
 running

Signed-off-by: Jan Dubois <jan.dubois@suse.com>
---
 cmd/limactl/edit.go   | 35 +++++-----------------------
 cmd/limactl/shell.go  | 28 ++++++++++++++++++++---
 cmd/limactl/start.go  | 53 +++++++++++++++++++++++++++++++++++++++++++
 pkg/instance/start.go | 13 ++++++++---
 4 files changed, 94 insertions(+), 35 deletions(-)

diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index 3d0dde19935..e8ef2d14298 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -10,12 +10,9 @@ import (
 	"github.com/lima-vm/lima/cmd/limactl/editflags"
 	"github.com/lima-vm/lima/cmd/limactl/guessarg"
 	"github.com/lima-vm/lima/pkg/editutil"
-	"github.com/lima-vm/lima/pkg/instance"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	networks "github.com/lima-vm/lima/pkg/networks/reconcile"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
-	"github.com/lima-vm/lima/pkg/uiutil"
 	"github.com/lima-vm/lima/pkg/yqutil"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -132,34 +129,14 @@ func editAction(cmd *cobra.Command, args []string) error {
 	}
 	if inst != nil {
 		logrus.Infof("Instance %q configuration edited", inst.Name)
-	}
-
-	if !tty {
-		// use "start" to start it
-		return nil
-	}
-	if inst == nil {
-		// edited a limayaml file directly
-		return nil
-	}
-	startNow, err := askWhetherToStart()
-	if err != nil {
-		return err
-	}
-	if !startNow {
-		return nil
-	}
-	ctx := cmd.Context()
-	err = networks.Reconcile(ctx, inst.Name)
-	if err != nil {
+		tty, err := interactive(cmd)
+		if tty && err == nil {
+			err = askToStart(cmd, inst.Name, false)
+		}
 		return err
 	}
-	return instance.Start(ctx, inst, "", false)
-}
-
-func askWhetherToStart() (bool, error) {
-	message := "Do you want to start the instance now? "
-	return uiutil.Confirm(message, true)
+	// inst is nil if edited a limayaml file directly
+	return nil
 }
 
 func editBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go
index 65d8fad7b29..473d83b96b7 100644
--- a/cmd/limactl/shell.go
+++ b/cmd/limactl/shell.go
@@ -51,6 +51,7 @@ func newShellCommand() *cobra.Command {
 }
 
 func shellAction(cmd *cobra.Command, args []string) error {
+	_ = os.Setenv("_LIMACTL_SHELL_IN_ACTION", "")
 	// simulate the behavior of double dash
 	newArg := []string{}
 	if len(args) >= 2 && args[1] == "--" {
@@ -68,15 +69,36 @@ func shellAction(cmd *cobra.Command, args []string) error {
 		}
 	}
 
+	tty, err := interactive(cmd)
+	if err != nil {
+		return err
+	}
 	inst, err := store.Inspect(instName)
 	if err != nil {
-		if errors.Is(err, os.ErrNotExist) {
+		if !errors.Is(err, os.ErrNotExist) {
+			return err
+		}
+		if !tty {
 			return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
 		}
+		if err := askToStart(cmd, instName, true); err != nil {
+			return err
+		}
+		inst, err = store.Inspect(instName)
+	} else if inst.Status == store.StatusStopped {
+		if !tty {
+			return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
+		}
+		if err := askToStart(cmd, instName, false); err != nil {
+			return err
+		}
+		inst, err = store.Inspect(instName)
+	}
+	if err != nil {
 		return err
 	}
-	if inst.Status == store.StatusStopped {
-		return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
+	if inst.Status != store.StatusRunning {
+		return fmt.Errorf("instance %q status is not %q but %q", inst.Name, store.StatusRunning, inst.Status)
 	}
 
 	// When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error.
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 90f329c59b3..a0e1a9cf527 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -23,6 +23,7 @@ import (
 	"github.com/lima-vm/lima/pkg/templatestore"
 	"github.com/lima-vm/lima/pkg/uiutil"
 	"github.com/lima-vm/lima/pkg/yqutil"
+	"github.com/mattn/go-isatty"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
@@ -529,3 +530,55 @@ func startBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobr
 	compTmpl, _ := bashCompleteTemplateNames(cmd)
 	return append(compInst, compTmpl...), cobra.ShellCompDirectiveDefault
 }
+
+// interactive returns true if --tty is true and both STDIN and STDOUT are terminals.
+func interactive(cmd *cobra.Command) (bool, error) {
+	flags := cmd.Flags()
+	tty, err := flags.GetBool("tty")
+	if err != nil {
+		return false, err
+	}
+	if !isatty.IsTerminal(os.Stdin.Fd()) && !isatty.IsCygwinTerminal(os.Stdin.Fd()) {
+		tty = false
+	}
+	if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
+		tty = false
+	}
+	return tty, nil
+}
+
+func askToStart(cmd *cobra.Command, instName string, create bool) error {
+	template := "default"
+	templates, err := templatestore.Templates()
+	if err != nil {
+		return err
+	}
+	for _, t := range templates {
+		if t.Name == instName {
+			template = instName
+			break
+		}
+	}
+	var message string
+	if create {
+		message = fmt.Sprintf("Do you want to create and start the instance %q using the %q template now?", instName, template)
+	} else {
+		message = fmt.Sprintf("Do you want to start the instance %q now?", instName)
+	}
+	ans, err := uiutil.Confirm(message, true)
+	if !ans || err != nil {
+		return err
+	}
+
+	rootCmd := cmd.Root()
+	if create {
+		// The create command shows the template chooser UI, etc.
+		rootCmd.SetArgs([]string{"create", "template://" + template})
+		if err := rootCmd.Execute(); err != nil {
+			return err
+		}
+	}
+	// The start command reconciles the networks, etc.
+	rootCmd.SetArgs([]string{"start", instName})
+	return rootCmd.Execute()
+}
diff --git a/pkg/instance/start.go b/pkg/instance/start.go
index f2b1c105b79..4ecbb3d614c 100644
--- a/pkg/instance/start.go
+++ b/pkg/instance/start.go
@@ -307,10 +307,17 @@ func watchHostAgentEvents(ctx context.Context, inst *store.Instance, haStdoutPat
 				err = xerr
 				return true
 			}
-			if *inst.Config.Plain {
-				logrus.Infof("READY. Run `ssh -F %q %s` to open the shell.", inst.SSHConfigFile, inst.Hostname)
+			// _LIMACTL_SHELL_IN_ACTION is set if `limactl shell` invoked `limactl start`.
+			// In this case we shouldn't print "Run `lima` to open the shell",
+			// because the user has already executed the `lima` command.
+			if _, limactlShellInAction := os.LookupEnv("_LIMACTL_SHELL_IN_ACTION"); limactlShellInAction {
+				logrus.Infof("READY.")
 			} else {
-				logrus.Infof("READY. Run `%s` to open the shell.", LimactlShellCmd(inst.Name))
+				if *inst.Config.Plain {
+					logrus.Infof("READY. Run `ssh -F %q %s` to open the shell.", inst.SSHConfigFile, inst.Hostname)
+				} else {
+					logrus.Infof("READY. Run `%s` to open the shell.", LimactlShellCmd(inst.Name))
+				}
 			}
 			_ = ShowMessage(inst)
 			err = nil