diff --git a/Makefile b/Makefile index ee8af5e..2e82291 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL := /bin/bash + tidy: go mod tidy @@ -27,4 +29,34 @@ tw: watch: @make -j2 dev tw -.PHONY: tidy clean build cli tui web dev tw \ No newline at end of file +completions: + @go build -o ./bin/pm ./cmd/pm + @mkdir -p ./bin + @./bin/pm completion bash > ./bin/pm_bash.sh + @./bin/pm completion zsh > ./bin/pm_zsh.sh + @./bin/pm completion fish > ./bin/pm_fish.sh + @./bin/pm completion powershell > ./bin/pm_powershell.ps1 + +install-bash-temp: completions + @go install ./cmd/pm + @source ./bin/pm_bash.sh + +install-zsh-temp: completions + @go install ./cmd/pm + @source ./bin/pm_zsh.sh + +install-fish-temp: completions + @go install ./cmd/pm + @source ./bin/pm_fish.sh + +install-powershell-temp: completions + @go install ./cmd/pm + @source ./bin/pm_powershell.ps1 + + +install-cli: completions + @go install ./cmd/pm + @sudo cp ./bin/pm_bash.sh /etc/bash_completion.d/pm + + +.PHONY: tidy clean build cli tui web dev tw completions install-bash-temp install-zsh-temp install-fish-temp install-powershell-temp install-cli \ No newline at end of file diff --git a/cmd/cli/main.go b/cmd/pm/main.go similarity index 78% rename from cmd/cli/main.go rename to cmd/pm/main.go index d4f103d..5ad70e1 100644 --- a/cmd/cli/main.go +++ b/cmd/pm/main.go @@ -3,12 +3,12 @@ package main import ( "context" - "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/cli" ) func main() { - config := service.Config{ + config := cli.CLIConfig{ + RootCmd: "pm", IssuePrefix: "pm", BeadsDBPath: "./.pm/db.db", StatisticsStoragePath: "./.pm/stats.json", diff --git a/internal/models/beads.go b/internal/models/beads.go index d009e86..bae490a 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -1,6 +1,13 @@ package models -import "github.com/steveyegge/beads" +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/muesli/reflow/truncate" + "github.com/steveyegge/beads" +) type ( Issue = beads.Issue @@ -71,3 +78,37 @@ const ( EventLabelRemoved = beads.EventLabelRemoved EventCompacted = beads.EventCompacted ) + +func IssuesPtrToIssues(issuePtr []*Issue) []Issue { + issues := make([]Issue, 0, len(issuePtr)) + for _, issuePtr := range issuePtr { + if issuePtr != nil { + issues = append(issues, *issuePtr) + } + } + return issues +} + +func FormatIssueRow(issue Issue) string { + return fmt.Sprintf( + "%s\t%s\t%s\t%s\t%s\t%d", + truncate.String(issue.ID, 10), + truncate.StringWithTail(issue.Title, 25, "..."), + truncate.StringWithTail(issue.Description, 40, "..."), + issue.Status, + issue.IssueType, + issue.Priority, + ) +} + +func PrintIssues(issues []Issue) { + w := tabwriter.NewWriter(os.Stdout, 8, 10, 5, ' ', 0) + + fmt.Fprintln(w, "ID\tTITLE\tDESCRIPTION\tSTATUS\tTYPE\tPRIORITY") + + for _, issue := range issues { + fmt.Fprintln(w, FormatIssueRow(issue)) + } + + w.Flush() +} diff --git a/internal/service/beads.go b/internal/service/beads.go index e9a191d..06c8373 100644 --- a/internal/service/beads.go +++ b/internal/service/beads.go @@ -37,12 +37,7 @@ func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { return []models.Issue{}, nil } - issues := make([]models.Issue, 0, len(issuesPtr)) - for _, issuePtr := range issuesPtr { - if issuePtr != nil { - issues = append(issues, *issuePtr) - } - } + issues := models.IssuesPtrToIssues(issuesPtr) return issues, nil } diff --git a/internal/service/service.go b/internal/service/service.go index 344ac92..f36730e 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,16 +1,21 @@ package service import ( + "context" + "fmt" + "os" + "time" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" - "context" - "time" + "github.com/charmbracelet/huh" "github.com/google/uuid" "github.com/steveyegge/beads" ) type Config struct { + RootCmd string WebAddress string BeadsDBPath string IssuePrefix string @@ -26,6 +31,11 @@ type Services struct { func NewServices(ctx context.Context, config Config) (*Services, func(), error) { var cleanupFuncs []func() + if !initialized(config.BeadsDBPath) { + fmt.Println("PM is not initialized") + os.Exit(0) + } + store, err := beads.NewSQLiteStorage(ctx, config.BeadsDBPath) if err != nil { return nil, nil, err @@ -61,3 +71,24 @@ func runCleanup(funcs []func()) { fn() } } + +func initialized(beadsPath string) bool { + _, err := os.Stat(beadsPath) + + if os.IsNotExist(err) { + var initialize bool + + huh.NewForm( + huh.NewGroup( + huh.NewConfirm().Title("PM is not initialized in this directory!"). + Description("Do you want to initialize it here?"). + Value(&initialize), + ), + ).WithTheme(huh.ThemeBase16()).WithAccessible(true).Run() + + if !initialize { + return false + } + } + return true +} diff --git a/pkg/cli/commands/ls.go b/pkg/cli/commands/ls.go new file mode 100644 index 0000000..76e68aa --- /dev/null +++ b/pkg/cli/commands/ls.go @@ -0,0 +1,91 @@ +package commands + +import ( + "strings" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" +) + +var ( + titleFlag string + descriptionFlag string + statusFlag string + typeFlag string + priorityFlag int + limit int = 25 +) + +const ( + lsExamples = `pm ls [id|title|description] +pm ls --status open --type bug +pm ls --title "New feature" --desc "feature description" +pm ls -p 1 -l 10` +) + +var getIssuesCmd = &cobra.Command{ + Use: "ls [search query]", + Short: "List all issues", + Long: `List all issues in the project management system.`, + Aliases: []string{"list", "search"}, + Example: lsExamples, + Args: cobra.MinimumNArgs(0), + RunE: runGetIssuesCmd, +} + +func runGetIssuesCmd(cmd *cobra.Command, args []string) error { + + queryArg := strings.Join(args, " ") + + filter := models.IssueFilter{ + TitleSearch: titleFlag, + DescriptionContains: descriptionFlag, + Limit: limit, + } + + if cmd.Flags().Changed("status") { + s := models.Status(statusFlag) + filter.Status = &s + } + if cmd.Flags().Changed("type") { + t := models.IssueType(typeFlag) + filter.IssueType = &t + } + if cmd.Flags().Changed("priority") { + filter.Priority = &priorityFlag + } + + issuesPtr, err := svc.Beads.SearchIssues(cmd.Context(), queryArg, filter) + if err != nil { + return err + } + + issues := models.IssuesPtrToIssues(issuesPtr) + + models.PrintIssues(issues) + + return nil +} + +func init() { + getIssuesCmd.Flags().StringVar(&titleFlag, "title", "", "Filter issues by title") + getIssuesCmd.Flags().StringVarP(&descriptionFlag, "desc", "d", "", "Filter issues by description") + getIssuesCmd.Flags().StringVarP(&statusFlag, "status", "s", "", "Filter issues by status (open, closed, in_progress)") + getIssuesCmd.Flags().StringVarP(&typeFlag, "type", "t", "", "Filter issues by type (bug, feature, task)") + getIssuesCmd.Flags().IntVarP(&priorityFlag, "priority", "p", 0, "Filter issues by priority (0-5)") + getIssuesCmd.Flags().IntVarP(&limit, "limit", "l", 25, "Limit the number of issues returned") + + getIssuesCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault + }) + + getIssuesCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault + }) + + getIssuesCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault + }) + + rootCmd.AddCommand(getIssuesCmd) +} diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go new file mode 100644 index 0000000..eb054c0 --- /dev/null +++ b/pkg/cli/commands/read.go @@ -0,0 +1,66 @@ +package commands + +import ( + "strings" + + "github.com/spf13/cobra" +) + +var getIssueCmd = &cobra.Command{ + Use: "describe [issue ID]", + Aliases: []string{"get", "read"}, + Short: "Get issue details", + Long: `Get issue details by ID`, + RunE: runGetCmd, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeIssues, +} + +func runGetCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) + if err != nil { + return err + } + + if issue == nil { + cmd.Printf("Issue with ID '%s' not found\n", issueID) + return nil + } + + cmd.Printf("Title: %s\n", issue.Title) + cmd.Printf("Description: %s\n", issue.Description) + cmd.Printf("Status: %s\n", issue.Status) + cmd.Printf("Type: %s\n", issue.IssueType) + cmd.Printf("Priority: %d\n", issue.Priority) + + return nil +} + +func init() { + rootCmd.AddCommand(getIssueCmd) +} + +func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if svc == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + issues, err := svc.Beads.AllIssues(cmd.Context()) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var completions []string + for _, issue := range issues { + + if strings.HasPrefix(issue.ID, toComplete) { + completions = append(completions, issue.ID) + } else if strings.HasPrefix(issue.Title, toComplete) { + completions = append(completions, issue.ID) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp +} diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 75098e3..158cdbb 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -9,13 +9,13 @@ import ( var svc *service.Services var rootCmd = &cobra.Command{ - Use: "pm", Short: "Project Management CLI", Long: `Project Management CLI for managing issues and tasks.`, } func Execute(services *service.Services) error { svc = services + rootCmd.Use = svc.Config.RootCmd return rootCmd.Execute() } @@ -23,7 +23,7 @@ func init() { rootCmd.AddCommand(createCmd) rootCmd.AddCommand(deleteCmd) rootCmd.CompletionOptions.DisableDefaultCmd = false - rootCmd.AddGroup(&cobra.Group{ID: "other", Title: "Helping Commands"}) - rootCmd.SetCompletionCommandGroupID("other") - rootCmd.SetHelpCommandGroupID("other") + rootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"}) + rootCmd.SetCompletionCommandGroupID("help") + rootCmd.SetHelpCommandGroupID("help") }