Guard root TUI entry against non-interactive env

Amolith created

Bare keld with no subcommand now checks isInteractive() before entering
the TUI. In non-interactive environments, returns a clear error message
directing the user to specify a subcommand instead of crashing with a
bubbletea TTY error.

Task: td-MY949DM

Change summary

cmd/root.go                     |  5 ++++
cmd/root_noninteractive_test.go | 38 +++++++++++++++++++++++++++++++++++
2 files changed, 43 insertions(+)

Detailed changes

cmd/root.go 🔗

@@ -44,6 +44,11 @@ var rootCmd = &cobra.Command{
 	TraverseChildren: true,
 
 	RunE: func(cmd *cobra.Command, _ []string) error {
+		// Bare keld with no subcommand requires a tty.
+		if !isInteractive() {
+			return fmt.Errorf("keld: no subcommand specified; non-interactive mode requires a subcommand (e.g. 'keld backup')")
+		}
+
 		commandName, preset, overrides, err := runInteractive("")
 		if err != nil {
 			return err

cmd/root_noninteractive_test.go 🔗

@@ -0,0 +1,38 @@
+package cmd
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestRootRunNonInteractiveRequiresSubcommand(t *testing.T) {
+	// Not parallel: mutates package-level isStdinTerminal.
+
+	// Override stdin terminal check to simulate non-interactive environment.
+	original := isStdinTerminal
+	t.Cleanup(func() {
+		isStdinTerminal = original
+	})
+	isStdinTerminal = func() bool { return false }
+
+	// Reset root command flags to defaults.
+	setRootFlagValuesForTest(t, "", false, "")
+	t.Setenv("KELD_CONFIG_FILE", "")
+	t.Setenv("KELD_DRYRUN", "")
+
+	err := rootCmd.RunE(rootCmd, nil)
+	if err == nil {
+		t.Fatal("expected error for bare keld in non-interactive mode, got nil")
+	}
+
+	errMsg := err.Error()
+	// Should NOT be a bubbletea TTY error.
+	if strings.Contains(errMsg, "bubbletea") || strings.Contains(errMsg, "TTY") {
+		t.Fatalf("non-interactive mode incorrectly tried to open TTY: %v", err)
+	}
+
+	// Should mention missing subcommand.
+	if !strings.Contains(errMsg, "subcommand") {
+		t.Fatalf("expected error to mention subcommand, got: %v", err)
+	}
+}