diff --git a/cmd/pm/main.go b/cmd/pm/main.go index 291d10b..8fb8863 100644 --- a/cmd/pm/main.go +++ b/cmd/pm/main.go @@ -22,5 +22,7 @@ func init() { issuesCmd.RootCmd.AddCommand(issuesCmd.CreateCmd) issuesCmd.RootCmd.AddCommand(issuesCmd.DeleteCmd) issuesCmd.RootCmd.AddCommand(issuesCmd.UpdateCmd) + issuesCmd.RootCmd.AddCommand(issuesCmd.CommentCmd) + issuesCmd.RootCmd.AddCommand(issuesCmd.CommentsCmd) issuesCmd.RootCmd.AddCommand(surveyCmd.StatusCmd) } diff --git a/internal/commands/issues/comment.go b/internal/commands/issues/comment.go new file mode 100644 index 0000000..decef9c --- /dev/null +++ b/internal/commands/issues/comment.go @@ -0,0 +1,90 @@ +package issuesCmd + +import ( + "fmt" + "os" + "os/user" + "strings" + + "github.com/spf13/cobra" +) + +// commentFlags holds the flag values for the comment command. +var commentFlags struct { + message string + author string +} + +const commentCmdExample = `pm comment ISSUE-1 Fix the login bug +pm comment ISSUE-1 LGTM --author alice +pm comment ISSUE-1 -m "Needs review"` + +var CommentCmd = &cobra.Command{ + Use: "comment [issue ID] [message...]", + Short: "Add a comment on an issue", + Long: `Add a comment on an issue by ID. All arguments after the issue ID form the message (no quotes needed), or use --message.`, + Example: commentCmdExample, + + Args: cobra.RangeArgs(1, 100), // issue ID + up to 99 words for the message + RunE: runCommentCmd, + ValidArgsFunction: completeIssues, +} + +// runCommentCmd executes the comment command logic, +// which adds a comment on an issue by its ID. +func runCommentCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + var text string + if len(args) > 1 { + text = strings.Join(args[1:], " ") + } else { + text = commentFlags.message + } + if strings.TrimSpace(text) == "" { + return fmt.Errorf("comment text cannot be empty (use --message or pass words after the issue ID)") + } + + app := AppFromContext(cmd.Context()) + + // Ensure issue exists. + issue, err := app.Issues.GetIssue(cmd.Context(), issueID) + if err != nil { + return fmt.Errorf("error fetching issue: %w", err) + } + if issue == nil { + return fmt.Errorf("issue with ID %s not found", issueID) + } + + author := commentFlags.author + if author == "" { + author = defaultCommentAuthor() + } + + comment, err := app.Issues.AddIssueComment(cmd.Context(), issue.ID, author, text) + if err != nil { + return fmt.Errorf("error adding comment: %w", err) + } + + cmd.Printf("Comment added on %s:\n", issueID) + cmd.Printf(" %s @ %s: %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"), comment.Text) + return nil +} + + +func defaultCommentAuthor() string { + if u, err := user.Current(); err == nil && u.Username != "" { + return u.Username + } + if s := os.Getenv("USER"); s != "" { + return s + } + if s := os.Getenv("USERNAME"); s != "" { + return s + } + return "user" +} + +func init() { + CommentCmd.Flags().StringVarP(&commentFlags.message, "message", "m", "", "Comment text (alternative to positional arguments)") + CommentCmd.Flags().StringVarP(&commentFlags.author, "author", "a", "", "Author name (default: current OS user)") +} diff --git a/internal/commands/issues/comments_list.go b/internal/commands/issues/comments_list.go new file mode 100644 index 0000000..897ebfb --- /dev/null +++ b/internal/commands/issues/comments_list.go @@ -0,0 +1,50 @@ +package issuesCmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// CommentsCmd represents the command to list comments on an issue. +var CommentsCmd = &cobra.Command{ + Use: "comments [issue ID]", + Short: "List comments on an issue", + Long: `List all comments on an issue by ID.`, + + Args: cobra.ExactArgs(1), + RunE: runCommentsCmd, + ValidArgsFunction: completeIssues, +} + +// runCommentsCmd executes the comments command logic. +func runCommentsCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + app := AppFromContext(cmd.Context()) + + issue, err := app.Issues.GetIssue(cmd.Context(), issueID) + if err != nil { + return fmt.Errorf("error fetching issue: %w", err) + } + if issue == nil { + return fmt.Errorf("issue with ID %s not found", issueID) + } + + comments, err := app.Issues.GetIssueComments(cmd.Context(), issue.ID) + if err != nil { + return fmt.Errorf("error fetching comments: %w", err) + } + + cmd.Printf("Comments on %s (%s):\n\n", issueID, issue.Title) + if len(comments) == 0 { + cmd.Println(" No comments yet.") + return nil + } + for _, c := range comments { + if c != nil { + cmd.Printf(" %s @ %s:\n %s\n\n", c.Author, c.CreatedAt.Format("2006-01-02 15:04"), c.Text) + } + } + return nil +} diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index bfbf51f..c12ab13 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -150,5 +150,7 @@ func init() { issuesCmd.RootCmd.AddCommand(issuesCmd.CreateCmd) issuesCmd.RootCmd.AddCommand(issuesCmd.DeleteCmd) issuesCmd.RootCmd.AddCommand(issuesCmd.UpdateCmd) + issuesCmd.RootCmd.AddCommand(issuesCmd.CommentCmd) + issuesCmd.RootCmd.AddCommand(issuesCmd.CommentsCmd) issuesCmd.RootCmd.AddCommand(surveyCmd.StatusCmd) } diff --git a/pkg/repl/suggestions.go b/pkg/repl/suggestions.go index 522afa1..48fd494 100644 --- a/pkg/repl/suggestions.go +++ b/pkg/repl/suggestions.go @@ -28,6 +28,8 @@ var baseSuggestions = []prompt.Suggest{ {Text: "update", Description: "Update an existing issue by ID"}, {Text: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, + {Text: "comment", Description: "Add a comment on an issue by ID"}, + {Text: "comments", Description: "List comments on an issue by ID"}, } // createFlags is a list of prompt suggestions for the create command flags. @@ -63,6 +65,13 @@ var deleteFlags = []prompt.Suggest{ {Text: "--interactive", Description: "Select issues to delete interactively"}, } +var commentFlags = []prompt.Suggest{ + {Text: "--message", Description: "Comment text (alternative to positional args)"}, + {Text: "-m", Description: "Comment text (short)"}, + {Text: "--author", Description: "Author name for the comment"}, + {Text: "-a", Description: "Author name (short)"}, +} + // statusValues is a list of prompt suggestions for status types var statusValues = []prompt.Suggest{ {Text: "open", Description: "Open status"}, @@ -98,20 +107,24 @@ var isIDCommand = map[string]bool{ "close": true, "update": true, "edit": true, + "comment": true, + "comments": true, } var commandFlags = map[string][]prompt.Suggest{ - "create": createFlags, - "add": createFlags, - "update": updateFlags, - "edit": updateFlags, - "list": listFlags, - "ls": listFlags, - "search": listFlags, - "delete": deleteFlags, - "del": deleteFlags, - "rm": deleteFlags, - "remove": deleteFlags, + "create": createFlags, + "add": createFlags, + "update": updateFlags, + "edit": updateFlags, + "list": listFlags, + "ls": listFlags, + "search": listFlags, + "delete": deleteFlags, + "del": deleteFlags, + "rm": deleteFlags, + "remove": deleteFlags, + "comment": commentFlags, + "comments": nil, // no flags, just issue ID } // commandSuggestions returns a list of prompt suggestions based on the current input words.