diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index 4ccb970..6422537 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -1,9 +1,10 @@ -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "cmd", srcs = [ "auth.go", + "flags.go", "mcp.go", "root.go", "version.go", @@ -22,9 +23,18 @@ go_library( "//pkg", "//pkg/auth", "//pkg/common", + "//pkg/utils", "@com_github_eat_pray_ai_cobra_mcp//:cobra-mcp", "@com_github_modelcontextprotocol_go_sdk//oauthex", "@com_github_savioxavier_termlink//:termlink", "@com_github_spf13_cobra//:cobra", + "@org_golang_x_term//:term", ], ) + +go_test( + name = "cmd_test", + srcs = ["flags_test.go"], + embed = [":cmd"], + deps = ["@com_github_spf13_cobra//:cobra"], +) diff --git a/cmd/caption/delete.go b/cmd/caption/delete.go index aad11b8..b53b85b 100644 --- a/cmd/caption/delete.go +++ b/cmd/caption/delete.go @@ -5,6 +5,7 @@ package caption import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -65,6 +66,7 @@ func init() { ) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -72,12 +74,17 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would delete caption(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := caption.NewCaption( caption.WithIds(ids), caption.WithOnBehalfOf(onBehalfOf), caption.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/caption/insert.go b/cmd/caption/insert.go index edd5848..6951cd9 100644 --- a/cmd/caption/insert.go +++ b/cmd/caption/insert.go @@ -107,6 +107,7 @@ func init() { _ = insertCmd.MarkFlagRequired("file") _ = insertCmd.MarkFlagRequired("videoId") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -114,7 +115,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert caption") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := caption.NewCaption( caption.WithFile(file), caption.WithAudioTrackType(audioTrackType), @@ -131,6 +137,6 @@ var insertCmd = &cobra.Command{ caption.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), caption.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/caption/update.go b/cmd/caption/update.go index 30ccbb8..fa38c78 100644 --- a/cmd/caption/update.go +++ b/cmd/caption/update.go @@ -105,6 +105,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("videoId") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -112,7 +113,12 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would update caption for video: %s", videoId) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := caption.NewCaption( caption.WithFile(file), caption.WithAudioTrackType(audioTrackType), @@ -129,6 +135,6 @@ var updateCmd = &cobra.Command{ caption.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), caption.WithOutput(output), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/channel/update.go b/cmd/channel/update.go index f0474e4..345d021 100644 --- a/cmd/channel/update.go +++ b/cmd/channel/update.go @@ -6,6 +6,7 @@ package channel import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -81,6 +82,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("id") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -88,7 +90,12 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would update channel: %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := channel.NewChannel( channel.WithIds(ids), channel.WithCountry(country), @@ -98,6 +105,6 @@ var updateCmd = &cobra.Command{ channel.WithTitle(title), channel.WithOutput(output), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/channelBanner/insert.go b/cmd/channelBanner/insert.go index 10f99fc..315a536 100644 --- a/cmd/channelBanner/insert.go +++ b/cmd/channelBanner/insert.go @@ -81,6 +81,7 @@ func init() { _ = insertCmd.MarkFlagRequired("channelId") _ = insertCmd.MarkFlagRequired("file") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -88,7 +89,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert channel banner") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := channelBanner.NewChannelBanner( channelBanner.WithChannelId(channelId), channelBanner.WithFile(file), @@ -96,6 +102,6 @@ var insertCmd = &cobra.Command{ channelBanner.WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel), channelBanner.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/channelSection/delete.go b/cmd/channelSection/delete.go index fc02981..82fffd7 100644 --- a/cmd/channelSection/delete.go +++ b/cmd/channelSection/delete.go @@ -5,6 +5,7 @@ package channelSection import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -64,6 +65,7 @@ func init() { ) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -71,11 +73,18 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm( + c, "Would delete channel section(s): %s", strings.Join(ids, ", "), + ) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := channelSection.NewChannelSection( channelSection.WithIds(ids), channelSection.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/comment/delete.go b/cmd/comment/delete.go index 6a7131a..ba7fd1d 100644 --- a/cmd/comment/delete.go +++ b/cmd/comment/delete.go @@ -5,6 +5,7 @@ package comment import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -56,6 +57,7 @@ func init() { deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, idsUsage) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -63,8 +65,13 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would delete comment(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := comment.NewComment(comment.WithIds(ids)) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/comment/insert.go b/cmd/comment/insert.go index 6ca20bd..f0cf6ea 100644 --- a/cmd/comment/insert.go +++ b/cmd/comment/insert.go @@ -83,6 +83,7 @@ func init() { _ = insertCmd.MarkFlagRequired("parentId") _ = insertCmd.MarkFlagRequired("textOriginal") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -90,7 +91,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert comment") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := comment.NewComment( comment.WithAuthorChannelId(authorChannelId), comment.WithChannelId(channelId), @@ -100,6 +106,6 @@ var insertCmd = &cobra.Command{ comment.WithVideoId(videoId), comment.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/comment/markAsSpam.go b/cmd/comment/markAsSpam.go index 4b9bb9c..b817ac4 100644 --- a/cmd/comment/markAsSpam.go +++ b/cmd/comment/markAsSpam.go @@ -6,6 +6,7 @@ package comment import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -64,6 +65,7 @@ func init() { markAsSpamCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = markAsSpamCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(markAsSpamCmd) } var markAsSpamCmd = &cobra.Command{ @@ -71,11 +73,16 @@ var markAsSpamCmd = &cobra.Command{ Short: masShort, Long: masLong, Example: masExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would mark comment(s) as spam: %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := comment.NewComment( comment.WithIds(ids), comment.WithOutput(output), ) - utils.HandleCmdError(input.MarkAsSpam(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.MarkAsSpam(c.OutOrStdout()), c) }, } diff --git a/cmd/comment/setModerationStatus.go b/cmd/comment/setModerationStatus.go index 5d15da1..b8cb449 100644 --- a/cmd/comment/setModerationStatus.go +++ b/cmd/comment/setModerationStatus.go @@ -6,6 +6,7 @@ package comment import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -83,6 +84,7 @@ func init() { _ = setModerationStatusCmd.MarkFlagRequired("ids") _ = setModerationStatusCmd.MarkFlagRequired("moderationStatus") + cmd.AddMutationFlags(setModerationStatusCmd) } var setModerationStatusCmd = &cobra.Command{ @@ -90,13 +92,18 @@ var setModerationStatusCmd = &cobra.Command{ Short: smsShort, Long: smsLong, Example: smsExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would set moderation status of comment(s): %s to %s", strings.Join(ids, ", "), moderationStatus) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := comment.NewComment( comment.WithIds(ids), comment.WithModerationStatus(moderationStatus), comment.WithBanAuthor(banAuthor), comment.WithOutput(output), ) - utils.HandleCmdError(input.SetModerationStatus(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.SetModerationStatus(c.OutOrStdout()), c) }, } diff --git a/cmd/comment/update.go b/cmd/comment/update.go index 75c2aa8..0ce061f 100644 --- a/cmd/comment/update.go +++ b/cmd/comment/update.go @@ -6,6 +6,7 @@ package comment import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -80,6 +81,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("id") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -87,7 +89,12 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would update comment: %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := comment.NewComment( comment.WithIds(ids), comment.WithCanRate(canRate), @@ -95,6 +102,6 @@ var updateCmd = &cobra.Command{ comment.WithViewerRating(viewerRating), comment.WithOutput(output), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/commentThread/insert.go b/cmd/commentThread/insert.go index 4d6bee1..5367b69 100644 --- a/cmd/commentThread/insert.go +++ b/cmd/commentThread/insert.go @@ -74,6 +74,7 @@ func init() { _ = insertCmd.MarkFlagRequired("channelId") _ = insertCmd.MarkFlagRequired("textOriginal") _ = insertCmd.MarkFlagRequired("videoId") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -81,7 +82,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert comment thread") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := commentThread.NewCommentThread( commentThread.WithAuthorChannelId(authorChannelId), commentThread.WithChannelId(channelId), @@ -89,6 +95,6 @@ var insertCmd = &cobra.Command{ commentThread.WithVideoId(videoId), commentThread.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 0000000..8f24037 --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,88 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + + "github.com/eat-pray-ai/yutu/pkg/utils" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +// ErrDryRun is returned by Confirm when --dry-run is set. +var ErrDryRun = utils.ErrDryRun + +// errAborted is returned by Confirm when the user declines the prompt. +var errAborted = errors.New("aborted") + +// AddMutationFlags registers --dry-run and --yes on the given command. +func AddMutationFlags(cmd *cobra.Command) { + cmd.Flags().Bool( + "dry-run", false, + "Print what would happen without calling the API", + ) + cmd.Flags().Bool( + "yes", false, + "Skip the confirmation prompt", + ) +} + +// DryRun returns whether --dry-run was passed. +func DryRun(cmd *cobra.Command) bool { + v, _ := cmd.Flags().GetBool("dry-run") + return v +} + +// Confirm checks dry-run and yes flags, then prompts the user for +// confirmation if running in an interactive terminal. +// +// Returns ErrDryRun if --dry-run is set, nil if --yes is set or the user +// confirms, and errAborted if the user declines. +func Confirm(cmd *cobra.Command, format string, args ...any) error { + isTTY := term.IsTerminal(int(os.Stdin.Fd())) + return confirmWithTTYOverride(cmd, isTTY, format, args...) +} + +// confirmWithTTYOverride is an internal helper that accepts a pre-computed +// isTTY value so tests can exercise the TTY and non-TTY paths without +// needing a real terminal. +func confirmWithTTYOverride(cmd *cobra.Command, isTTY bool, format string, args ...any) error { + msg := fmt.Sprintf(format, args...) + + // 1. --dry-run takes precedence over everything. + if DryRun(cmd) { + fmt.Fprintln(cmd.OutOrStdout(), msg) + return ErrDryRun + } + + // 2. --yes skips the prompt entirely. + yes, _ := cmd.Flags().GetBool("yes") + if yes { + return nil + } + + // 3. Non-interactive stdin requires --yes. + if !isTTY { + return errors.New("terminal required for confirmation; use --yes to skip") + } + + // 4. Interactive prompt. + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s [y/N] ", msg) + + scanner := bufio.NewScanner(cmd.InOrStdin()) + if !scanner.Scan() { + return errAborted + } + answer := strings.TrimSpace(scanner.Text()) + + if answer == "y" || answer == "Y" { + return nil + } + return errAborted +} diff --git a/cmd/flags_test.go b/cmd/flags_test.go new file mode 100644 index 0000000..b1eaf40 --- /dev/null +++ b/cmd/flags_test.go @@ -0,0 +1,173 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +// newTestCmd creates a minimal cobra.Command with mutation flags registered. +func newTestCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "test", + Run: func(cmd *cobra.Command, args []string) {}, + } + AddMutationFlags(cmd) + return cmd +} + +func TestAddMutationFlags(t *testing.T) { + cmd := newTestCmd() + + dr := cmd.Flags().Lookup("dry-run") + if dr == nil { + t.Fatal("expected --dry-run flag to be registered") + } + if dr.DefValue != "false" { + t.Fatalf("expected --dry-run default to be false, got %s", dr.DefValue) + } + + yes := cmd.Flags().Lookup("yes") + if yes == nil { + t.Fatal("expected --yes flag to be registered") + } + if yes.DefValue != "false" { + t.Fatalf("expected --yes default to be false, got %s", yes.DefValue) + } +} + +func TestDryRun_DefaultFalse(t *testing.T) { + cmd := newTestCmd() + if DryRun(cmd) { + t.Fatal("expected DryRun to be false by default") + } +} + +func TestDryRun_SetTrue(t *testing.T) { + cmd := newTestCmd() + if err := cmd.Flags().Set("dry-run", "true"); err != nil { + t.Fatal(err) + } + if !DryRun(cmd) { + t.Fatal("expected DryRun to be true after setting flag") + } +} + +func TestConfirm_DryRun(t *testing.T) { + cmd := newTestCmd() + if err := cmd.Flags().Set("dry-run", "true"); err != nil { + t.Fatal(err) + } + + var stdout bytes.Buffer + cmd.SetOut(&stdout) + + err := Confirm(cmd, "would delete %s", "video123") + if !errors.Is(err, ErrDryRun) { + t.Fatalf("expected ErrDryRun, got %v", err) + } + + got := stdout.String() + if !strings.Contains(got, "would delete video123") { + t.Fatalf("expected message in stdout, got %q", got) + } +} + +func TestConfirm_Yes(t *testing.T) { + cmd := newTestCmd() + if err := cmd.Flags().Set("yes", "true"); err != nil { + t.Fatal(err) + } + + err := Confirm(cmd, "delete video?") + if err != nil { + t.Fatalf("expected nil, got %v", err) + } +} + +func TestConfirm_NonTTY(t *testing.T) { + cmd := newTestCmd() + cmd.SetIn(strings.NewReader("")) + + // Use the internal helper with isTTY=false to simulate non-TTY stdin. + err := confirmWithTTYOverride(cmd, false, "delete video?") + if err == nil { + t.Fatal("expected error for non-TTY stdin") + } + if !strings.Contains( + err.Error(), "terminal required for confirmation; use --yes to skip", + ) { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestConfirm_TTY_Yes(t *testing.T) { + cmd := newTestCmd() + cmd.SetIn(strings.NewReader("y\n")) + + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + err := confirmWithTTYOverride(cmd, true, "delete %s?", "video123") + if err != nil { + t.Fatalf("expected nil, got %v", err) + } + + prompt := stderr.String() + if !strings.Contains(prompt, "delete video123?") { + t.Fatalf("expected prompt on stderr, got %q", prompt) + } + if !strings.Contains(prompt, "[y/N]") { + t.Fatalf("expected [y/N] in prompt, got %q", prompt) + } +} + +func TestConfirm_TTY_No(t *testing.T) { + cmd := newTestCmd() + cmd.SetIn(strings.NewReader("n\n")) + + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + err := confirmWithTTYOverride(cmd, true, "delete video?") + if !errors.Is(err, errAborted) { + t.Fatalf("expected errAborted, got %v", err) + } +} + +func TestConfirm_TTY_Default(t *testing.T) { + cmd := newTestCmd() + cmd.SetIn(strings.NewReader("\n")) + + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + err := confirmWithTTYOverride(cmd, true, "delete video?") + if !errors.Is(err, errAborted) { + t.Fatalf("expected errAborted for empty input (default N), got %v", err) + } +} + +func TestConfirm_DryRunTakesPrecedence(t *testing.T) { + cmd := newTestCmd() + if err := cmd.Flags().Set("dry-run", "true"); err != nil { + t.Fatal(err) + } + if err := cmd.Flags().Set("yes", "true"); err != nil { + t.Fatal(err) + } + + var stdout bytes.Buffer + cmd.SetOut(&stdout) + + err := Confirm(cmd, "would delete video") + if !errors.Is(err, ErrDryRun) { + t.Fatalf("expected ErrDryRun even with --yes, got %v", err) + } +} diff --git a/cmd/playlist/delete.go b/cmd/playlist/delete.go index 99759da..bbd26de 100644 --- a/cmd/playlist/delete.go +++ b/cmd/playlist/delete.go @@ -5,6 +5,7 @@ package playlist import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -63,6 +64,7 @@ func init() { ) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -70,11 +72,16 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would delete playlist(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlist.NewPlaylist( playlist.WithIds(ids), playlist.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/playlist/insert.go b/cmd/playlist/insert.go index 4c4654d..a5cceef 100644 --- a/cmd/playlist/insert.go +++ b/cmd/playlist/insert.go @@ -82,6 +82,7 @@ func init() { _ = insertCmd.MarkFlagRequired("title") _ = insertCmd.MarkFlagRequired("channelId") _ = insertCmd.MarkFlagRequired("privacy") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -89,7 +90,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert playlist") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlist.NewPlaylist( playlist.WithTitle(title), playlist.WithDescription(description), @@ -99,6 +105,6 @@ var insertCmd = &cobra.Command{ playlist.WithPrivacy(privacy), playlist.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/playlist/update.go b/cmd/playlist/update.go index 934c5f2..84313bb 100644 --- a/cmd/playlist/update.go +++ b/cmd/playlist/update.go @@ -6,6 +6,7 @@ package playlist import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -85,6 +86,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("id") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -92,7 +94,12 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + confirmErr := cmd.Confirm(c, "Would update playlist: %s", strings.Join(ids, ", ")) + if confirmErr != nil { + utils.HandleCmdError(confirmErr, c) + return + } p := playlist.NewPlaylist( playlist.WithIds(ids), playlist.WithTitle(title), @@ -102,7 +109,7 @@ var updateCmd = &cobra.Command{ playlist.WithPrivacy(privacy), playlist.WithOutput(output), ) - err := p.Update(cmd.OutOrStdout()) - utils.HandleCmdError(err, cmd) + err := p.Update(c.OutOrStdout()) + utils.HandleCmdError(err, c) }, } diff --git a/cmd/playlistImage/delete.go b/cmd/playlistImage/delete.go index 420ff6b..257c21f 100644 --- a/cmd/playlistImage/delete.go +++ b/cmd/playlistImage/delete.go @@ -5,6 +5,7 @@ package playlistImage import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -63,6 +64,7 @@ func init() { ) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -70,11 +72,18 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm( + c, "Would delete playlist image(s): %s", strings.Join(ids, ", "), + ) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlistImage.NewPlaylistImage( playlistImage.WithIds(ids), playlistImage.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/playlistImage/insert.go b/cmd/playlistImage/insert.go index d70780e..0bc875f 100644 --- a/cmd/playlistImage/insert.go +++ b/cmd/playlistImage/insert.go @@ -94,6 +94,7 @@ func init() { _ = insertCmd.MarkFlagRequired("file") _ = insertCmd.MarkFlagRequired("playlistId") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -101,7 +102,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + confirmErr := cmd.Confirm(c, "Would insert playlist image") + if confirmErr != nil { + utils.HandleCmdError(confirmErr, c) + return + } pi := playlistImage.NewPlaylistImage( playlistImage.WithFile(file), playlistImage.WithPlaylistId(playlistId), @@ -112,7 +118,7 @@ var insertCmd = &cobra.Command{ playlistImage.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), playlistImage.WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel), ) - err := pi.Insert(cmd.OutOrStdout()) - utils.HandleCmdError(err, cmd) + err := pi.Insert(c.OutOrStdout()) + utils.HandleCmdError(err, c) }, } diff --git a/cmd/playlistImage/update.go b/cmd/playlistImage/update.go index d9e04cb..8401130 100644 --- a/cmd/playlistImage/update.go +++ b/cmd/playlistImage/update.go @@ -87,6 +87,7 @@ func init() { ) _ = updateCmd.MarkFlagRequired("playlistId") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -94,7 +95,14 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm( + c, "Would update playlist image for playlist: %s", playlistId, + ) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlistImage.NewPlaylistImage( playlistImage.WithPlaylistId(playlistId), playlistImage.WithType(type_), @@ -105,6 +113,6 @@ var updateCmd = &cobra.Command{ playlistImage.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), playlistImage.WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/playlistItem/delete.go b/cmd/playlistItem/delete.go index e62449a..a58590d 100644 --- a/cmd/playlistItem/delete.go +++ b/cmd/playlistItem/delete.go @@ -5,6 +5,7 @@ package playlistItem import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -63,6 +64,7 @@ func init() { ) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -70,11 +72,18 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm( + c, "Would delete playlist item(s): %s", strings.Join(ids, ", "), + ) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlistItem.NewPlaylistItem( playlistItem.WithIds(ids), playlistItem.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/playlistItem/insert.go b/cmd/playlistItem/insert.go index e4233f9..9f7f370 100644 --- a/cmd/playlistItem/insert.go +++ b/cmd/playlistItem/insert.go @@ -92,6 +92,7 @@ func init() { _ = insertCmd.MarkFlagRequired("kind") _ = insertCmd.MarkFlagRequired("playlistId") _ = insertCmd.MarkFlagRequired("channelId") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -99,7 +100,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert playlist item") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlistItem.NewPlaylistItem( playlistItem.WithTitle(title), playlistItem.WithDescription(description), @@ -113,6 +119,6 @@ var insertCmd = &cobra.Command{ playlistItem.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), playlistItem.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/playlistItem/update.go b/cmd/playlistItem/update.go index 7090335..bd2f770 100644 --- a/cmd/playlistItem/update.go +++ b/cmd/playlistItem/update.go @@ -6,6 +6,7 @@ package playlistItem import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -80,6 +81,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("id") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -87,7 +89,14 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm( + c, "Would update playlist item: %s", strings.Join(ids, ", "), + ) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := playlistItem.NewPlaylistItem( playlistItem.WithIds(ids), playlistItem.WithTitle(title), @@ -96,6 +105,6 @@ var updateCmd = &cobra.Command{ playlistItem.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), playlistItem.WithOutput(output), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/subscription/delete.go b/cmd/subscription/delete.go index 5d0d0aa..aa6b789 100644 --- a/cmd/subscription/delete.go +++ b/cmd/subscription/delete.go @@ -5,6 +5,7 @@ package subscription import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -57,6 +58,7 @@ func init() { deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -64,10 +66,15 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would delete subscription(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := subscription.NewSubscription( subscription.WithIds(ids), ) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/subscription/insert.go b/cmd/subscription/insert.go index a2e3f9b..e022ae5 100644 --- a/cmd/subscription/insert.go +++ b/cmd/subscription/insert.go @@ -71,6 +71,7 @@ func init() { _ = insertCmd.MarkFlagRequired("subscriberChannelId") _ = insertCmd.MarkFlagRequired("channelId") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -78,7 +79,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert subscription") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := subscription.NewSubscription( subscription.WithSubscriberChannelId(subscriberChannelId), subscription.WithDescription(description), @@ -86,6 +92,6 @@ var insertCmd = &cobra.Command{ subscription.WithTitle(title), subscription.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/thumbnail/set.go b/cmd/thumbnail/set.go index 461899c..45797f2 100644 --- a/cmd/thumbnail/set.go +++ b/cmd/thumbnail/set.go @@ -64,6 +64,7 @@ func init() { _ = setCmd.MarkFlagRequired("file") _ = setCmd.MarkFlagRequired("videoId") + cmd.AddMutationFlags(setCmd) } var setCmd = &cobra.Command{ @@ -71,12 +72,17 @@ var setCmd = &cobra.Command{ Short: setShort, Long: setLong, Example: setExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would set thumbnail for video: %s", videoId) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := thumbnail.NewThumbnail( thumbnail.WithFile(file), thumbnail.WithVideoId(videoId), thumbnail.WithOutput(output), ) - utils.HandleCmdError(input.Set(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Set(c.OutOrStdout()), c) }, } diff --git a/cmd/video/delete.go b/cmd/video/delete.go index 580fcc0..340b56d 100644 --- a/cmd/video/delete.go +++ b/cmd/video/delete.go @@ -5,6 +5,7 @@ package video import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -57,6 +58,7 @@ func init() { deleteCmd.Flags().StringSliceVarP(&ids, "ids", "i", []string{}, deleteIdsUsage) _ = deleteCmd.MarkFlagRequired("ids") + cmd.AddMutationFlags(deleteCmd) } var deleteCmd = &cobra.Command{ @@ -64,8 +66,13 @@ var deleteCmd = &cobra.Command{ Short: deleteShort, Long: deleteLong, Example: deleteExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would delete video(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := video.NewVideo(video.WithIds(ids)) - utils.HandleCmdError(input.Delete(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Delete(c.OutOrStdout()), c) }, } diff --git a/cmd/video/insert.go b/cmd/video/insert.go index ec13ca1..a38c447 100644 --- a/cmd/video/insert.go +++ b/cmd/video/insert.go @@ -139,6 +139,7 @@ func init() { _ = insertCmd.MarkFlagRequired("file") _ = insertCmd.MarkFlagRequired("categoryId") _ = insertCmd.MarkFlagRequired("privacy") + cmd.AddMutationFlags(insertCmd) } var insertCmd = &cobra.Command{ @@ -146,7 +147,12 @@ var insertCmd = &cobra.Command{ Short: insertShort, Long: insertLong, Example: insertExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would insert video") + if err != nil { + utils.HandleCmdError(err, c) + return + } input := video.NewVideo( video.WithAutoLevels(autoLevels), video.WithFile(file), @@ -172,6 +178,6 @@ var insertCmd = &cobra.Command{ video.WithOnBehalfOfContentOwnerChannel(onBehalfOfContentOwnerChannel), video.WithOutput(output), ) - utils.HandleCmdError(input.Insert(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Insert(c.OutOrStdout()), c) }, } diff --git a/cmd/video/rate.go b/cmd/video/rate.go index 5c5b45e..13f90b5 100644 --- a/cmd/video/rate.go +++ b/cmd/video/rate.go @@ -5,6 +5,7 @@ package video import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -67,6 +68,7 @@ func init() { _ = rateCmd.MarkFlagRequired("ids") _ = rateCmd.MarkFlagRequired("rating") + cmd.AddMutationFlags(rateCmd) } var rateCmd = &cobra.Command{ @@ -74,11 +76,16 @@ var rateCmd = &cobra.Command{ Short: rateShort, Long: rateLong, Example: rateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would rate video(s): %s as %s", strings.Join(ids, ", "), rating) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := video.NewVideo( video.WithIds(ids), video.WithRating(rating), ) - utils.HandleCmdError(input.Rate(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Rate(c.OutOrStdout()), c) }, } diff --git a/cmd/video/reportAbuse.go b/cmd/video/reportAbuse.go index 743219c..8b230a4 100644 --- a/cmd/video/reportAbuse.go +++ b/cmd/video/reportAbuse.go @@ -5,6 +5,7 @@ package video import ( "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -82,6 +83,7 @@ func init() { _ = reportAbuseCmd.MarkFlagRequired("ids") _ = reportAbuseCmd.MarkFlagRequired("reasonId") + cmd.AddMutationFlags(reportAbuseCmd) } var reportAbuseCmd = &cobra.Command{ @@ -89,7 +91,12 @@ var reportAbuseCmd = &cobra.Command{ Short: reportAbuseShort, Long: reportAbuseLong, Example: reportAbuseExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would report abuse on video(s): %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := video.NewVideo( video.WithIds(ids), video.WithReasonId(reasonId), @@ -98,6 +105,6 @@ var reportAbuseCmd = &cobra.Command{ video.WithLanguage(language), video.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.ReportAbuse(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.ReportAbuse(c.OutOrStdout()), c) }, } diff --git a/cmd/video/update.go b/cmd/video/update.go index 87d26e2..aa146f5 100644 --- a/cmd/video/update.go +++ b/cmd/video/update.go @@ -6,6 +6,7 @@ package video import ( "encoding/json" "io" + "strings" cobramcp "github.com/eat-pray-ai/cobra-mcp" "github.com/eat-pray-ai/yutu/cmd" @@ -106,6 +107,7 @@ func init() { updateCmd.Flags().StringVarP(&output, "output", "o", "", pkg.SilentUsage) _ = updateCmd.MarkFlagRequired("id") + cmd.AddMutationFlags(updateCmd) } var updateCmd = &cobra.Command{ @@ -113,7 +115,12 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Example: updateExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would update video: %s", strings.Join(ids, ", ")) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := video.NewVideo( video.WithIds(ids), video.WithTitle(title), @@ -131,6 +138,6 @@ var updateCmd = &cobra.Command{ video.WithMaxResults(1), video.WithOutput(output), ) - utils.HandleCmdError(input.Update(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Update(c.OutOrStdout()), c) }, } diff --git a/cmd/watermark/set.go b/cmd/watermark/set.go index 75548f9..9f22415 100644 --- a/cmd/watermark/set.go +++ b/cmd/watermark/set.go @@ -80,6 +80,7 @@ func init() { _ = setCmd.MarkFlagRequired("channelId") _ = setCmd.MarkFlagRequired("file") + cmd.AddMutationFlags(setCmd) } var setCmd = &cobra.Command{ @@ -87,7 +88,12 @@ var setCmd = &cobra.Command{ Short: setShort, Long: setLong, Example: setExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would set watermark for channel: %s", channelId) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := watermark.NewWatermark( watermark.WithChannelId(channelId), watermark.WithFile(file), @@ -97,6 +103,6 @@ var setCmd = &cobra.Command{ watermark.WithOffsetType(offsetType), watermark.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), ) - utils.HandleCmdError(input.Set(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Set(c.OutOrStdout()), c) }, } diff --git a/cmd/watermark/unset.go b/cmd/watermark/unset.go index 295e281..952ca40 100644 --- a/cmd/watermark/unset.go +++ b/cmd/watermark/unset.go @@ -51,6 +51,7 @@ func init() { unsetCmd.Flags().StringVarP(&channelId, "channelId", "c", "", cidUsage) _ = unsetCmd.MarkFlagRequired("channelId") + cmd.AddMutationFlags(unsetCmd) } var unsetCmd = &cobra.Command{ @@ -58,8 +59,13 @@ var unsetCmd = &cobra.Command{ Short: unsetShort, Long: unsetLong, Example: unsetExample, - Run: func(cmd *cobra.Command, args []string) { + Run: func(c *cobra.Command, args []string) { + err := cmd.Confirm(c, "Would unset watermark for channel: %s", channelId) + if err != nil { + utils.HandleCmdError(err, c) + return + } input := watermark.NewWatermark(watermark.WithChannelId(channelId)) - utils.HandleCmdError(input.Unset(cmd.OutOrStdout()), cmd) + utils.HandleCmdError(input.Unset(c.OutOrStdout()), c) }, } diff --git a/go.mod b/go.mod index bee31fb..5796884 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -35,7 +34,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect @@ -52,10 +50,10 @@ require ( go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect + golang.org/x/term v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/go.sum b/go.sum index ff60827..871a6d8 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eat-pray-ai/cobra-mcp v0.1.0 h1:szHIgFhCH0HqWSRRlyVVWzrB+3c9SMNfQCxlKA4lNLo= @@ -74,7 +72,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savioxavier/termlink v1.4.3 h1:Gh6vrG7jSn21cRiYdQqFXYcdXfM+Fg14aG487JTfKpA= github.com/savioxavier/termlink v1.4.3/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= @@ -123,7 +120,6 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= @@ -135,6 +131,8 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= diff --git a/internal/tools/cmdtestgen/BUILD.bazel b/internal/tools/cmdtestgen/BUILD.bazel index 5d59c56..6992235 100644 --- a/internal/tools/cmdtestgen/BUILD.bazel +++ b/internal/tools/cmdtestgen/BUILD.bazel @@ -7,8 +7,8 @@ go_library( visibility = ["//visibility:private"], deps = [ "//cmd", - "//cmd/agent", "//cmd/activity", + "//cmd/agent", "//cmd/caption", "//cmd/channel", "//cmd/channelBanner", diff --git a/pkg/auth/verifier_test.go b/pkg/auth/verifier_test.go index 0b56b3d..4b03b72 100644 --- a/pkg/auth/verifier_test.go +++ b/pkg/auth/verifier_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + package auth import ( diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e2c49a0..cb1b735 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "os/exec" @@ -99,9 +100,13 @@ func ExtractHl(uri string) string { return "" } +// ErrDryRun is returned when --dry-run is set; HandleCmdError silently ignores it. +var ErrDryRun = errors.New("dry run") + func HandleCmdError(err error, cmd *cobra.Command) { - if err != nil { - _ = cmd.Help() - cmd.PrintErrf("Error: %v\n", err) + if err == nil || errors.Is(err, ErrDryRun) { + return } + _ = cmd.Help() + cmd.PrintErrf("Error: %v\n", err) } diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index eb527a4..1ba55e2 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -331,6 +331,16 @@ func TestRandomStage(t *testing.T) { } } +func TestHandleCmdError_DryRun(t *testing.T) { + c := &cobra.Command{Use: "test"} + var stderr bytes.Buffer + c.SetErr(&stderr) + HandleCmdError(ErrDryRun, c) + if stderr.Len() != 0 { + t.Errorf("expected no error output for ErrDryRun, got %q", stderr.String()) + } +} + func TestHandleCmdError(t *testing.T) { tests := []struct { name string