Merge pull request #75 from LazyBachelor/LPM-140

LPM-140 The Sprinting Update
Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer.

Changes:

Introduces sprint entities in storage and exposes sprint operations via IssueService.
Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes.
Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
This commit is contained in:
Robin Olsen
2026-03-23 11:26:04 -07:00
committed by GitHub
parent 06404708ba
commit d900a5fcd3
33 changed files with 2378 additions and 771 deletions

View File

@@ -11,7 +11,7 @@ import (
// Variables for completion options and functions.
var (
typeOptions = []string{"bug", "feature", "task", "chore"}
statusOptions = []string{"open", "closed", "in_progress", "blocked", "ready_to_sprint"}
statusOptions = []string{"open", "closed", "in_progress", "blocked"}
priorityRange = []string{"0", "1", "2", "3", "4"}
)

View File

@@ -26,7 +26,7 @@ var CreateCmd = &cobra.Command{
Example: createCmdExample,
Args: cobra.MinimumNArgs(0),
Aliases: []string{"add"},
Aliases: []string{"add", "new"},
RunE: runCreateCmd,
}
@@ -112,7 +112,7 @@ func runCreateInteractive() error {
func init() {
//CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively")
CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description")
CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress, ready_to_sprint)")
CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)")
CreateCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)")
CreateCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)")
CreateCmd.Flags().StringVarP(&createFlags.assignee, "assignee", "a", "", "Issue assignee")

View File

@@ -74,7 +74,7 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error {
func init() {
ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title")
ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description")
ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress, ready_to_sprint)")
ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)")
ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)")
ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)")
ListCmd.Flags().StringVarP(&listFlags.assignee, "assignee", "a", "", "Filter issues by assignee")

View File

