3 Commits

Author SHA1 Message Date
Robin Olsen
235b07f7ce use registry for interfaces also
add list interfaces command
2026-02-22 22:58:04 +01:00
Robin Olsen
78769396e7 forgot to commit this 2026-02-22 19:47:17 +01:00
Robin Olsen
33c8c05ae3 refactor to decouple commands form entrypoint for reusability and centralizing commands.
Increase reusability and composition
2026-02-22 19:46:55 +01:00
30 changed files with 386 additions and 325 deletions

View File

@@ -3,12 +3,24 @@ package main
import ( import (
"context" "context"
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/service"
"github.com/LazyBachelor/LazyPM/pkg/cli" "github.com/LazyBachelor/LazyPM/pkg/cli"
) )
func main() { func main() {
if err := cli.NewCli().Run(context.Background(), service.BaseConfig); err != nil { if err := cli.NewCli(issuesCmd.RootCmd).Run(context.Background(), service.BaseConfig); err != nil {
return return
} }
} }
func init() {
issuesCmd.RootCmd.AddCommand(issuesCmd.GetCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.ListCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.CloseCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.CreateCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.DeleteCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.UpdateCmd)
issuesCmd.RootCmd.AddCommand(surveyCmd.StatusCmd)
}

View File

@@ -1,98 +0,0 @@
package main
import (
"fmt"
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/spf13/cobra"
)
var interfaceType string
var stage int
var rootCmd = &cobra.Command{
Use: "survey",
Long: `Project Management Interface Survey
Thank you for participating in our survey!
We are gathering data on how users interact with different task management interfaces to better understand their preferences and compare their usability.
This survey will present you with a series of tasks to complete using various interfaces, including command-line, web-based, and terminal user interfaces.
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 startCmd = &cobra.Command{
Use: "start",
Short: "Start the user survey",
RunE: runStartCmd,
}
var submitCmd = &cobra.Command{
Use: "submit",
Short: "Submit your survey responses",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("Submitting responses and metrics...")
return nil
},
}
func runStartCmd(cmd *cobra.Command, args []string) error {
interfaces := initInterfaces()
svc, cleanup, err := initializeServices(cmd.Context())
if err != nil {
return returnIfUserQuit(err, "failed to initialize services")
}
defer cleanup()
surveyTasks := initTasks(svc)
if cmd.Flags().Changed("interface") {
if _, ok := interfaces[interfaceType]; !ok {
return fmt.Errorf("invalid interface, valid are (tui, repl, web)")
}
interfaces = map[string]task.Interface{
interfaceType: interfaces[interfaceType],
}
}
if cmd.Flags().Changed("stage") {
if stage < 1 || stage > len(surveyTasks) {
return fmt.Errorf("invalid stage")
}
if err := runTask(cmd.Context(), surveyTasks[stage-1], interfaces[interfaceType]); err != nil {
return err
}
return nil
}
if err := newIntroModel().Run(); err != nil {
return returnIfUserQuit(err, "failed to run intro")
}
if err := taskLoop(cmd.Context(), surveyTasks, interfaces); err != nil {
return returnIfUserQuit(err, "task loop failed")
}
return nil
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List available tasks",
RunE: func(cmd *cobra.Command, args []string) error {
for i, name := range task.List() {
cmd.Printf("%d. %s\n", i+1, name)
}
return nil
},
}
func init() {
rootCmd.CompletionOptions.DisableDefaultCmd = true
startCmd.Flags().StringVarP(&interfaceType, "interface", "i", "tui", "Specify interface.")
startCmd.Flags().IntVarP(&stage, "stage", "s", 1, "Run stage directly")
rootCmd.AddCommand(startCmd)
rootCmd.AddCommand(submitCmd)
rootCmd.AddCommand(listCmd)
}

View File

@@ -3,47 +3,38 @@ package main
import ( import (
"context" "context"
"github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/cli/repl"
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/LazyBachelor/LazyPM/pkg/tui"
"github.com/LazyBachelor/LazyPM/pkg/web"
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks" "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
"github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/task"
_ "github.com/LazyBachelor/LazyPM/cmd/survey/tasks" _ "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
) )
func init() {
task.Register("create_issue", func(app *service.App) task.Tasker {
return tasks.NewCreateIssueTask(app)
})
task.Register("coding_task", func(app *service.App) task.Tasker {
return tasks.NewCodingTask(app)
})
}
func initTasks(app *service.App) []task.Tasker {
var taskers []task.Tasker
for _, name := range task.List() {
t, err := task.Get(name, app)
if err != nil {
continue
}
taskers = append(taskers, t)
}
return taskers
}
func initializeServices(ctx context.Context) (*service.App, func(), error) { func initializeServices(ctx context.Context) (*service.App, func(), error) {
return service.NewServices(ctx, tasks.BaseConfig()) return service.NewServices(ctx, tasks.BaseConfig())
} }
func initInterfaces() map[string]task.Interface { func initInterfaces() map[string]task.Interface {
return map[string]task.Interface{ interfaces := make(map[string]task.Interface)
"repl": repl.NewRepl(), for _, name := range task.ListInterfaces() {
"tui": tui.NewTui(), i, err := task.GetInterface(name)
"web": web.NewWeb(), if err != nil {
continue
}
interfaces[name] = i
} }
return interfaces
}
func initTasks(app *service.App) []task.Tasker {
var taskList []task.Tasker
for _, name := range task.ListTasks() {
t, err := task.GetTasks(name, app)
if err != nil {
continue
}
taskList = append(taskList, t)
}
return taskList
} }

View File

@@ -3,14 +3,42 @@ package main
import ( import (
"context" "context"
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
surveyCmd "github.com/LazyBachelor/LazyPM/internal/commands/survey"
"github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/repl"
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/LazyBachelor/LazyPM/pkg/tui"
"github.com/LazyBachelor/LazyPM/pkg/web"
"github.com/charmbracelet/fang" "github.com/charmbracelet/fang"
) )
func main() { func main() {
ctx := context.Background() ctx := context.Background()
if err := fang.Execute(ctx, rootCmd, if err := fang.Execute(ctx, surveyCmd.RootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme)); err != nil { fang.WithColorSchemeFunc(fang.AnsiColorScheme)); err != nil {
return return
} }
} }
func init() {
task.RegisterInterface("tui", tui.NewTui())
task.RegisterInterface("web", web.NewWeb())
task.RegisterInterface("repl", repl.NewRepl())
surveyCmd.StartCmd.RunE = runStartCmd
surveyCmd.RootCmd.AddCommand(surveyCmd.StartCmd)
surveyCmd.RootCmd.AddCommand(surveyCmd.SubmitCmd)
surveyCmd.RootCmd.AddCommand(surveyCmd.StatusCmd)
surveyCmd.RootCmd.AddCommand(surveyCmd.ListTasksCmd)
surveyCmd.RootCmd.AddCommand(surveyCmd.ListInterfacesCmd)
task.RegisterTask("create_issue", func(app *service.App) task.Tasker {
return tasks.NewCreateIssueTask(app)
})
task.RegisterTask("coding_task", func(app *service.App) task.Tasker {
return tasks.NewCodingTask(app)
})
}

View File

@@ -7,28 +7,67 @@ import (
"math/rand" "math/rand"
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks" "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
surveyCmd "github.com/LazyBachelor/LazyPM/internal/commands/survey"
"github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/spf13/cobra"
) )
func runTask(ctx context.Context, t task.Tasker, i task.Interface) error { func runStartCmd(cmd *cobra.Command, args []string) error {
return task.RunTask(ctx, t, i, tasks.InterfaceToType(i)) interfaces := initInterfaces()
svc, cleanup, err := initializeServices(cmd.Context())
if err != nil {
return returnIfUserQuit(err, "failed to initialize services")
}
defer cleanup()
surveyTasks := initTasks(svc)
if cmd.Flags().Changed("interface") {
if _, ok := interfaces[surveyCmd.InterfaceType]; !ok {
return fmt.Errorf("invalid interface, valid are %v", task.ListInterfaces())
}
interfaces = map[string]task.Interface{
surveyCmd.InterfaceType: interfaces[surveyCmd.InterfaceType],
}
}
if cmd.Flags().Changed("stage") {
if surveyCmd.Task < 1 || surveyCmd.Task > len(surveyTasks) {
return fmt.Errorf("invalid stage")
}
if err := task.RunTask(cmd.Context(), surveyTasks[surveyCmd.Task-1],
interfaces[surveyCmd.InterfaceType], tasks.InterfaceToType(interfaces[surveyCmd.InterfaceType])); err != nil {
return err
}
return nil
}
if err := newIntroModel().Run(); err != nil {
return returnIfUserQuit(err, "failed to run intro")
}
if err := taskLoop(cmd.Context(), surveyTasks, interfaces); err != nil {
return returnIfUserQuit(err, "task loop failed")
}
return nil
} }
func taskLoop(ctx context.Context, surveyTasks []task.Tasker, interfaces map[string]task.Interface) error { func taskLoop(ctx context.Context, surveyTasks []task.Tasker, interfaces map[string]task.Interface) error {
var ifaceNames []string var iNames []string
for name := range interfaces { for name := range interfaces {
ifaceNames = append(ifaceNames, name) iNames = append(iNames, name)
} }
rand.Shuffle(len(ifaceNames), func(i, j int) { rand.Shuffle(len(iNames), func(i, j int) {
ifaceNames[i], ifaceNames[j] = ifaceNames[j], ifaceNames[i] iNames[i], iNames[j] = iNames[j], iNames[i]
}) })
for i, t := range surveyTasks { for i, t := range surveyTasks {
idx := i % len(ifaceNames) idx := i % len(iNames)
selected := interfaces[ifaceNames[idx]] selected := interfaces[iNames[idx]]
if err := runTask(ctx, t, selected); err != nil { if err := task.RunTask(ctx, t, selected, tasks.InterfaceToType(selected)); err != nil {
return err return err
} }
} }

View File

@@ -2,7 +2,7 @@ package tasks
import ( import (
"github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/cli/repl" "github.com/LazyBachelor/LazyPM/pkg/repl"
"github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/task"
taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
"github.com/LazyBachelor/LazyPM/pkg/tui" "github.com/LazyBachelor/LazyPM/pkg/tui"

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"fmt" "fmt"
@@ -7,9 +7,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// closeCmd represents the close command, // CloseCmd represents the close command,
// which allows users to close an existing issue by its ID. // which allows users to close an existing issue by its ID.
var closeCmd = &cobra.Command{ var CloseCmd = &cobra.Command{
Use: "close [id]", Use: "close [id]",
Short: "Close an existing issue", Short: "Close an existing issue",
Long: `Close an existing issue by its ID.`, Long: `Close an existing issue by its ID.`,
@@ -58,8 +58,3 @@ func runCloseCmd(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// init function to set up the close command and its flags.
func init() {
rootCmd.AddCommand(closeCmd)
}

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"context" "context"

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"fmt" "fmt"
@@ -18,8 +18,8 @@ const (
pm create Fix bug --desc "Bug description" --status in_progress --type bug --priority 5` pm create Fix bug --desc "Bug description" --status in_progress --type bug --priority 5`
) )
// createCmd represents the create command, which allows users to create a new issue with specified details. // CreateCmd represents the create command, which allows users to create a new issue with specified details.
var createCmd = &cobra.Command{ var CreateCmd = &cobra.Command{
Use: "create [title]", Use: "create [title]",
Short: "Create a new issue", Short: "Create a new issue",
Long: `Create a new issue with the specified details.`, Long: `Create a new issue with the specified details.`,
@@ -105,15 +105,13 @@ func runCreateInteractive() error {
// init function to set up the create command and its flags. // init function to set up the create command and its flags.
func init() { func init() {
createCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively") CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively")
createCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description") CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description")
createCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)") 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().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().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)")
createCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) CreateCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
createCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) CreateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
createCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) CreateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
rootCmd.AddCommand(createCmd)
} }

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"context" "context"
@@ -17,8 +17,8 @@ var (
deleteInteractive bool deleteInteractive bool
) )
// deleteCmd represents the delete command. // DeleteCmd represents the delete command.
var deleteCmd = &cobra.Command{ var DeleteCmd = &cobra.Command{
Use: "delete [id]", Use: "delete [id]",
Short: "Delete an existing issue", Short: "Delete an existing issue",
Long: `Delete an existing issue by its ID.`, Long: `Delete an existing issue by its ID.`,
@@ -125,8 +125,6 @@ func runDeleteInteractive(ctx context.Context) error {
// init function to set up the delete command and its flags. // init function to set up the delete command and its flags.
func init() { func init() {
deleteCmd.Flags().BoolVarP(&deleteInteractive, "interactive", "i", false, "Delete issues interactively") DeleteCmd.Flags().BoolVarP(&deleteInteractive, "interactive", "i", false, "Delete issues interactively")
deleteCmd.Flags().BoolVarP(&confirmDelete, "yes", "y", true, "Confirm deletion without prompt") DeleteCmd.Flags().BoolVarP(&confirmDelete, "yes", "y", true, "Confirm deletion without prompt")
rootCmd.AddCommand(deleteCmd)
} }

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"strings" "strings"
@@ -17,8 +17,8 @@ pm list --title "New feature" --desc "feature description"
pm list -p 1 -l 10` pm list -p 1 -l 10`
) )
// getIssuesCmd represents the get issues command. // ListCmd represents the get issues command.
var getIssuesCmd = &cobra.Command{ var ListCmd = &cobra.Command{
Use: "list [search query]", Use: "list [search query]",
Short: "List all issues", Short: "List all issues",
Long: `List all issues in the project management system.`, Long: `List all issues in the project management system.`,
@@ -70,17 +70,15 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error {
// init function to set up the get issues command and its flags. // init function to set up the get issues command and its flags.
func init() { func init() {
getIssuesCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title") ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title")
getIssuesCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description") ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description")
getIssuesCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)") ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)")
getIssuesCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)") ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)")
getIssuesCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)") ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)")
getIssuesCmd.Flags().IntVarP(&listFlags.limit, "limit", "l", 25, "Limit the number of issues returned") ListCmd.Flags().IntVarP(&listFlags.limit, "limit", "l", 25, "Limit the number of issues returned")
getIssuesCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) ListCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
getIssuesCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) ListCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
getIssuesCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) ListCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
rootCmd.AddCommand(getIssuesCmd)
} }

View File

@@ -1,12 +1,12 @@
package commands package issuesCmd
import ( import (
"github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/models"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// getIssueCmd represents the get issue command. // GetCmd represents the get issue command.
var getIssueCmd = &cobra.Command{ var GetCmd = &cobra.Command{
Use: "describe [issue ID]", Use: "describe [issue ID]",
Short: "Get issue details", Short: "Get issue details",
Long: `Get issue details by ID`, Long: `Get issue details by ID`,
@@ -44,5 +44,4 @@ func runGetCmd(cmd *cobra.Command, args []string) error {
// init function to set up the get issue command. // init function to set up the get issue command.
func init() { func init() {
rootCmd.AddCommand(getIssueCmd)
} }

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"bytes" "bytes"
@@ -29,8 +29,8 @@ type Flags struct {
priority int priority int
} }
// rootCmd is the base command for the CLI application. // RootCmd is the base command for the CLI application.
var rootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Short: "Project Management CLI", Short: "Project Management CLI",
Long: `Project Management CLI for managing issues and tasks.`, Long: `Project Management CLI for managing issues and tasks.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
@@ -45,7 +45,7 @@ var rootCmd = &cobra.Command{
// Must be called before executing any commands to ensure services are available. // Must be called before executing any commands to ensure services are available.
func SetApp(application *service.App) { func SetApp(application *service.App) {
app = application app = application
rootCmd.Use = app.Config.RootCmd RootCmd.Use = app.Config.RootCmd
} }
// AppFromContext retrieves the App from the command context // AppFromContext retrieves the App from the command context
@@ -57,37 +57,37 @@ func AppFromContext(ctx context.Context) *service.App {
return app return app
} }
// Execute executes the root command using the fang library.
func Execute() error {
return fang.Execute(context.Background(), rootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme))
}
// ExecuteArgs executes the command with the given arguments using the fang library. // ExecuteArgs executes the command with the given arguments using the fang library.
func ExecuteArgs(args []string) error { func ExecuteArgs(args []string) error {
rootCmd.SetArgs(args) RootCmd.SetArgs(args)
return fang.Execute(context.Background(), rootCmd, return fang.Execute(context.Background(), RootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme)) fang.WithColorSchemeFunc(fang.AnsiColorScheme))
} }
// ExecuteArgsString executes the command with the given arguments and returns the output as a string. // ExecuteArgsString executes the command with the given arguments and returns the output as a string.
// This is useful for testing command outputs and used in the REPL // This is useful for testing command outputs and used in the REPL
func ExecuteArgsString(args []string) (string, error) { func ExecuteArgsString(args []string) (string, error) {
return ExecuteArgsStringWithContext(context.Background(), args)
}
// ExecuteArgsStringWithContext executes the command with context and returns the output as a string.
func ExecuteArgsStringWithContext(ctx context.Context, args []string) (string, error) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
rootCmd.SetOut(buf) RootCmd.SetOut(buf)
rootCmd.SetErr(buf) RootCmd.SetErr(buf)
rootCmd.SetArgs(args) RootCmd.SetArgs(args)
RootCmd.SetContext(ctx)
err := rootCmd.Execute() err := RootCmd.Execute()
return buf.String(), err return buf.String(), err
} }
// init function to set up the command hierarchy and options. // init function to set up the command hierarchy and options.
func init() { func init() {
rootCmd.CompletionOptions.DisableDefaultCmd = false RootCmd.CompletionOptions.DisableDefaultCmd = false
rootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"}) RootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"})
rootCmd.SetCompletionCommandGroupID("help") RootCmd.SetCompletionCommandGroupID("help")
rootCmd.SetHelpCommandGroupID("help") RootCmd.SetHelpCommandGroupID("help")
} }

