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 (
"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/pkg/cli"
)
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
}
}
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 (
"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/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/task"
_ "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) {
return service.NewServices(ctx, tasks.BaseConfig())
}
func initInterfaces() map[string]task.Interface {
return map[string]task.Interface{
"repl": repl.NewRepl(),
"tui": tui.NewTui(),
"web": web.NewWeb(),
interfaces := make(map[string]task.Interface)
for _, name := range task.ListInterfaces() {
i, err := task.GetInterface(name)
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 (
"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"
)
func main() {
ctx := context.Background()
if err := fang.Execute(ctx, rootCmd,
if err := fang.Execute(ctx, surveyCmd.RootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme)); err != nil {
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"
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
surveyCmd "github.com/LazyBachelor/LazyPM/internal/commands/survey"
"github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/spf13/cobra"
)
func runTask(ctx context.Context, t task.Tasker, i task.Interface) error {
return task.RunTask(ctx, t, i, tasks.InterfaceToType(i))
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[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 {
var ifaceNames []string
var iNames []string
for name := range interfaces {
ifaceNames = append(ifaceNames, name)
iNames = append(iNames, name)
}
rand.Shuffle(len(ifaceNames), func(i, j int) {
ifaceNames[i], ifaceNames[j] = ifaceNames[j], ifaceNames[i]
rand.Shuffle(len(iNames), func(i, j int) {
iNames[i], iNames[j] = iNames[j], iNames[i]
})
for i, t := range surveyTasks {
idx := i % len(ifaceNames)
selected := interfaces[ifaceNames[idx]]
idx := i % len(iNames)
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
}
}

View File

@@ -2,7 +2,7 @@ package tasks
import (
"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"
taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
"github.com/LazyBachelor/LazyPM/pkg/tui"

View File

@@ -1,4 +1,4 @@
package commands
package issuesCmd
import (
"fmt"
@@ -7,9 +7,9 @@ import (
"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.
var closeCmd = &cobra.Command{
var CloseCmd = &cobra.Command{
Use: "close [id]",
Short: "Close an existing issue",
Long: `Close an existing issue by its ID.`,
@@ -58,8 +58,3 @@ func runCloseCmd(cmd *cobra.Command, args []string) error {
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 (
"context"

View File

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

View File

@@ -1,4 +1,4 @@
package commands
package issuesCmd
import (
"context"
@@ -17,8 +17,8 @@ var (
deleteInteractive bool
)
// deleteCmd represents the delete command.
var deleteCmd = &cobra.Command{
// DeleteCmd represents the delete command.
var DeleteCmd = &cobra.Command{
Use: "delete [id]",
Short: "Delete an existing issue",
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.
func init() {
deleteCmd.Flags().BoolVarP(&deleteInteractive, "interactive", "i", false, "Delete issues interactively")
deleteCmd.Flags().BoolVarP(&confirmDelete, "yes", "y", true, "Confirm deletion without prompt")
rootCmd.AddCommand(deleteCmd)
DeleteCmd.Flags().BoolVarP(&deleteInteractive, "interactive", "i", false, "Delete issues interactively")
DeleteCmd.Flags().BoolVarP(&confirmDelete, "yes", "y", true, "Confirm deletion without prompt")
}

View File

@@ -1,4 +1,4 @@
package commands
package issuesCmd
import (
"strings"
@@ -17,8 +17,8 @@ pm list --title "New feature" --desc "feature description"
pm list -p 1 -l 10`
)
// getIssuesCmd represents the get issues command.
var getIssuesCmd = &cobra.Command{
// ListCmd represents the get issues command.
var ListCmd = &cobra.Command{
Use: "list [search query]",
Short: "List all issues",
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.
func init() {
getIssuesCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title")
getIssuesCmd.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)")
getIssuesCmd.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().StringVar(&listFlags.title, "title", "", "Filter issues by title")
ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description")
ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)")
ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)")
ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)")
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))
getIssuesCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
getIssuesCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
rootCmd.AddCommand(getIssuesCmd)
ListCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
ListCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
ListCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
}

View File

@@ -1,12 +1,12 @@
package commands
package issuesCmd
import (
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/spf13/cobra"
)
// getIssueCmd represents the get issue command.
var getIssueCmd = &cobra.Command{
// GetCmd represents the get issue command.
var GetCmd = &cobra.Command{
Use: "describe [issue ID]",
Short: "Get issue details",
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.
func init() {
rootCmd.AddCommand(getIssueCmd)
}

View File

@@ -1,4 +1,4 @@
package commands
package issuesCmd
import (
"bytes"
@@ -29,8 +29,8 @@ type Flags struct {
priority int
}
// rootCmd is the base command for the CLI application.
var rootCmd = &cobra.Command{
// RootCmd is the base command for the CLI application.
var RootCmd = &cobra.Command{
Short: "Project Management CLI",
Long: `Project Management CLI for managing issues and tasks.`,
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.
func SetApp(application *service.App) {
app = application
rootCmd.Use = app.Config.RootCmd
RootCmd.Use = app.Config.RootCmd
}
// AppFromContext retrieves the App from the command context
@@ -57,37 +57,37 @@ func AppFromContext(ctx context.Context) *service.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.
func ExecuteArgs(args []string) error {
rootCmd.SetArgs(args)
return fang.Execute(context.Background(), rootCmd,
RootCmd.SetArgs(args)
return fang.Execute(context.Background(), RootCmd,
fang.WithColorSchemeFunc(fang.AnsiColorScheme))
}
// 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
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)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
rootCmd.SetArgs(args)
RootCmd.SetOut(buf)
RootCmd.SetErr(buf)
RootCmd.SetArgs(args)
RootCmd.SetContext(ctx)
err := rootCmd.Execute()
err := RootCmd.Execute()
return buf.String(), err
}
// init function to set up the command hierarchy and options.
func init() {
rootCmd.CompletionOptions.DisableDefaultCmd = false
rootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"})
rootCmd.SetCompletionCommandGroupID("help")
rootCmd.SetHelpCommandGroupID("help")
RootCmd.CompletionOptions.DisableDefaultCmd = false
RootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"})
RootCmd.SetCompletionCommandGroupID("help")
RootCmd.SetHelpCommandGroupID("help")
}

View File

@@ -1,4 +1,4 @@
package commands
package issuesCmd
import (
"fmt"
@@ -9,7 +9,7 @@ import (
var updateFlags Flags
var updateCmd = &cobra.Command{
var UpdateCmd = &cobra.Command{
Use: "update [issue ID]",
Short: "Update an existing issue",
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() {
updateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title")
updateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description")
updateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)")
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().StringVar(&updateFlags.title, "title", "", "New issue title")
UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description")
UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)")
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", completionFunc(typeOptions))
updateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
updateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
rootCmd.AddCommand(updateCmd)
UpdateCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions))
UpdateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions))
UpdateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange))
}
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"
)
// ValidationFeedback holds task validation status
type ValidationFeedback struct {
Success bool
Message string
}
type App struct {
Config Config
Issues IssueService
Stats StatsService
Logger *slog.Logger
CurrentFeedback *ValidationFeedback
}
type IssueService interface {

View File

@@ -4,17 +4,23 @@ package cli
import (
"context"
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
"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.
type Config = service.Config
type CLI struct{}
type CLI struct {
RootCmd *cobra.Command
}
func NewCli() *CLI {
return &CLI{}
func NewCli(rootCmd *cobra.Command) *CLI {
return &CLI{
RootCmd: rootCmd,
}
}
// 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()
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
}
@@ -44,9 +51,9 @@ func (c *CLI) RunWithArgs(ctx context.Context, config Config, args []string) err
defer cleanup()
commands.SetApp(app)
issuesCmd.SetApp(app)
if err := commands.ExecuteArgs(args); err != nil {
if err := issuesCmd.ExecuteArgs(args); err != nil {
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"
"strings"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands"
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
)
// 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) {
if input == "" {
return "", nil
@@ -29,8 +27,6 @@ func execute(input string) (string, error) {
return executeShellCommand(input)
}
// executeShellCommand executes a shell command
// and returns its output or an error if it occurs.
func executeShellCommand(input string) (string, error) {
parts := strings.Fields(input)
if len(parts) == 0 {
@@ -42,14 +38,12 @@ func executeShellCommand(input string) (string, error) {
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) {
parts := strings.Fields(input)
if len(parts) == 0 {
return "", nil
}
output, err := commands.ExecuteArgsString(parts)
output, err := issuesCmd.ExecuteArgsString(parts)
return output, err
}

View File

@@ -7,11 +7,13 @@ import (
"os"
"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/style"
"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/tui/styles"
"github.com/c-bata/go-prompt"
"golang.org/x/term"
)
@@ -27,6 +29,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
currentFeedback task.ValidationFeedback
exitRequested bool
@@ -55,12 +58,12 @@ func (r *REPL) Run(ctx context.Context, config cli.Config) error {
defer cleanup()
// 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
commands.SetRepl(r)
// Store app reference for updating feedback
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
if r.feedbackChan != nil && r.quitChan != nil {
@@ -103,7 +106,7 @@ func (r *REPL) Run(ctx context.Context, config cli.Config) error {
history = append(history, input)
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
@@ -114,6 +117,13 @@ func (r *REPL) watchValidation() {
select {
case feedback := <-r.feedbackChan:
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 {
fmt.Printf("\n%s\n", styles.TitleStyle.Render("Task completed successfully!"))
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
func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan chan bool) {
r.feedbackChan = feedbackChan
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"
"strings"
"github.com/LazyBachelor/LazyPM/pkg/cli/commands"
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
"github.com/c-bata/go-prompt"
"github.com/muesli/reflow/truncate"
)
@@ -21,6 +21,7 @@ var rootSuggestions = []prompt.Suggest{
// commandSuggestions is a list of prompt suggestions for PM commands.
var baseSuggestions = []prompt.Suggest{
{Text: "help", Description: "Show help information"},
{Text: "status", Description: "Show task status"},
{Text: "delete", Description: "Delete an issue by ID"},
{Text: "close", Description: "Close an issue by ID"},
{Text: "create", Description: "Create a new issue with title"},
@@ -148,7 +149,7 @@ func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest {
return nil
}
issues, _ := commands.GetIssueCompletions(context.Background(), partial)
issues, _ := issuesCmd.GetIssueCompletions(context.Background(), partial)
var suggestions []prompt.Suggest
for _, issue := range issues {

View File

@@ -6,26 +6,51 @@ import (
"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) {
if _, exists := registry[name]; exists {
func RegisterInterface(name string, iface Interface) {
if _, exists := interfaceRegistry[name]; exists {
panic(fmt.Sprintf("interface %q already registered", name))
}
interfaceRegistry[name] = iface
}
func GetInterface(name string) (Interface, error) {
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))
}
registry[name] = constructor
taskRegistry[name] = constructor
}
func Get(name string, app *service.App) (Tasker, error) {
constructor, ok := registry[name]
func GetTasks(name string, app *service.App) (Tasker, error) {
constructor, ok := taskRegistry[name]
if !ok {
return nil, fmt.Errorf("task %q not found", name)
}
return constructor(app), nil
}
func List() []string {
names := make([]string, 0, len(registry))
for name := range registry {
func ListTasks() []string {
names := make([]string, 0, len(taskRegistry))
for name := range taskRegistry {
names = append(names, name)
}
return names

View File

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