@@ -0,0 +1,322 @@
package issues
import (
"fmt"
"strconv"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/spf13/cobra"
)
// sprintFlags holds the flag values for sprint commands
type sprintFlags struct {
sprintNum int
issueID string
}
var sprintCmdFlags sprintFlags
// SprintCmd represents the sprint management command
var SprintCmd = &cobra.Command{
Use: "sprint",
Short: "Manage sprints",
Long: `Manage sprints - create, list, add/remove issues, and view sprint contents.`,
}
// SprintListCmd lists all sprints
var SprintListCmd = &cobra.Command{
Use: "list",
Short: "List all sprints",
Long: `List all sprints in the project.`,
Aliases: []string{"ls"},
RunE: runSprintListCmd,
}
// SprintCreateCmd creates a new sprint
var SprintCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a new sprint",
Long: `Create a new sprint for organizing issues.`,
Aliases: []string{"new"},
RunE: runSprintCreateCmd,
}
// SprintIssuesCmd lists issues in a sprint
var SprintIssuesCmd = &cobra.Command{
Use: "issues [sprint-num]",
Short: "List issues in a sprint",
Long: `List all issues assigned to a specific sprint. Use 'backlog' or omit to view the backlog.`,
Aliases: []string{"show", "view"},
Args: cobra.MaximumNArgs(1),
RunE: runSprintIssuesCmd,
}
// SprintAddCmd adds an issue to a sprint
var SprintAddCmd = &cobra.Command{
Use: "add [issue-id] [sprint-num]",
Short: "Add an issue to a sprint",
Long: `Add an issue to a sprint. If sprint-num is omitted, adds to backlog.`,
Args: cobra.RangeArgs(1, 2),
RunE: runSprintAddCmd,
ValidArgsFunction: completeIssues,
}
// SprintRemoveCmd removes an issue from a sprint
var SprintRemoveCmd = &cobra.Command{
Use: "remove [issue-id] [sprint-num]",
Short: "Remove an issue from a sprint",
Long: `Remove an issue from a sprint. If sprint-num is omitted, removes from backlog.`,
Aliases: []string{"rm"},
Args: cobra.RangeArgs(1, 2),
RunE: runSprintRemoveCmd,
ValidArgsFunction: completeIssues,
}
// SprintBacklogCmd shows the backlog sprint
var SprintBacklogCmd = &cobra.Command{
Use: "backlog",
Short: "Show backlog issues",
Long: `Show all issues in the backlog sprint.`,
RunE: runSprintBacklogCmd,
}
// SprintDeleteCmd deletes a sprint
var SprintDeleteCmd = &cobra.Command{
Use: "delete [sprint-num]",
Short: "Delete a sprint",
Long: `Delete a sprint by its number. Issues in the sprint will not be deleted.`,
Aliases: []string{"del"},
Args: cobra.MaximumNArgs(1),
RunE: runSprintDeleteCmd,
}
func runSprintListCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
sprints, err := app.Issues.GetSprints(cmd.Context())
if err != nil {
return fmt.Errorf("failed to get sprints: %w", err)
}
if len(sprints) == 0 {
cmd.Println("No sprints found.")
return nil
}
backlogNum, _ := app.Issues.GetBacklogSprint(cmd.Context())
cmd.Println("Sprints:")
for _, sprintNum := range sprints {
label := ""
if sprintNum == backlogNum {
label = " (backlog)"
}
cmd.Printf(" Sprint %d%s\n", sprintNum, label)
}
return nil
}
func runSprintCreateCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
sprintNum, err := app.Issues.AddSprint(cmd.Context())
if err != nil {
return fmt.Errorf("failed to create sprint: %w", err)
}
cmd.Printf("Created sprint %d\n", sprintNum)
return nil
}
func runSprintIssuesCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
ctx := cmd.Context()
var sprintNum int
var isBacklog bool
var err error
if len(args) == 0 || args[0] == "backlog" {
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
if err != nil {
return fmt.Errorf("failed to get backlog sprint: %w", err)
}
isBacklog = true
} else {
sprintNum, err = strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid sprint number: %s", args[0])
}
isBacklog = false
}
var issues []*models.Issue
if isBacklog {
issues, err = app.Issues.GetIssuesNotInAnySprint(ctx)
} else {
issues, err = app.Issues.GetIssuesBySprint(ctx, sprintNum)
}
if err != nil {
return fmt.Errorf("failed to get issues in sprint %d: %w", sprintNum, err)
}
if isBacklog {
cmd.Printf("Backlog (%d issues):\n", len(issues))
} else {
cmd.Printf("Sprint %d (%d issues):\n", sprintNum, len(issues))
}
if len(issues) == 0 {
cmd.Println(" No issues in this sprint.")
return nil
}
issuesList := models.IssuesPtrToIssues(issues)
models.PrintIssues(issuesList)
return nil
}
func runSprintAddCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
ctx := cmd.Context()
issueID := args[0]
var sprintNum int
var err error
if len(args) == 1 {
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
if err != nil {
return fmt.Errorf("failed to get backlog sprint: %w", err)
}
} else {
sprintNum, err = strconv.Atoi(args[1])
if err != nil {
return fmt.Errorf("invalid sprint number: %s", args[1])
}
}
err = app.Issues.AddIssueToSprint(ctx, issueID, sprintNum)
if err != nil {
return fmt.Errorf("failed to add issue to sprint: %w", err)
}
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
if sprintNum == backlogNum {
cmd.Printf("Added issue %s to backlog\n", issueID)
} else {
cmd.Printf("Added issue %s to sprint %d\n", issueID, sprintNum)
}
return nil
}
func runSprintRemoveCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
ctx := cmd.Context()
issueID := args[0]
var sprintNum int
var err error
if len(args) == 1 {
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
if err != nil {
return fmt.Errorf("failed to get backlog sprint: %w", err)
}
} else {
sprintNum, err = strconv.Atoi(args[1])
if err != nil {
return fmt.Errorf("invalid sprint number: %s", args[1])
}
}
err = app.Issues.RemoveIssueFromSprint(ctx, issueID, sprintNum)
if err != nil {
return fmt.Errorf("failed to remove issue from sprint: %w", err)
}
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
if sprintNum == backlogNum {
cmd.Printf("Removed issue %s from backlog\n", issueID)
} else {
cmd.Printf("Removed issue %s from sprint %d\n", issueID, sprintNum)
}
return nil
}
func runSprintBacklogCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
ctx := cmd.Context()
issues, err := app.Issues.GetIssuesNotInAnySprint(ctx)
if err != nil {
return fmt.Errorf("failed to get backlog issues: %w", err)
}
cmd.Printf("Backlog (%d issues):\n", len(issues))
if len(issues) == 0 {
cmd.Println(" No issues in backlog.")
return nil
}
issuesList := models.IssuesPtrToIssues(issues)
models.PrintIssues(issuesList)
return nil
}
func runSprintDeleteCmd(cmd *cobra.Command, args []string) error {
app := AppFromContext(cmd.Context())
ctx := cmd.Context()
var sprintNum int
var err error
if len(args) == 0 {
sprintNum, err = app.Issues.GetBacklogSprint(ctx)
if err != nil {
return fmt.Errorf("failed to get backlog sprint: %w", err)
}
} else {
sprintNum, err = strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid sprint number: %s", args[0])
}
}
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
if sprintNum == backlogNum {
return fmt.Errorf("cannot delete the backlog sprint")
}
err = app.Issues.RemoveSprint(ctx, sprintNum)
if err != nil {
return fmt.Errorf("failed to delete sprint: %w", err)
}
cmd.Printf("Deleted sprint %d\n", sprintNum)
return nil
}
func init() {
SprintCmd.AddCommand(SprintListCmd)
SprintCmd.AddCommand(SprintBacklogCmd)
SprintCmd.AddCommand(SprintCreateCmd)
SprintCmd.AddCommand(SprintIssuesCmd)
SprintCmd.AddCommand(SprintAddCmd)
SprintCmd.AddCommand(SprintRemoveCmd)
SprintCmd.AddCommand(SprintDeleteCmd)
RootCmd.AddCommand(SprintCmd)
}

View File

@@ -66,7 +66,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error {
func init() {
UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title")
UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description")
UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress, ready_to_sprint)")
UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)")
UpdateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)")
UpdateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-4)")
UpdateCmd.Flags().StringVarP(&updateFlags.assignee, "assignee", "a", "", "New issue assignee")