View File

@@ -1,4 +1,4 @@
package commands package issuesCmd
import ( import (
"fmt" "fmt"
@@ -9,7 +9,7 @@ import (
var updateFlags Flags var updateFlags Flags
var updateCmd = &cobra.Command{ var UpdateCmd = &cobra.Command{
Use: "update [issue ID]", Use: "update [issue ID]",
Short: "Update an existing issue", Short: "Update an existing issue",
Long: `Update an existing issue by its ID with the specified details.`, Long: `Update an existing issue by its ID with the specified details.`,
@@ -55,17 +55,15 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error {
} }
func init() { func init() {
updateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title") UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title")
updateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description") UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description")
updateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)") 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().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.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-5)")
updateCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) UpdateCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
updateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) UpdateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
updateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) UpdateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
rootCmd.AddCommand(updateCmd)
} }
func getUpdateValues(cmd *cobra.Command) (map[string]interface{}, error) { func getUpdateValues(cmd *cobra.Command) (map[string]interface{}, error) {

View File

@@ -0,0 +1,30 @@
package surveyCmd
import (
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/spf13/cobra"
)
var ListTasksCmd = &cobra.Command{
Use: "list-tasks",
Aliases: []string{"ls-t"},
Short: "List available tasks",
RunE: func(cmd *cobra.Command, args []string) error {
for i, name := range task.ListTasks() {
cmd.Printf("%d. %s\n", i+1, name)
}
return nil
},
}
var ListInterfacesCmd = &cobra.Command{
Use: "list-interfaces",
Aliases: []string{"ls-i"},
Short: "List available interfaces",
RunE: func(cmd *cobra.Command, args []string) error {
for i, name := range task.ListInterfaces() {
cmd.Printf("%d. %s\n", i+1, name)
}
return nil
},
}

View File

@@ -0,0 +1,29 @@
package surveyCmd
import (
"github.com/spf13/cobra"
)
var (
InterfaceType string
Task int
)
// RootCmd is the base command for the survey CLI.
var RootCmd = &cobra.Command{
Use: "survey",
Long: `Project Management Interface Survey
Thank you for participating in our survey!
We are gathering data on how users interact with different task management interfaces to better understand their preferences and compare their usability.
This survey will present you with a series of tasks to complete using various interfaces, including command-line, web-based, and terminal user interfaces.
Please answer the questions honestly and to the best of your ability.
Your responses will be kept confidential and used solely for research purposes.`}
func init() {
RootCmd.CompletionOptions.DisableDefaultCmd = true
StartCmd.Flags().StringVarP(&InterfaceType, "interface", "i", "tui", "Specify interface.")
StartCmd.Flags().IntVarP(&Task, "task", "t", 1, "Run task directly")
}

View File

@@ -0,0 +1,9 @@
package surveyCmd
import "github.com/spf13/cobra"
// StartCmd is the start command - RunE is set in cmd/survey/
var StartCmd = &cobra.Command{
Use: "start",
Short: "Start the user survey",
}

View File

@@ -0,0 +1,30 @@
package surveyCmd
import (
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
"github.com/spf13/cobra"
)
// StatusCmd displays the current task validation status
var StatusCmd = &cobra.Command{
Use: "status",
Short: "Check task validation status",
Long: "Displays the current task validation status and feedback.",
RunE: runStatusCmd,
}
func runStatusCmd(cmd *cobra.Command, args []string) error {
app := issuesCmd.AppFromContext(cmd.Context())
if app == nil || app.CurrentFeedback == nil {
cmd.Println("No validation status available.")
return nil
}
if app.CurrentFeedback.Message == "" {
cmd.Println("No validation status available yet.")
return nil
}
cmd.Print(app.CurrentFeedback.Message)
return nil
}

View File

@@ -0,0 +1,12 @@
package surveyCmd
import "github.com/spf13/cobra"
var SubmitCmd = &cobra.Command{
Use: "submit",
Short: "Submit your survey responses",
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("Submitting responses and metrics...")
return nil
},
}

View File

@@ -8,11 +8,18 @@ import (
"github.com/steveyegge/beads" "github.com/steveyegge/beads"
) )
// ValidationFeedback holds task validation status
type ValidationFeedback struct {
Success bool
Message string
}
type App struct { type App struct {
Config Config Config Config
Issues IssueService Issues IssueService
Stats StatsService Stats StatsService
Logger *slog.Logger Logger *slog.Logger
CurrentFeedback *ValidationFeedback
} }
type IssueService interface { type IssueService interface {

View File

@@ -4,17 +4,23 @@ package cli
import ( import (
"context" "context"
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
"github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands" "github.com/charmbracelet/fang"
"github.com/spf13/cobra"
) )
// Config is an alias for service.Config, used to configure the CLI. // Config is an alias for service.Config, used to configure the CLI.
type Config = service.Config type Config = service.Config
type CLI struct{} type CLI struct {
RootCmd *cobra.Command
}
func NewCli() *CLI { func NewCli(rootCmd *cobra.Command) *CLI {
return &CLI{} return &CLI{
RootCmd: rootCmd,
}
} }
// Run initializes the services and executes the CLI commands. // Run initializes the services and executes the CLI commands.
@@ -26,9 +32,10 @@ func (c *CLI) Run(ctx context.Context, config Config) error {
defer cleanup() defer cleanup()
commands.SetApp(app) issuesCmd.SetApp(app)
if err := commands.Execute(); err != nil { if err := fang.Execute(ctx, c.RootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme)); err != nil {
return err return err
} }
@@ -44,9 +51,9 @@ func (c *CLI) RunWithArgs(ctx context.Context, config Config, args []string) err
defer cleanup() defer cleanup()
commands.SetApp(app) issuesCmd.SetApp(app)
if err := commands.ExecuteArgs(args); err != nil { if err := issuesCmd.ExecuteArgs(args); err != nil {
return err return err
} }

View File

@@ -1,46 +0,0 @@
package commands
import (
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/spf13/cobra"
)
// replInstance holds a reference to the REPL for accessing validation feedback
var replInstance interface {
GetCurrentFeedback() task.ValidationFeedback
}
// SetRepl sets the REPL instance for use by commands
func SetRepl(repl interface {
GetCurrentFeedback() task.ValidationFeedback
}) {
replInstance = repl
}
// StatusCmd displays the current task validation status
var StatusCmd = &cobra.Command{
Use: "status",
Short: "Check task validation status",
Long: "Displays the current task validation status and feedback.",
RunE: runStatusCmd,
}
func runStatusCmd(cmd *cobra.Command, args []string) error {
if replInstance == nil {
cmd.Println("No task validation available")
return nil
}
feedback := replInstance.GetCurrentFeedback()
if feedback.Message == "" {
cmd.Println("No validation status available yet.")
return nil
}
cmd.Print(feedback.Message)
return nil
}
func init() {
rootCmd.AddCommand(StatusCmd)
}

View File

@@ -1,10 +0,0 @@
// Package styles defines the styling for the CLI output using the lipgloss library.
package styles
import "github.com/charmbracelet/lipgloss"
var (
TitleStyle = lipgloss.NewStyle().Bold(true).Padding(1)
CommandStyle = lipgloss.NewStyle().Padding(1)
)

View File

@@ -4,12 +4,10 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands" "github.com/LazyBachelor/LazyPM/internal/commands/issues"
) )
// execute processes the input command and returns the output // execute processes the input command and returns the output
// or an error if it occurs. It handles what type of command is being executed,
// whether it's a PM command or a shell command, and routes it accordingly.
func execute(input string) (string, error) { func execute(input string) (string, error) {
if input == "" { if input == "" {
return "", nil return "", nil
@@ -29,8 +27,6 @@ func execute(input string) (string, error) {
return executeShellCommand(input) return executeShellCommand(input)
} }
// executeShellCommand executes a shell command
// and returns its output or an error if it occurs.
func executeShellCommand(input string) (string, error) { func executeShellCommand(input string) (string, error) {
parts := strings.Fields(input) parts := strings.Fields(input)
if len(parts) == 0 { if len(parts) == 0 {
@@ -42,14 +38,12 @@ func executeShellCommand(input string) (string, error) {
return string(output), err return string(output), err
} }
// executePMCommand executes a PM command using the commands package
// and returns its output or an error if it occurs.
func executePMCommand(input string) (string, error) { func executePMCommand(input string) (string, error) {
parts := strings.Fields(input) parts := strings.Fields(input)
if len(parts) == 0 { if len(parts) == 0 {
return "", nil return "", nil
} }
output, err := commands.ExecuteArgsString(parts) output, err := issuesCmd.ExecuteArgsString(parts)
return output, err return output, err
} }

View File

@@ -7,11 +7,13 @@ import (
"os" "os"
"strings" "strings"
"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/service"
"github.com/LazyBachelor/LazyPM/internal/style"
"github.com/LazyBachelor/LazyPM/pkg/cli" "github.com/LazyBachelor/LazyPM/pkg/cli"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands"
"github.com/LazyBachelor/LazyPM/pkg/cli/styles"
"github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
"github.com/c-bata/go-prompt" "github.com/c-bata/go-prompt"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -27,6 +29,7 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.`
type REPL struct { type REPL struct {
feedbackChan chan task.ValidationFeedback feedbackChan chan task.ValidationFeedback
quitChan chan bool quitChan chan bool
app *service.App
currentFeedback task.ValidationFeedback currentFeedback task.ValidationFeedback
exitRequested bool exitRequested bool
@@ -55,12 +58,12 @@ func (r *REPL) Run(ctx context.Context, config cli.Config) error {
defer cleanup() defer cleanup()
// Make sure to set app, to ensure they are available. // Make sure to set app, to ensure they are available.
commands.SetApp(app) issuesCmd.SetApp(app)
// Set the REPL instance so status command can access it // Store app reference for updating feedback
commands.SetRepl(r) r.app = app
fmt.Println(styles.TitleStyle.Render(ReplTitle)) // Print REPL title. fmt.Println(style.TitleStyle.Render(ReplTitle)) // Print REPL title.
// Start goroutine to watch for validation feedback and quit signals // Start goroutine to watch for validation feedback and quit signals
if r.feedbackChan != nil && r.quitChan != nil { if r.feedbackChan != nil && r.quitChan != nil {
@@ -102,8 +105,8 @@ func (r *REPL) Run(ctx context.Context, config cli.Config) error {
// Add the input to the history for future navigation. // Add the input to the history for future navigation.
history = append(history, input) history = append(history, input)
output, _ := execute(input) // Ignore errors for now, gives better ux output, _ := execute(input) // Ignore errors for now, gives better ux
fmt.Println(styles.CommandStyle.Render(output)) // Print the output of the command in a styled format. fmt.Println(style.TextStyle.Render(output)) // Print the output of the command in a styled format.
} }
return nil return nil
@@ -114,6 +117,13 @@ func (r *REPL) watchValidation() {
select { select {
case feedback := <-r.feedbackChan: case feedback := <-r.feedbackChan:
r.currentFeedback = feedback r.currentFeedback = feedback
// Update app's CurrentFeedback so status command can access it
if r.app != nil {
r.app.CurrentFeedback = &service.ValidationFeedback{
Success: feedback.Success,
Message: feedback.Message,
}
}
if feedback.Success { if feedback.Success {
fmt.Printf("\n%s\n", styles.TitleStyle.Render("Task completed successfully!")) fmt.Printf("\n%s\n", styles.TitleStyle.Render("Task completed successfully!"))
fmt.Println("Press Enter to exit...") fmt.Println("Press Enter to exit...")
@@ -127,13 +137,18 @@ func (r *REPL) watchValidation() {
} }
} }
// GetCurrentFeedback returns the current validation feedback for the status command
func (r *REPL) GetCurrentFeedback() task.ValidationFeedback {
return r.currentFeedback
}
// SetChannels sets the channels for receiving validation feedback and quit signals from the task interface // SetChannels sets the channels for receiving validation feedback and quit signals from the task interface
func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan chan bool) { func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan chan bool) {
r.feedbackChan = feedbackChan r.feedbackChan = feedbackChan
r.quitChan = quitChan r.quitChan = quitChan
} }
func init() {
issuesCmd.RootCmd.AddCommand(issuesCmd.GetCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.ListCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.CloseCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.CreateCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.DeleteCmd)
issuesCmd.RootCmd.AddCommand(issuesCmd.UpdateCmd)
issuesCmd.RootCmd.AddCommand(surveyCmd.StatusCmd)
}

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"strings" "strings"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands" "github.com/LazyBachelor/LazyPM/internal/commands/issues"
"github.com/c-bata/go-prompt" "github.com/c-bata/go-prompt"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
) )
@@ -21,6 +21,7 @@ var rootSuggestions = []prompt.Suggest{
// commandSuggestions is a list of prompt suggestions for PM commands. // commandSuggestions is a list of prompt suggestions for PM commands.
var baseSuggestions = []prompt.Suggest{ var baseSuggestions = []prompt.Suggest{
{Text: "help", Description: "Show help information"}, {Text: "help", Description: "Show help information"},
{Text: "status", Description: "Show task status"},
{Text: "delete", Description: "Delete an issue by ID"}, {Text: "delete", Description: "Delete an issue by ID"},
{Text: "close", Description: "Close an issue by ID"}, {Text: "close", Description: "Close an issue by ID"},
{Text: "create", Description: "Create a new issue with title"}, {Text: "create", Description: "Create a new issue with title"},
@@ -148,7 +149,7 @@ func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest {
return nil return nil
} }
issues, _ := commands.GetIssueCompletions(context.Background(), partial) issues, _ := issuesCmd.GetIssueCompletions(context.Background(), partial)
var suggestions []prompt.Suggest var suggestions []prompt.Suggest
for _, issue := range issues { for _, issue := range issues {

View File

@@ -6,26 +6,51 @@ import (
"github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/service"
) )
var registry = make(map[string]func(*service.App) Tasker) var interfaceRegistry = make(map[string]Interface)
func Register(name string, constructor func(*service.App) Tasker) { func RegisterInterface(name string, iface Interface) {
if _, exists := registry[name]; exists { if _, exists := interfaceRegistry[name]; exists {
panic(fmt.Sprintf("task %q already registered", name)) panic(fmt.Sprintf("interface %q already registered", name))
} }
registry[name] = constructor interfaceRegistry[name] = iface
} }
func Get(name string, app *service.App) (Tasker, error) { func GetInterface(name string) (Interface, error) {
constructor, ok := registry[name] iface, ok := interfaceRegistry[name]
if !ok {
return nil, fmt.Errorf("interface %q not found", name)
}
return iface, nil
}
func ListInterfaces() []string {
names := make([]string, 0, len(interfaceRegistry))
for name := range interfaceRegistry {
names = append(names, name)
}
return names
}
var taskRegistry = make(map[string]func(*service.App) Tasker)
func RegisterTask(name string, constructor func(*service.App) Tasker) {
if _, exists := taskRegistry[name]; exists {
panic(fmt.Sprintf("task %q already registered", name))
}
taskRegistry[name] = constructor
}
func GetTasks(name string, app *service.App) (Tasker, error) {
constructor, ok := taskRegistry[name]
if !ok { if !ok {
return nil, fmt.Errorf("task %q not found", name) return nil, fmt.Errorf("task %q not found", name)
} }
return constructor(app), nil return constructor(app), nil
} }
func List() []string { func ListTasks() []string {
names := make([]string, 0, len(registry)) names := make([]string, 0, len(taskRegistry))
for name := range registry { for name := range taskRegistry {
names = append(names, name) names = append(names, name)
} }
return names return names

View File

@@ -15,7 +15,7 @@ import (
// 3. Run the interface // 3. Run the interface
// 4. Start validation loop in background // 4. Start validation loop in background
// 5. Show questionnaire when done // 5. Show questionnaire when done
func RunTask(ctx context.Context, t Tasker, i Interface, ifaceType InterfaceType) error { func RunTask(ctx context.Context, t Tasker, i Interface, iType InterfaceType) error {
doneChan := make(chan bool, 1) doneChan := make(chan bool, 1)
quitChan := make(chan bool, 1) quitChan := make(chan bool, 1)
feedbackChan := make(chan ValidationFeedback, 10) feedbackChan := make(chan ValidationFeedback, 10)
@@ -65,7 +65,7 @@ func RunTask(ctx context.Context, t Tasker, i Interface, ifaceType InterfaceType
} }
// Show questionnaire // Show questionnaire
questions := t.Questions(ifaceType) questions := t.Questions(iType)
questionare := taskui.NewQuestionnaireModel(questions) questionare := taskui.NewQuestionnaireModel(questions)
model, err = tea.NewProgram(questionare, tea.WithAltScreen()).Run() model, err = tea.NewProgram(questionare, tea.WithAltScreen()).Run()
if err != nil { if err != nil {