From ba0115d1bc4a399a16c9d84d6b49def96e1640d0 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sat, 28 Feb 2026 16:39:50 +0100 Subject: [PATCH] lots of changes and reducing dependency imports --- cmd/pm/main.go | 4 +- cmd/survey/init.go | 2 +- cmd/survey/intro.go | 4 +- cmd/survey/runner.go | 3 +- cmd/survey/tasks/backlogRefinement.go | 7 +- cmd/survey/tasks/base.go | 56 +++++---- cmd/survey/tasks/codingTask.go | 38 +----- cmd/survey/tasks/createIssue.go | 67 ++++------- cmd/survey/tasks/dependencyManagement.go | 7 +- cmd/survey/tasks/gitTask.go | 22 +--- cmd/survey/tasks/issueTriage.go | 8 +- cmd/survey/tasks/milestoneTracking.go | 8 +- cmd/survey/tasks/priorityManagement.go | 8 +- cmd/survey/tasks/reportGeneration.go | 8 +- cmd/survey/tasks/sprintPlanning.go | 9 +- cmd/survey/tasks/stakeholderUpdate.go | 8 +- cmd/survey/tasks/teamCapacity.go | 8 +- cmd/tui/main.go | 4 +- cmd/web/main.go | 4 +- go.mod | 1 - go.sum | 2 - internal/commands/issues/completion.go | 6 +- internal/commands/issues/create.go | 8 +- internal/commands/issues/list.go | 8 +- internal/commands/issues/root.go | 13 +- internal/commands/issues/update.go | 8 +- internal/commands/survey/root.go | 14 ++- internal/commands/survey/start.go | 6 +- .../{service/interfaces.go => models/app.go} | 16 +-- internal/{service => models}/config.go | 2 +- internal/models/errors.go | 5 + internal/models/interface.go | 21 ++++ internal/models/{issue.go => issueBuilder.go} | 0 internal/models/statistics.go | 11 +- .../ui/types.go => internal/models/task.go | 32 ++++- internal/service/{service.go => app.go} | 11 +- internal/{service => storage}/beads.go | 4 +- .../{commands/util.go => utils/completion.go} | 4 +- internal/utils/expect.go | 111 ++++++++++++++++++ pkg/cli/cli.go | 9 +- pkg/repl/repl.go | 10 +- pkg/task/runner.go | 27 +++-- pkg/task/types.go | 36 ------ pkg/task/ui/questionnaire.go | 3 + pkg/task/ui/task.go | 3 + pkg/tui/tui.go | 11 +- pkg/tui/views/dashboard/issue_list.go | 22 ++-- pkg/tui/views/dashboard/model.go | 30 ++--- pkg/tui/views/dashboard/operations.go | 14 +-- pkg/tui/views/views.go | 4 +- pkg/web/handler/issues.go | 2 +- pkg/web/handler/task.go | 44 +++++-- pkg/web/server/routes.go | 1 + pkg/web/web.go | 11 +- 54 files changed, 458 insertions(+), 327 deletions(-) rename internal/{service/interfaces.go => models/app.go} (58%) rename internal/{service => models}/config.go (98%) create mode 100644 internal/models/errors.go create mode 100644 internal/models/interface.go rename internal/models/{issue.go => issueBuilder.go} (100%) rename pkg/task/ui/types.go => internal/models/task.go (57%) rename internal/service/{service.go => app.go} (87%) rename internal/{service => storage}/beads.go (88%) rename internal/{commands/util.go => utils/completion.go} (80%) create mode 100644 internal/utils/expect.go delete mode 100644 pkg/task/types.go diff --git a/cmd/pm/main.go b/cmd/pm/main.go index 8fb8863..3746bcb 100644 --- a/cmd/pm/main.go +++ b/cmd/pm/main.go @@ -5,12 +5,12 @@ import ( issuesCmd "github.com/LazyBachelor/LazyPM/internal/commands/issues" surveyCmd "github.com/LazyBachelor/LazyPM/internal/commands/survey" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/cli" ) func main() { - if err := cli.NewCli(issuesCmd.RootCmd).Run(context.Background(), service.BaseConfig); err != nil { + if err := cli.NewCli(issuesCmd.RootCmd).Run(context.Background(), models.BaseConfig); err != nil { return } } diff --git a/cmd/survey/init.go b/cmd/survey/init.go index 1f6bdba..cd8e346 100644 --- a/cmd/survey/init.go +++ b/cmd/survey/init.go @@ -11,7 +11,7 @@ import ( ) func initializeServices(ctx context.Context) (*service.App, func(), error) { - return service.NewServices(ctx, tasks.BaseConfig().WithAutoInit(true)) + return service.NewApp(ctx, tasks.BaseConfig().WithAutoInit(true)) } func initInterfaces() map[string]task.Interface { diff --git a/cmd/survey/intro.go b/cmd/survey/intro.go index 83824d7..7bc2547 100644 --- a/cmd/survey/intro.go +++ b/cmd/survey/intro.go @@ -3,8 +3,8 @@ package main import ( "strings" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" - "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -75,7 +75,7 @@ func (m introModel) Run() error { return err } if m, ok := model.(introModel); ok && m.userQuit { - return task.ErrUserQuit + return models.ErrUserQuit } return nil } diff --git a/cmd/survey/runner.go b/cmd/survey/runner.go index 227cb7e..17d4263 100644 --- a/cmd/survey/runner.go +++ b/cmd/survey/runner.go @@ -8,6 +8,7 @@ import ( "github.com/LazyBachelor/LazyPM/cmd/survey/tasks" surveyCmd "github.com/LazyBachelor/LazyPM/internal/commands/survey" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/spf13/cobra" ) @@ -70,7 +71,7 @@ func taskLoop(ctx context.Context, surveyTasks map[string]task.Tasker, interface } func returnIfUserQuit(err error, msg string) error { - if errors.Is(err, task.ErrUserQuit) { + if errors.Is(err, models.ErrUserQuit) { return nil } return fmt.Errorf("%s: %w", msg, err) diff --git a/cmd/survey/tasks/backlogRefinement.go b/cmd/survey/tasks/backlogRefinement.go index 0fd6173..aaec36a 100644 --- a/cmd/survey/tasks/backlogRefinement.go +++ b/cmd/survey/tasks/backlogRefinement.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -122,7 +122,8 @@ func (t *BacklogRefinementTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *BacklogRefinementTask) Validate(ctx context.Context) (bool, error) { +func (t *BacklogRefinementTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() - return EndTaskWithTimeout(&t.done, "Backlog refinement task completed!", 5*time.Second) + return expect.Complete() } diff --git a/cmd/survey/tasks/base.go b/cmd/survey/tasks/base.go index ee5a909..aae7fff 100644 --- a/cmd/survey/tasks/base.go +++ b/cmd/survey/tasks/base.go @@ -1,9 +1,9 @@ package tasks import ( - "fmt" - "time" + "context" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/repl" "github.com/LazyBachelor/LazyPM/pkg/task" @@ -13,22 +13,27 @@ import ( "github.com/charmbracelet/huh" ) +type ValidationFeedback = models.ValidationFeedback +type InterfaceType = models.InterfaceType +type Interface = task.Interface + const ( - InterfaceTUI task.InterfaceType = "tui" - InterfaceREPL task.InterfaceType = "repl" - InterfaceWeb task.InterfaceType = "web" + InterfaceTypeCLI = models.InterfaceTypeCLI + InterfaceTypeTUI = models.InterfaceTypeTUI + InterfaceTypeWeb = models.InterfaceTypeWeb + InterfaceTypeREPL = models.InterfaceTypeREPL ) -func InterfaceToType(it task.Interface) task.InterfaceType { +func InterfaceToType(it Interface) InterfaceType { switch it.(type) { case *repl.REPL: - return InterfaceREPL + return InterfaceTypeREPL case *tui.Tui: - return InterfaceTUI + return InterfaceTypeTUI case *web.Web: - return InterfaceWeb + return InterfaceTypeWeb default: - return task.InterfaceType("unknown") + return InterfaceType("unknown") } } @@ -42,7 +47,7 @@ func BaseDetails() taskui.TaskDetails { } func BaseConfig() task.Config { - return service.BaseConfig + return models.BaseConfig } func ClearIssues(app *service.App) error { @@ -74,31 +79,42 @@ func Question(fields ...huh.Field) *huh.Group { } func ReplQuestion(interfaceType task.InterfaceType, fields ...huh.Field) *huh.Group { - if interfaceType != InterfaceREPL { + if interfaceType != InterfaceTypeREPL { return nil } return huh.NewGroup(fields...) } func WebQuestion(interfaceType task.InterfaceType, fields ...huh.Field) *huh.Group { - if interfaceType != InterfaceWeb { + if interfaceType != InterfaceTypeWeb { return nil } return huh.NewGroup(fields...) } func TUIQuestion(interfaceType task.InterfaceType, fields ...huh.Field) *huh.Group { - if interfaceType != InterfaceTUI { + if interfaceType != InterfaceTypeTUI { return nil } return huh.NewGroup(fields...) } -func EndTaskWithTimeout(taskDone *bool, message string, timeout time.Duration) (bool, error) { - if !*taskDone { - *taskDone = true - return false, fmt.Errorf("%s", message) +// FetchIssues retrives all issues from the app and returns those that are relevant for validation, +// excluding the setup issue. It also updates the setup issue with the latest data from the app. +func FetchIssues(app *service.App, setupIssue *models.Issue) ([]*models.Issue, error) { + issues, err := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) + if err != nil { + return nil, err } - time.Sleep(timeout) - return true, nil + + var relevantIssues []*models.Issue + for _, issue := range issues { + if issue.ID != setupIssue.ID { + relevantIssues = append(relevantIssues, issue) + } else { + *setupIssue = *issue + } + } + + return relevantIssues, nil } diff --git a/cmd/survey/tasks/codingTask.go b/cmd/survey/tasks/codingTask.go index 743c85b..fea4be5 100644 --- a/cmd/survey/tasks/codingTask.go +++ b/cmd/survey/tasks/codingTask.go @@ -2,12 +2,10 @@ package tasks import ( "context" - "fmt" "os" - "strings" - "time" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -84,34 +82,8 @@ func (t *CodingTask) Setup(ctx context.Context) error { return nil } -func (t *CodingTask) Validate(ctx context.Context) (bool, error) { - file, err := os.ReadFile("./code.txt") - if err != nil { - return false, err - } - - if string(file) == "" { - return false, fmt.Errorf("the file is empty") - } - - code, ok := strings.CutPrefix(string(file), textFileContent) - if !ok { - return false, fmt.Errorf("the file content is not in the expected format") - } - - code = strings.TrimSpace(code) - - if !strings.Contains(code, "package coding") { - return false, fmt.Errorf("the code does not belong to the 'coding' package") - } - - if !strings.Contains(code, "func Add") { - return false, fmt.Errorf("the function 'Add' is not defined") - } - - if !strings.Contains(code, "return") { - return false, fmt.Errorf("the function does not contain a return statement") - } - - return EndTaskWithTimeout(&t.done, "Task completed!", 5*time.Second) +func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + expect.Assert(true, "This task is always valid") + return expect.Complete() } diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go index 50daa3b..ac85642 100644 --- a/cmd/survey/tasks/createIssue.go +++ b/cmd/survey/tasks/createIssue.go @@ -3,10 +3,10 @@ package tasks import ( "context" "fmt" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" ) @@ -25,9 +25,9 @@ Your task: Make sure to fill out all the necessary details to help others understand the work item.` type CreateIssueTask struct { - done bool - app *service.App - setupTask *models.Issue + done bool + app *service.App + setupIssue *models.Issue } func NewCreateIssueTask(app *service.App) *CreateIssueTask { @@ -51,59 +51,34 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error { return err } - t.setupTask = models.NewBaseIssue(). + t.setupIssue = models.NewBaseIssue(). WithTitle("Create a New Issue"). WithDescription(description). Build() - return t.app.Issues.CreateIssue(ctx, t.setupTask, "") + return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *CreateIssueTask) Validate(ctx context.Context) (bool, error) { - issues, err := t.app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) +func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + issues, err := FetchIssues(t.app, t.setupIssue) if err != nil { - return false, err + return expect.ValidationFeedback } - if t.setupTask.Assignee == "" { - return false, fmt.Errorf("issue not assigned to self") + expect.NotEmptyString(t.setupIssue.Assignee, + fmt.Sprintf("%s is not assigned to anyone", t.setupIssue.ID)) + + if len(issues) == 0 { + expect.Fail("No new issues created") + return expect.ValidationFeedback } - if len(issues) < 2 { - return false, fmt.Errorf("issue not created") - } + issue := issues[0] - var createdIssue *models.Issue - for i := range issues { - if issues[i].ID != "pm-abc" { - createdIssue = issues[i] - break - } - } + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") + expect.NotEmptyString(issue.Description, "Issue description should not be empty") - if createdIssue == nil { - return false, fmt.Errorf("new issue not found") - } - - if createdIssue.Title == "" { - return false, fmt.Errorf("issue title is empty") - } - - if createdIssue.Description == "" { - return false, fmt.Errorf("issue description is empty") - } - - if createdIssue.Assignee == "" { - return false, fmt.Errorf("issue not assigned to self") - } - - if createdIssue.Status != models.StatusClosed { - return false, fmt.Errorf("issue status is not closed") - } - - if t.setupTask.Status != models.StatusClosed { - return false, fmt.Errorf("setup issue status is not closed") - } - - return EndTaskWithTimeout(&t.done, "Task completed!", 5*time.Second) + return expect.Complete() } diff --git a/cmd/survey/tasks/dependencyManagement.go b/cmd/survey/tasks/dependencyManagement.go index 375e1b5..8c0dc23 100644 --- a/cmd/survey/tasks/dependencyManagement.go +++ b/cmd/survey/tasks/dependencyManagement.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -114,7 +114,8 @@ func (t *DependencyManagementTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *DependencyManagementTask) Validate(ctx context.Context) (bool, error) { +func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() - return EndTaskWithTimeout(&t.done, "Dependency management task completed!", 5*time.Second) + return expect.Complete() } diff --git a/cmd/survey/tasks/gitTask.go b/cmd/survey/tasks/gitTask.go index d1b6d81..bbcfe3d 100644 --- a/cmd/survey/tasks/gitTask.go +++ b/cmd/survey/tasks/gitTask.go @@ -3,12 +3,11 @@ package tasks import ( "context" "errors" - "fmt" "os" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -89,23 +88,10 @@ func (t *GitTask) Setup(ctx context.Context) error { return nil } -func (t *GitTask) Validate(ctx context.Context) (bool, error) { +func (t *GitTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() - if t.setupIssue == nil { - return false, errors.New("setup issue not found") - } - - if t.setupIssue.Assignee == "" { - return false, fmt.Errorf("issue not assigned to self") - } - - if t.setupIssue.Status != models.StatusInProgress { - return false, fmt.Errorf("issue status is not in progress") - } - - // Git related validation can be added here, such as checking for commits, branches, etc. - - return EndTaskWithTimeout(&t.done, "Task completed!", 5*time.Second) + return expect.Complete() } func (t *GitTask) initRepo() (*git.Repository, error) { diff --git a/cmd/survey/tasks/issueTriage.go b/cmd/survey/tasks/issueTriage.go index a515f63..cf2de89 100644 --- a/cmd/survey/tasks/issueTriage.go +++ b/cmd/survey/tasks/issueTriage.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -115,6 +115,8 @@ func (t *IssueTriageTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *IssueTriageTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Issue triage task completed!", 5*time.Second) +func (t *IssueTriageTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/survey/tasks/milestoneTracking.go b/cmd/survey/tasks/milestoneTracking.go index a90a135..130fb1d 100644 --- a/cmd/survey/tasks/milestoneTracking.go +++ b/cmd/survey/tasks/milestoneTracking.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -115,6 +115,8 @@ func (t *MilestoneTrackingTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *MilestoneTrackingTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Milestone tracking task completed!", 5*time.Second) +func (t *MilestoneTrackingTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/survey/tasks/priorityManagement.go b/cmd/survey/tasks/priorityManagement.go index 05cfbd5..325eae1 100644 --- a/cmd/survey/tasks/priorityManagement.go +++ b/cmd/survey/tasks/priorityManagement.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -115,6 +115,8 @@ func (t *PriorityManagementTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *PriorityManagementTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Priority management task completed!", 5*time.Second) +func (t *PriorityManagementTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/survey/tasks/reportGeneration.go b/cmd/survey/tasks/reportGeneration.go index 60c9bac..6745c6e 100644 --- a/cmd/survey/tasks/reportGeneration.go +++ b/cmd/survey/tasks/reportGeneration.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -117,6 +117,8 @@ func (t *ReportGenerationTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *ReportGenerationTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Report generation task completed!", 5*time.Second) +func (t *ReportGenerationTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/survey/tasks/sprintPlanning.go b/cmd/survey/tasks/sprintPlanning.go index 4015cef..05175cb 100644 --- a/cmd/survey/tasks/sprintPlanning.go +++ b/cmd/survey/tasks/sprintPlanning.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -116,6 +116,9 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *SprintPlanningTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Sprint planning task completed!", 5*time.Second) +func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() + } diff --git a/cmd/survey/tasks/stakeholderUpdate.go b/cmd/survey/tasks/stakeholderUpdate.go index bf2f6b3..7cc4290 100644 --- a/cmd/survey/tasks/stakeholderUpdate.go +++ b/cmd/survey/tasks/stakeholderUpdate.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -110,6 +110,8 @@ func (t *StakeholderUpdateTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *StakeholderUpdateTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Stakeholder update task completed!", 5*time.Second) +func (t *StakeholderUpdateTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/survey/tasks/teamCapacity.go b/cmd/survey/tasks/teamCapacity.go index 4790f80..73dd842 100644 --- a/cmd/survey/tasks/teamCapacity.go +++ b/cmd/survey/tasks/teamCapacity.go @@ -2,10 +2,10 @@ package tasks import ( "context" - "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" "github.com/charmbracelet/huh" @@ -122,6 +122,8 @@ func (t *TeamCapacityTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -func (t *TeamCapacityTask) Validate(ctx context.Context) (bool, error) { - return EndTaskWithTimeout(&t.done, "Team capacity task completed!", 5*time.Second) +func (t *TeamCapacityTask) Validate(ctx context.Context) ValidationFeedback { + expect := utils.NewExpector() + + return expect.Complete() } diff --git a/cmd/tui/main.go b/cmd/tui/main.go index b381675..ec31695 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -3,12 +3,12 @@ package main import ( "context" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui" ) func main() { - if err := tui.NewTui().Run(context.Background(), service.BaseConfig); err != nil { + if err := tui.NewTui().Run(context.Background(), models.BaseConfig); err != nil { return } } diff --git a/cmd/web/main.go b/cmd/web/main.go index be466fa..cd8f4b5 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -5,12 +5,12 @@ import ( "fmt" "os" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web" ) func main() { - if err := web.NewWeb().Run(context.Background(), service.BaseConfig); err != nil { + if err := web.NewWeb().Run(context.Background(), models.BaseConfig); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } diff --git a/go.mod b/go.mod index a85c344..d87e220 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.25.6 require ( charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 - github.com/google/uuid v1.6.0 github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 ) diff --git a/go.sum b/go.sum index 596fcea..30fb654 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 h1:sb7SfxZfN+U9OHC61tcS98Ge0zY9uEkW5CP6KB4YVHg= github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320/go.mod h1:LFP8N8y5ORkifb+LZuOVNZYlJuV3WqdXCjxX5pGUaNI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/commands/issues/completion.go b/internal/commands/issues/completion.go index c5075ea..04c54a8 100644 --- a/internal/commands/issues/completion.go +++ b/internal/commands/issues/completion.go @@ -26,18 +26,18 @@ func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]str } // GetIssueCompletions fetches issues matching the toComplete string for shell completion. -func GetIssueCompletions(ctx context.Context, toComplete string) ([]models.Issue, cobra.ShellCompDirective) { +func GetIssueCompletions(ctx context.Context, toComplete string) ([]*models.Issue, cobra.ShellCompDirective) { app := AppFromContext(ctx) if app == nil { return nil, cobra.ShellCompDirectiveNoFileComp } - issues, err := app.Issues.AllIssues(ctx) + issues, err := app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } - var completions []models.Issue + var completions []*models.Issue for _, issue := range issues { if strings.HasPrefix(issue.ID, toComplete) { completions = append(completions, issue) diff --git a/internal/commands/issues/create.go b/internal/commands/issues/create.go index df51434..97d0262 100644 --- a/internal/commands/issues/create.go +++ b/internal/commands/issues/create.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/LazyBachelor/LazyPM/internal/commands" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/charmbracelet/huh" "github.com/spf13/cobra" @@ -112,7 +112,7 @@ func init() { 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.RegisterFlagCompletionFunc("type", commands.CompletionFunc(typeOptions)) - CreateCmd.RegisterFlagCompletionFunc("status", commands.CompletionFunc(statusOptions)) - CreateCmd.RegisterFlagCompletionFunc("priority", commands.CompletionFunc(priorityRange)) + CreateCmd.RegisterFlagCompletionFunc("type", utils.CompletionFunc(typeOptions)) + CreateCmd.RegisterFlagCompletionFunc("status", utils.CompletionFunc(statusOptions)) + CreateCmd.RegisterFlagCompletionFunc("priority", utils.CompletionFunc(priorityRange)) } diff --git a/internal/commands/issues/list.go b/internal/commands/issues/list.go index 0872bf8..14c226c 100644 --- a/internal/commands/issues/list.go +++ b/internal/commands/issues/list.go @@ -3,8 +3,8 @@ package issuesCmd import ( "strings" - "github.com/LazyBachelor/LazyPM/internal/commands" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/spf13/cobra" ) @@ -79,7 +79,7 @@ func init() { ListCmd.Flags().IntVarP(&listFlags.limit, "limit", "l", 25, "Limit the number of issues returned") - ListCmd.RegisterFlagCompletionFunc("status", commands.CompletionFunc(statusOptions)) - ListCmd.RegisterFlagCompletionFunc("type", commands.CompletionFunc(typeOptions)) - ListCmd.RegisterFlagCompletionFunc("priority", commands.CompletionFunc(priorityRange)) + ListCmd.RegisterFlagCompletionFunc("status", utils.CompletionFunc(statusOptions)) + ListCmd.RegisterFlagCompletionFunc("type", utils.CompletionFunc(typeOptions)) + ListCmd.RegisterFlagCompletionFunc("priority", utils.CompletionFunc(priorityRange)) } diff --git a/internal/commands/issues/root.go b/internal/commands/issues/root.go index f606c29..e08d51f 100644 --- a/internal/commands/issues/root.go +++ b/internal/commands/issues/root.go @@ -4,7 +4,7 @@ import ( "bytes" "context" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/charmbracelet/fang" "github.com/spf13/cobra" @@ -14,8 +14,9 @@ type contextKey string const appKey contextKey = "app" -// app is a package-level variable used during command setup -var app *service.App +type App = models.App + +var app *App // Flags struct to hold command-line flag values for issues. type Flags struct { @@ -44,14 +45,14 @@ var RootCmd = &cobra.Command{ // SetApp sets the app variable for use in command execution. // Must be called before executing any commands to ensure services are available. -func SetApp(application *service.App) { +func SetApp(application *App) { app = application RootCmd.Use = app.Config.RootCmd } // AppFromContext retrieves the App from the command context -func AppFromContext(ctx context.Context) *service.App { - if a, ok := ctx.Value(appKey).(*service.App); ok { +func AppFromContext(ctx context.Context) *App { + if a, ok := ctx.Value(appKey).(*App); ok { return a } // Fallback to package-level app (for testing or edge cases) diff --git a/internal/commands/issues/update.go b/internal/commands/issues/update.go index b28e544..05dceb0 100644 --- a/internal/commands/issues/update.go +++ b/internal/commands/issues/update.go @@ -3,8 +3,8 @@ package issuesCmd import ( "fmt" - "github.com/LazyBachelor/LazyPM/internal/commands" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/spf13/cobra" ) @@ -62,9 +62,9 @@ func init() { 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-5)") - UpdateCmd.RegisterFlagCompletionFunc("type", commands.CompletionFunc(typeOptions)) - UpdateCmd.RegisterFlagCompletionFunc("status", commands.CompletionFunc(statusOptions)) - UpdateCmd.RegisterFlagCompletionFunc("priority", commands.CompletionFunc(priorityRange)) + UpdateCmd.RegisterFlagCompletionFunc("type", utils.CompletionFunc(typeOptions)) + UpdateCmd.RegisterFlagCompletionFunc("status", utils.CompletionFunc(statusOptions)) + UpdateCmd.RegisterFlagCompletionFunc("priority", utils.CompletionFunc(priorityRange)) } func getUpdateValues(cmd *cobra.Command) (map[string]interface{}, error) { diff --git a/internal/commands/survey/root.go b/internal/commands/survey/root.go index 23986bf..ebf6edf 100644 --- a/internal/commands/survey/root.go +++ b/internal/commands/survey/root.go @@ -3,10 +3,14 @@ package surveyCmd import ( "context" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/spf13/cobra" ) +type App = models.App + +var app *App + const appKey string = "app" const long string = `Project Management Interface Survey @@ -18,8 +22,6 @@ This survey will present you with a series of tasks to complete using various in Please answer the questions honestly and to the best of your ability. Your responses will be kept confidential and used solely for research purposes.` -var app *service.App - // RootCmd is the base command for the survey CLI. var RootCmd = &cobra.Command{ Use: "survey", @@ -31,12 +33,12 @@ var RootCmd = &cobra.Command{ }, } -func SetApp(application *service.App) { +func SetApp(application *App) { app = application } -func AppFromContext(ctx context.Context) *service.App { - if a, ok := ctx.Value(appKey).(*service.App); ok { +func AppFromContext(ctx context.Context) *App { + if a, ok := ctx.Value(appKey).(*App); ok { return a } return app diff --git a/internal/commands/survey/start.go b/internal/commands/survey/start.go index 8541017..62ebba8 100644 --- a/internal/commands/survey/start.go +++ b/internal/commands/survey/start.go @@ -1,7 +1,7 @@ package surveyCmd import ( - "github.com/LazyBachelor/LazyPM/internal/commands" + "github.com/LazyBachelor/LazyPM/internal/utils" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/spf13/cobra" ) @@ -20,6 +20,6 @@ var StartCmd = &cobra.Command{ func init() { StartCmd.Flags().StringVarP(&Task, "task", "t", "", "Specify task.") StartCmd.Flags().StringVarP(&InterfaceType, "interface", "i", "", "Specify interface.") - StartCmd.RegisterFlagCompletionFunc("task", commands.CompletionFunc(task.ListTasks())) - StartCmd.RegisterFlagCompletionFunc("interface", commands.CompletionFunc(task.ListInterfaces())) + StartCmd.RegisterFlagCompletionFunc("task", utils.CompletionFunc(task.ListTasks())) + StartCmd.RegisterFlagCompletionFunc("interface", utils.CompletionFunc(task.ListInterfaces())) } diff --git a/internal/service/interfaces.go b/internal/models/app.go similarity index 58% rename from internal/service/interfaces.go rename to internal/models/app.go index 31c0648..e43a674 100644 --- a/internal/service/interfaces.go +++ b/internal/models/app.go @@ -1,10 +1,9 @@ -package service +package models import ( "context" "log/slog" - "github.com/LazyBachelor/LazyPM/internal/models" "github.com/steveyegge/beads" ) @@ -15,22 +14,19 @@ type App struct { Logger *slog.Logger + Tasks *map[string]Tasker + Interfaces *map[string]Interface + CurrentFeedback *ValidationFeedback } type IssueService interface { - beads.Storage - AllIssues(ctx context.Context) ([]models.Issue, error) + beads.Storage // Want to get rid of this dependency, but it provides a lot of useful methods that would be a pain to re-implement right now DeleteIssues() error } type StatsService interface { Load(ctx context.Context) error Save(ctx context.Context) error - GetStatistics() (models.Statistics, error) -} - -type ValidationFeedback struct { - Success bool - Message string + GetStatistics() (Statistics, error) } diff --git a/internal/service/config.go b/internal/models/config.go similarity index 98% rename from internal/service/config.go rename to internal/models/config.go index ff3af43..c5a6ce5 100644 --- a/internal/service/config.go +++ b/internal/models/config.go @@ -1,4 +1,4 @@ -package service +package models type Config struct { AutoInit bool diff --git a/internal/models/errors.go b/internal/models/errors.go new file mode 100644 index 0000000..0629c43 --- /dev/null +++ b/internal/models/errors.go @@ -0,0 +1,5 @@ +package models + +import "fmt" + +var ErrUserQuit = fmt.Errorf("user quit") diff --git a/internal/models/interface.go b/internal/models/interface.go new file mode 100644 index 0000000..c7c017b --- /dev/null +++ b/internal/models/interface.go @@ -0,0 +1,21 @@ +package models + +import "context" + +// Interface represents a user interface for interacting with the application. +// It can be any implementation that fulfills the Run method. +type Interface interface { + Run(context.Context, Config) error +} + +// InterfaceType represents the type of user interface +type InterfaceType string + +// Interface represents a user interface for interacting with the application. +// Keep lower case to avoid confusion with Go's built-in interfaces. +const ( + InterfaceTypeCLI InterfaceType = "cli" + InterfaceTypeTUI InterfaceType = "tui" + InterfaceTypeWeb InterfaceType = "web" + InterfaceTypeREPL InterfaceType = "repl" +) diff --git a/internal/models/issue.go b/internal/models/issueBuilder.go similarity index 100% rename from internal/models/issue.go rename to internal/models/issueBuilder.go diff --git a/internal/models/statistics.go b/internal/models/statistics.go index c9e127a..8ad224a 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -2,19 +2,10 @@ package models import ( "time" - - "github.com/google/uuid" -) - -type InterfaceType string - -const ( - InterfaceTypeCLI InterfaceType = "CLI" - InterfaceTypeWeb InterfaceType = "Web" ) type Statistics struct { - ID uuid.UUID `json:"id"` + ID int `json:"id"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Duration time.Duration `json:"duration"` diff --git a/pkg/task/ui/types.go b/internal/models/task.go similarity index 57% rename from pkg/task/ui/types.go rename to internal/models/task.go index 421bdb5..9f0538b 100644 --- a/pkg/task/ui/types.go +++ b/internal/models/task.go @@ -1,6 +1,34 @@ -package taskui +package models -import "github.com/charmbracelet/huh" +import ( + "context" + + "github.com/charmbracelet/huh" +) + +type Tasker interface { + Config() Config + Details() TaskDetails + Setup(context.Context) error + Questions(InterfaceType) Questions + Validate(context.Context) ValidationFeedback +} + +type ValidationFeedback struct { + Success bool + Message string + Checks []Check +} + +type Check struct { + Message string + Valid bool +} + +type ValidatedInterface interface { + Interface + SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) +} type TaskDetails struct { Title string diff --git a/internal/service/service.go b/internal/service/app.go similarity index 87% rename from internal/service/service.go rename to internal/service/app.go index 99966ae..0ddcef6 100644 --- a/internal/service/service.go +++ b/internal/service/app.go @@ -10,12 +10,13 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" "github.com/charmbracelet/huh" - - "github.com/google/uuid" "github.com/steveyegge/beads" ) -func NewServices(ctx context.Context, config Config) (*App, func(), error) { +type App = models.App +type Config = models.Config + +func NewApp(ctx context.Context, config Config) (*App, func(), error) { var cleanupFuncs []func() if !config.AutoInit { @@ -31,14 +32,14 @@ func NewServices(ctx context.Context, config Config) (*App, func(), error) { } cleanupFuncs = append(cleanupFuncs, func() { store.Close() }) - beadsSvc, err := NewBeadsService(ctx, store, config.IssuePrefix) + beadsSvc, err := storage.NewBeadsIssueStorage(ctx, store, config.IssuePrefix) if err != nil { return nil, nil, err } cleanupFuncs = append(cleanupFuncs, func() { beadsSvc.Close() }) statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{ - ID: uuid.New(), + ID: 0, StartTime: time.Now(), }) diff --git a/internal/service/beads.go b/internal/storage/beads.go similarity index 88% rename from internal/service/beads.go rename to internal/storage/beads.go index e2b0652..3d9ecf9 100644 --- a/internal/service/beads.go +++ b/internal/storage/beads.go @@ -1,4 +1,4 @@ -package service +package storage import ( "context" @@ -13,7 +13,7 @@ type BeadsService struct { beads.Storage } -func NewBeadsService(ctx context.Context, storage beads.Storage, prefix string) (*BeadsService, error) { +func NewBeadsIssueStorage(ctx context.Context, storage beads.Storage, prefix string) (*BeadsService, error) { issue_prefix, err := storage.GetConfig(ctx, "issue_prefix") if err != nil || issue_prefix == "" { if err := storage.SetConfig(ctx, "issue_prefix", prefix); err != nil { diff --git a/internal/commands/util.go b/internal/utils/completion.go similarity index 80% rename from internal/commands/util.go rename to internal/utils/completion.go index 033a022..662660e 100644 --- a/internal/commands/util.go +++ b/internal/utils/completion.go @@ -1,8 +1,8 @@ -package commands +package utils import "github.com/spf13/cobra" -// completionFunc returns a function that provides shell completion for the given options. +// CompletionFunc returns a function that provides shell completion for the given options. func CompletionFunc(options []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return options, cobra.ShellCompDirectiveDefault diff --git a/internal/utils/expect.go b/internal/utils/expect.go new file mode 100644 index 0000000..23077b7 --- /dev/null +++ b/internal/utils/expect.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/LazyBachelor/LazyPM/internal/models" +) + +type ValidationFeedback = models.ValidationFeedback +type Check = models.Check + +func NewCheck(message string, valid bool) Check { + return Check{ + Message: message, + Valid: valid, + } +} + +type Expector struct { + ValidationFeedback +} + +func NewExpector() *Expector { + return &Expector{ + ValidationFeedback: ValidationFeedback{ + Success: false, + Checks: []Check{}, + }, + } +} + +func (e *Expector) Complete() ValidationFeedback { + e.Success = len(e.Errors()) == 0 + return e.ValidationFeedback +} + +func (e *Expector) Fail(message string) ValidationFeedback { + e.Checks = append(e.Checks, NewCheck(message, false)) + return e.ValidationFeedback +} + +func (e *Expector) Errors() []error { + var errors []error + for _, check := range e.Checks { + if !check.Valid { + errors = append(errors, fmt.Errorf("%s", check.Message)) + } + } + return errors +} + +func (e *Expector) Assert(condition bool, message string) *Expector { + check := NewCheck(message, condition) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) Nil(value any, message string) *Expector { + check := NewCheck(message, value == nil) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) NotNil(value any, message string) *Expector { + check := NewCheck(message, value != nil) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) Contains(s, substr, message string) *Expector { + check := NewCheck(message, strings.Contains(s, substr)) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) NotContains(s, substr, message string) *Expector { + check := NewCheck(message, !strings.Contains(s, substr)) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) Equal(a, b any, message string) *Expector { + check := NewCheck(message, a == b) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) NotEqual(a, b any, message string) *Expector { + check := NewCheck(message, a != b) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) NotEmptyString(s string, message string) *Expector { + check := NewCheck(message, s != "") + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) EmptySlice(s []*any, message string) *Expector { + check := NewCheck(message, len(s) == 0) + e.Checks = append(e.Checks, check) + return e +} + +func (e *Expector) NotEmptySlice(s []*any, message string) *Expector { + check := NewCheck(message, len(s) > 0) + e.Checks = append(e.Checks, check) + return e +} diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 1b9d01f..4ef5c12 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -4,14 +4,15 @@ package cli import ( "context" - "github.com/LazyBachelor/LazyPM/internal/commands/issues" + issuesCmd "github.com/LazyBachelor/LazyPM/internal/commands/issues" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/charmbracelet/fang" "github.com/spf13/cobra" ) // Config is an alias for service.Config, used to configure the CLI. -type Config = service.Config +type Config = models.Config type CLI struct { RootCmd *cobra.Command @@ -25,7 +26,7 @@ func NewCli(rootCmd *cobra.Command) *CLI { // Run initializes the services and executes the CLI commands. func (c *CLI) Run(ctx context.Context, config Config) error { - app, cleanup, err := service.NewServices(ctx, config) + app, cleanup, err := service.NewApp(ctx, config) if err != nil { return err } @@ -44,7 +45,7 @@ func (c *CLI) Run(ctx context.Context, config Config) error { // RunWithArgs initializes the services and executes the CLI commands with the provided arguments. func (c *CLI) RunWithArgs(ctx context.Context, config Config, args []string) error { - app, cleanup, err := service.NewServices(ctx, config) + app, cleanup, err := service.NewApp(ctx, config) if err != nil { return err } diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 52ff236..46e616e 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -8,6 +8,7 @@ import ( "strings" issuesCmd "github.com/LazyBachelor/LazyPM/internal/commands/issues" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/style" "github.com/LazyBachelor/LazyPM/pkg/cli" @@ -17,6 +18,9 @@ import ( "golang.org/x/term" ) +type App = models.App +type ValidationFeedback = models.ValidationFeedback + const ( ReplHelp = `Type 'pm help' for available PM commands. Type 'pm status' to check task progress. @@ -28,7 +32,7 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.` type REPL struct { feedbackChan chan task.ValidationFeedback quitChan chan bool - app *service.App + app *App currentFeedback task.ValidationFeedback exitRequested bool @@ -50,7 +54,7 @@ func (r *REPL) Run(ctx context.Context, config cli.Config) error { defer term.Restore(int(os.Stdin.Fd()), oldState) // Initialize services for beads, config and stats. - app, cleanup, err := service.NewServices(ctx, config) + app, cleanup, err := service.NewApp(ctx, config) if err != nil { return fmt.Errorf("failed to initialize services: %w", err) } @@ -118,7 +122,7 @@ func (r *REPL) watchValidation() { r.currentFeedback = feedback // Update app's CurrentFeedback so status command can access it if r.app != nil { - r.app.CurrentFeedback = &service.ValidationFeedback{ + r.app.CurrentFeedback = &ValidationFeedback{ Success: feedback.Success, Message: feedback.Message, } diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 3af739f..ab0cad8 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -5,10 +5,23 @@ import ( "fmt" "time" + "github.com/LazyBachelor/LazyPM/internal/models" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" tea "github.com/charmbracelet/bubbletea" ) +type Config = models.Config + +type Tasker = models.Tasker + +type Interface = models.Interface +type InterfaceType = models.InterfaceType + +type ValidatedInterface = models.ValidatedInterface +type ValidationFeedback = models.ValidationFeedback + +var ErrUserQuit = models.ErrUserQuit + // RunTask orchestrates the complete task execution flow: // 1. Setup the task // 2. Show task intro screen @@ -85,21 +98,15 @@ func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan Valida for { select { case <-ticker.C: - ok, err := t.Validate(ctx) - feedback := ValidationFeedback{ - Success: ok, - } - if ok { + feedback := t.Validate(ctx) + if feedback.Success { feedback.Message = "Task completed successfully!" feedbackChan <- feedback + time.Sleep(4 * time.Second) doneChan <- true return } - if err != nil { - feedback.Message = err.Error() - } else { - feedback.Message = "Task not yet complete" - } + feedback.Message = "Task not completed!" feedbackChan <- feedback case <-quitChan: return diff --git a/pkg/task/types.go b/pkg/task/types.go deleted file mode 100644 index 085d3bb..0000000 --- a/pkg/task/types.go +++ /dev/null @@ -1,36 +0,0 @@ -package task - -import ( - "context" - "fmt" - - "github.com/LazyBachelor/LazyPM/internal/service" - taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" -) - -type Config = service.Config -type InterfaceType string - -type Interface interface { - Run(context.Context, Config) error -} - -type Tasker interface { - Config() Config - Details() taskui.TaskDetails - Questions(InterfaceType) taskui.Questions - Setup(context.Context) error - Validate(context.Context) (bool, error) -} - -type ValidationFeedback struct { - Success bool - Message string -} - -type ValidatedInterface interface { - Interface - SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) -} - -var ErrUserQuit = fmt.Errorf("user quit") diff --git a/pkg/task/ui/questionnaire.go b/pkg/task/ui/questionnaire.go index 69fd767..55a7a9d 100644 --- a/pkg/task/ui/questionnaire.go +++ b/pkg/task/ui/questionnaire.go @@ -2,11 +2,14 @@ package taskui import ( "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" ) +type Questions = models.Questions + type QuestionnaireModel struct { Questions form *huh.Form diff --git a/pkg/task/ui/task.go b/pkg/task/ui/task.go index 858b3bc..9c250a4 100644 --- a/pkg/task/ui/task.go +++ b/pkg/task/ui/task.go @@ -5,11 +5,14 @@ import ( "strings" "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) +type TaskDetails = models.TaskDetails + type TaskModel struct { TaskDetails keys TaskHelpKeys diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 25c08c8..7cd99f5 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -3,16 +3,17 @@ package tui import ( "context" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" - "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/tui/views" tea "github.com/charmbracelet/bubbletea" ) -type Config = service.Config +type Config = models.Config +type ValidationFeedback = models.ValidationFeedback type Tui struct { - feedbackChan chan task.ValidationFeedback + feedbackChan chan ValidationFeedback quitChan chan bool } @@ -21,7 +22,7 @@ func NewTui() *Tui { } func (t *Tui) Run(ctx context.Context, config Config) error { - app, cleanup, err := service.NewServices(ctx, config) + app, cleanup, err := service.NewApp(ctx, config) if err != nil { return err } @@ -45,7 +46,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error { return nil } -func (t *Tui) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan chan bool) { +func (t *Tui) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) { t.feedbackChan = feedbackChan t.quitChan = quitChan } diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go index 0818566..500fd78 100644 --- a/pkg/tui/views/dashboard/issue_list.go +++ b/pkg/tui/views/dashboard/issue_list.go @@ -78,14 +78,14 @@ func renderHeaders(cols []TableColumn) string { } func NewIssueList(app *service.App, width, height int) IssueList { - issues, err := app.Issues.AllIssues(context.Background()) + issues, err := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return IssueList{} } listIssues := []ListIssue{} for _, issue := range issues { - listIssues = append(listIssues, ListIssue{Issue: issue}) + listIssues = append(listIssues, ListIssue{Issue: *issue}) } items := make([]list.Item, len(listIssues)) @@ -111,11 +111,11 @@ func NewIssueList(app *service.App, width, height int) IssueList { } } -func NewIssueListFromIssues(app *service.App, issues []models.Issue, width, height int) IssueList { +func NewIssueListFromIssues(app *service.App, issues []*models.Issue, width, height int) IssueList { // for making an IssueList from a pre-existing list of issues. listIssues := make([]list.Item, len(issues)) for i, issue := range issues { - listIssues[i] = ListIssue{Issue: issue} + listIssues[i] = ListIssue{Issue: *issue} } l := list.New(listIssues, NewIssueListDelegate(width), width, height) l.SetShowTitle(false) @@ -134,9 +134,9 @@ func NewIssueListFromIssues(app *service.App, issues []models.Issue, width, heig } } -func OpenAndInProgressOnly(issues []models.Issue) []models.Issue { +func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue { // used to display open & in-progress issues in the first window in the dashboard - out := make([]models.Issue, 0, len(issues)) + out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { if issue.Status == models.StatusOpen || issue.Status == models.StatusInProgress { out = append(out, issue) @@ -146,9 +146,9 @@ func OpenAndInProgressOnly(issues []models.Issue) []models.Issue { return out } -func ClosedOnly(issues []models.Issue) []models.Issue { +func ClosedOnly(issues []*models.Issue) []*models.Issue { // used to display issues in the second window in the dashboard - out := make([]models.Issue, 0, len(issues)) + out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { if issue.Status == models.StatusClosed { out = append(out, issue) @@ -158,7 +158,7 @@ func ClosedOnly(issues []models.Issue) []models.Issue { return out } -func sortByPriorityDesc(issues []models.Issue) { +func sortByPriorityDesc(issues []*models.Issue) { // sorts issues by priority, highest first. sort.Slice(issues, func(i, j int) bool { return issues[i].Priority > issues[j].Priority @@ -248,10 +248,10 @@ func (l IssueList) FilterState() list.FilterState { return l.list.FilterState() } -func (l *IssueList) SetIssues(issues []models.Issue) tea.Cmd { +func (l *IssueList) SetIssues(issues []*models.Issue) tea.Cmd { listIssues := make([]list.Item, len(issues)) for i, issue := range issues { - listIssues[i] = ListIssue{Issue: issue} + listIssues[i] = ListIssue{Issue: *issue} } return l.list.SetItems(listIssues) } diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index 4c87d05..cfe2023 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -3,15 +3,15 @@ package dashboard import ( "context" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" - "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type ValidationFeedbackMsg struct { - Feedback task.ValidationFeedback + Feedback models.ValidationFeedback } type Model struct { @@ -24,9 +24,9 @@ type Model struct { app *service.App width int height int - focusedWindow int // 0 = main (display issues), 1 = closed issues + focusedWindow int // 0 = main (display issues), 1 = closed issues focusedPaneMain int // 0 = list, 1 = detail - focusedPaneClosed int + focusedPaneClosed int editingTitle bool // true while we are editing a title titleInput textinput.Model editingIssueID string @@ -42,18 +42,18 @@ type Model struct { deleteConfirmIndex int choosingStatus bool // true while choosing a status - statusIssueID string - choosingPriority bool // true while choosing a priority - priorityIssueID string - choosingType bool // true while choosing a type - typeIssueID string - feedbackChan chan task.ValidationFeedback - quitChan chan bool - currentFeedback task.ValidationFeedback - showComplete bool + statusIssueID string + choosingPriority bool // true while choosing a priority + priorityIssueID string + choosingType bool // true while choosing a type + typeIssueID string + feedbackChan chan models.ValidationFeedback + quitChan chan bool + currentFeedback models.ValidationFeedback + showComplete bool } -func NewDashboard(app *service.App, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *Model { +func NewDashboard(app *service.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model { m := &Model{ header: NewHeader("Project Manager Dashboard"), keyMap: defaultDashboardKeyMap, @@ -67,7 +67,7 @@ func NewDashboard(app *service.App, feedbackChan chan task.ValidationFeedback, q quitChan: quitChan, } - allIssues, _ := app.Issues.AllIssues(context.Background()) + allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) m.issueList = NewIssueListFromIssues(app, OpenAndInProgressOnly(allIssues), 0, 0) m.issueDetail = NewIssueDetail() m.closedIssueList = NewIssueListFromIssues(app, ClosedOnly(allIssues), 0, 0) diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index e8a744e..c820244 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -113,7 +113,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. */ - issues, err := m.app.Issues.AllIssues(context.Background()) + issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return nil } @@ -121,7 +121,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues)) for _, issue := range issues { if issue.ID == issueID { - m.issueDetail.SetIssue(issue) + m.issueDetail.SetIssue(*issue) break } } @@ -184,7 +184,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil || msg.Issue == nil { return m, nil } - issues, err := m.app.Issues.AllIssues(context.Background()) + issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return m, nil } @@ -197,7 +197,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for _, issue := range issues { // Prefer an issue that matches the created issue's title when ID is not yet known. if issue.Title == msg.Issue.Title { - selectedIssue = &issue + selectedIssue = issue break } } @@ -211,7 +211,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - issues, err := m.app.Issues.AllIssues(context.Background()) + issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return m, nil } @@ -226,7 +226,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Determine which list to use for the next selection. - var targetIssues []models.Issue + var targetIssues []*models.Issue if m.focusedWindow == 0 { targetIssues = openIssues if len(targetIssues) == 0 && len(closedIssues) > 0 { @@ -254,7 +254,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newIndex = len(targetIssues) - 1 } selectedIssue := targetIssues[newIndex] - m.issueDetail.SetIssue(selectedIssue) + m.issueDetail.SetIssue(*selectedIssue) return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} }) diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go index f60f508..62d51de 100644 --- a/pkg/tui/views/views.go +++ b/pkg/tui/views/views.go @@ -1,11 +1,11 @@ package views import ( + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" - "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" ) -func NewDashboardView(app *service.App, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *dashboard.Model { +func NewDashboardView(app *service.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *dashboard.Model { return dashboard.NewDashboard(app, feedbackChan, quitChan) } diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index b12b300..76f1e94 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -68,7 +68,7 @@ func ListIssues(w http.ResponseWriter, r *http.Request) { app := App(r) hx := HTMX(r) - issues, err := app.Issues.AllIssues(r.Context()) + issues, err := app.Issues.SearchIssues(r.Context(), "", models.IssueFilter{}) if err != nil { http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) return diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index da31308..62c64cd 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -1,29 +1,55 @@ package handler import ( + "context" + "io" "net/http" - "github.com/LazyBachelor/LazyPM/pkg/task" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" + "github.com/a-h/templ" + "github.com/donseba/go-htmx" ) -var taskFeedback task.ValidationFeedback +type ValidationFeedback = models.ValidationFeedback -func SetTaskFeedback(feedback task.ValidationFeedback) { +var taskFeedback ValidationFeedback + +func SetTaskFeedback(feedback ValidationFeedback) { taskFeedback = feedback } func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { hx := HTMX(r) - if hx.IsHxRequest() { - if taskFeedback.Success { - hx.WriteString("
Task completed successfully!
") - } else { - hx.WriteString("
" + taskFeedback.Message + "
") - } + hx.WriteString(`` + taskFeedback.Message + ``) return } w.Header().Set("Content-Type", "application/json") hx.WriteJSON(taskFeedback) } + +func HandleTaskStatusModal(w http.ResponseWriter, r *http.Request) { + err := components.Modal(components.ModalProps{ + ID: "task-status-modal", + Title: "Task Status", + Content: feedbackList(HTMX(r), taskFeedback), + Open: true, + }).Render(r.Context(), w) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func feedbackList(hx *htmx.Handler, feedback ValidationFeedback) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + for _, check := range feedback.Checks { + if !check.Valid { + hx.WriteString(`

` + check.Message + `

`) + } + } + return nil + }) +} diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 90b3fb7..11e90c5 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -31,6 +31,7 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r.Get("/", handler.DashboardHandler) r.Get("/status", handler.HandleTaskStatus) + r.Get("/status/modal", handler.HandleTaskStatusModal) r.Route("/issues", func(r chi.Router) { r.Get("/", handler.ListIssues) diff --git a/pkg/web/web.go b/pkg/web/web.go index 3eb1fe1..bc419d4 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -9,17 +9,18 @@ import ( "strings" "time" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/utils" - "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/web/handler" "github.com/LazyBachelor/LazyPM/pkg/web/server" ) -type Config = service.Config +type Config = models.Config +type ValidationFeedback = models.ValidationFeedback type Web struct { - feedbackChan chan task.ValidationFeedback + feedbackChan chan ValidationFeedback quitChan chan bool } @@ -31,7 +32,7 @@ func NewWeb() *Web { var assets embed.FS func (w *Web) Run(ctx context.Context, config Config) error { - app, cleanup, err := service.NewServices(ctx, config) + app, cleanup, err := service.NewApp(ctx, config) if err != nil { return err } @@ -84,7 +85,7 @@ func (w *Web) Run(ctx context.Context, config Config) error { } } -func (w *Web) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan chan bool) { +func (w *Web) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) { w.feedbackChan = feedbackChan w.quitChan = quitChan }