From d3911ec1f9b3c5f58801a97e2c07703791f063fe Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:48:09 +0100 Subject: [PATCH 01/12] add ls command for listing all issues with query and filter tags --- pkg/cli/commands/ls.go | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pkg/cli/commands/ls.go 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) +} From 7e0c45e9a2629c71f8b532132730a6045318c404 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:49:23 +0100 Subject: [PATCH 02/12] add helper for printing issue list. add helper for converting issue pointer to issue --- internal/models/beads.go | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index d009e86..83ec37b 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,35 @@ const ( EventLabelRemoved = beads.EventLabelRemoved EventCompacted = beads.EventCompacted ) + +func IssuesPtrToIssues(issuePtr []*Issue) []Issue { + issues := make([]Issue, 0, len(issuePtr)) + for _, issue := range issuePtr { + issues = append(issues, *issue) + } + 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, 5), + 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() +} From 98df41ff54a158e53f81f5df2c8b9d14736663f4 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:50:01 +0100 Subject: [PATCH 03/12] add read/describe command for viewing issue details --- pkg/cli/commands/read.go | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pkg/cli/commands/read.go diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go new file mode 100644 index 0000000..0116b96 --- /dev/null +++ b/pkg/cli/commands/read.go @@ -0,0 +1,63 @@ +package commands + +import ( + "strings" + + "github.com/spf13/cobra" +) + +var getIssueCmd = &cobra.Command{ + Use: "describe [issue ID]", + Aliases: []string{"get", "read"}, + Short: "Gets issue details", + Long: `Gets 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 + } + + 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.Title, toComplete) { + completions = append(completions, issue.ID) + } + + if strings.HasPrefix(issue.ID, toComplete) { + completions = append(completions, issue.ID) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp +} From 6caf654af9d471a2314799803eb4408b66803dcc Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:24:24 +0100 Subject: [PATCH 04/12] add variable to service config to store rootcmd name --- internal/service/service.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/service/service.go b/internal/service/service.go index 344ac92..c6afc25 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,16 +1,18 @@ package service import ( - "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/internal/storage" "context" "time" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/storage" + "github.com/google/uuid" "github.com/steveyegge/beads" ) type Config struct { + RootCmd string WebAddress string BeadsDBPath string IssuePrefix string From 12748ede0050b0cf99e2295c8b3bf7ce8f88f6ff Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:25:34 +0100 Subject: [PATCH 05/12] use new rootCmd name var --- cmd/{cli => pm}/main.go | 4 ++-- pkg/cli/commands/root.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) rename cmd/{cli => pm}/main.go (78%) 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/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index ebe772f..edee54c 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -9,20 +9,21 @@ 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() } func init() { rootCmd.AddCommand(createCmd) + 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") } From d3207c26fb31ba0667c8e2a33edf4d5d55004bf0 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:26:02 +0100 Subject: [PATCH 06/12] add completion scripts for command line autocomplete --- Makefile | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) 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 From 07af46c46b9ac55ffad9fdd7f55809d61e875fc3 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:48:41 +0100 Subject: [PATCH 07/12] Update pkg/cli/commands/read.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/read.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 0116b96..53e1ba7 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -50,12 +50,10 @@ func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]str var completions []string for _, issue := range issues { - if strings.HasPrefix(issue.Title, toComplete) { - completions = append(completions, issue.ID) - } - if strings.HasPrefix(issue.ID, toComplete) { completions = append(completions, issue.ID) + } else if strings.HasPrefix(issue.Title, toComplete) { + completions = append(completions, issue.ID) } } From a731e375d3b09d8e5d59606e63651887ba884e0e Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:51:19 +0100 Subject: [PATCH 08/12] Update pkg/cli/commands/read.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/read.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 53e1ba7..42e292b 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -9,8 +9,8 @@ import ( var getIssueCmd = &cobra.Command{ Use: "describe [issue ID]", Aliases: []string{"get", "read"}, - Short: "Gets issue details", - Long: `Gets issue details by ID`, + Short: "Get issue details", + Long: `Get issue details by ID`, RunE: runGetCmd, Args: cobra.ExactArgs(1), ValidArgsFunction: completeIssues, From 893c9e3bd69639a5dcc9663aa87842b4cc119038 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:55:20 +0100 Subject: [PATCH 09/12] chack for nil pointer in helper use helper for converting to issue --- internal/models/beads.go | 6 ++++-- internal/service/beads.go | 7 +------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index 83ec37b..0e9c663 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -81,8 +81,10 @@ const ( func IssuesPtrToIssues(issuePtr []*Issue) []Issue { issues := make([]Issue, 0, len(issuePtr)) - for _, issue := range issuePtr { - issues = append(issues, *issue) + for _, issuePtr := range issuePtr { + if issuePtr != nil { + issues = append(issues, *issuePtr) + } } return issues } 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 } From 1c248b2c8d1eddfa874d5b94c007c199d5ba96f7 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 14:27:44 +0100 Subject: [PATCH 10/12] decrease truncate leangth for issueID --- internal/models/beads.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index 0e9c663..bae490a 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -92,7 +92,7 @@ func IssuesPtrToIssues(issuePtr []*Issue) []Issue { func FormatIssueRow(issue Issue) string { return fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%d", - truncate.String(issue.ID, 5), + truncate.String(issue.ID, 10), truncate.StringWithTail(issue.Title, 25, "..."), truncate.StringWithTail(issue.Description, 40, "..."), issue.Status, From d6884b6ac5ce89f728ab9c725fef864f9b039edb Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 14:28:15 +0100 Subject: [PATCH 11/12] nil check issue so it does not crash when issue not found --- pkg/cli/commands/read.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 42e292b..eb054c0 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -24,6 +24,11 @@ func runGetCmd(cmd *cobra.Command, args []string) error { 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) From 2215b5ad34dc8273ddb0db2cc2146871f31741b9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 15:53:15 +0100 Subject: [PATCH 12/12] add promt to user to confirm initializaton in direcory --- internal/service/service.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/service/service.go b/internal/service/service.go index c6afc25..f36730e 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -2,10 +2,13 @@ package service import ( "context" + "fmt" + "os" "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" + "github.com/charmbracelet/huh" "github.com/google/uuid" "github.com/steveyegge/beads" @@ -28,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 @@ -63,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 +}