lots of changes and reducing dependency imports

This commit is contained in:
Robin Olsen
2026-02-28 16:39:50 +01:00
parent 697b17e890
commit ba0115d1bc
54 changed files with 458 additions and 327 deletions

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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

View File

@@ -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()))
}

View File

@@ -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)
}

View File

@@ -1,4 +1,4 @@
package service
package models
type Config struct {
AutoInit bool

View File

@@ -0,0 +1,5 @@
package models
import "fmt"
var ErrUserQuit = fmt.Errorf("user quit")

View File

@@ -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"
)

View File

@@ -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"`

67
internal/models/task.go Normal file
View File

@@ -0,0 +1,67 @@
package models
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
Description string
TimeToComplete string
Difficulty string
}
func (td TaskDetails) WithTitle(title string) TaskDetails {
td.Title = title
return td
}
func (td TaskDetails) WithDescription(description string) TaskDetails {
td.Description = description
return td
}
func (td TaskDetails) WithTimeToComplete(time string) TaskDetails {
td.TimeToComplete = time
return td
}
func (td TaskDetails) WithDifficulty(difficulty string) TaskDetails {
td.Difficulty = difficulty
return td
}
type Questions []*huh.Group
func (q Questions) With(group *huh.Group) Questions {
if group == nil {
return q
}
return append(q, group)
}

View File

@@ -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(),
})

View File

@@ -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 {

View File

@@ -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

111
internal/utils/expect.go Normal file
View File

@@ -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
}