diff --git a/internal/style/styles.go b/internal/style/styles.go index bd55ae6..85c8b50 100644 --- a/internal/style/styles.go +++ b/internal/style/styles.go @@ -27,4 +27,5 @@ var ( TitleStyle = lipgloss.NewStyle().Foreground(PrimaryColor).Bold(true) TextStyle = lipgloss.NewStyle().Foreground(TextColor) HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(AccentColor) + ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) // Red color for errors ) diff --git a/pkg/repl/executor.go b/pkg/repl/executor.go index ff254f4..2789bec 100644 --- a/pkg/repl/executor.go +++ b/pkg/repl/executor.go @@ -21,14 +21,70 @@ func execute(input string) (string, error) { return ReplTitle, nil } + if input == "status" { + return executePMCommand("survey status") + } + if after, ok := strings.CutPrefix(input, "pm"); ok { return executePMCommand(after) } return executeShellCommand(input) } +// shellSplit splits input respecting quoted strings and escapes +// similar to how a shell would parse arguments +func shellSplit(input string) []string { + var args []string + var current strings.Builder + var inQuote rune + var escaped bool + + for _, ch := range input { + if escaped { + current.WriteRune(ch) + escaped = false + continue + } + + if ch == '\\' { + escaped = true + continue + } + + if inQuote != 0 { + if ch == inQuote { + inQuote = 0 + continue + } + current.WriteRune(ch) + continue + } + + if ch == '"' || ch == '\'' { + inQuote = ch + continue + } + + if ch == ' ' || ch == '\t' { + if current.Len() > 0 { + args = append(args, current.String()) + current.Reset() + } + continue + } + + current.WriteRune(ch) + } + + if current.Len() > 0 { + args = append(args, current.String()) + } + + return args +} + func executeShellCommand(input string) (string, error) { - parts := strings.Fields(input) + parts := shellSplit(input) if len(parts) == 0 { return "", nil } @@ -39,7 +95,7 @@ func executeShellCommand(input string) (string, error) { } func executePMCommand(input string) (string, error) { - parts := strings.Fields(input) + parts := shellSplit(input) if len(parts) == 0 { return "", nil } diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index ee63736..bfc825b 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -22,8 +22,8 @@ type ValidationFeedback = models.ValidationFeedback const ( ReplHelp = `Type 'pm help' for available PM commands. -Type 'pm status' to check task progress. -You can also run shell commands directly. Type 'exit' or 'quit' to leave.` +You can also run shell commands directly. Type 'exit' or 'quit' to leave. +Type 'status' to check task progress.` ReplTitle = "Welcome to Project Management CLI! " + ReplHelp ) @@ -144,8 +144,19 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // Add the input to the history for future navigation. history = append(history, input) - output, _ := execute(input) // Ignore errors for now, gives better ux - fmt.Println(style.TextStyle.Render(output)) // Print the output of the command in a styled format. + output, err := execute(input) + if err != nil { + // Show command output (even on error) in normal text style + if output != "" { + fmt.Println(style.TextStyle.Render(output)) + } + // Show error message in red if no output was captured + if output == "" { + fmt.Println(style.ErrorStyle.Render(err.Error())) + } + } else if output != "" { + fmt.Println(style.TextStyle.Render(output)) + } } return nil diff --git a/pkg/repl/suggestions.go b/pkg/repl/suggestions.go index 360791d..e66fbc4 100644 --- a/pkg/repl/suggestions.go +++ b/pkg/repl/suggestions.go @@ -12,6 +12,7 @@ import ( // rootSuggestions is a list of prompt suggestions for root-level commands. var rootSuggestions = []prompt.Suggest{ {Text: "pm", Description: "Project Management System"}, + {Text: "status", Description: "Show task status"}, {Text: "exit", Description: "Exit pm CLI"}, {Text: "help", Description: "Show help information"}, {Text: "title", Description: "Print the welcome title"}, @@ -38,7 +39,8 @@ var createFlags = []prompt.Suggest{ {Text: "--desc", Description: "Issue description"}, {Text: "--status", Description: "Issue status (open, closed, in_progress)"}, {Text: "--type", Description: "Issue type (bug, feature, task)"}, - {Text: "--priority", Description: "Issue priority (0-5)"}, + {Text: "--priority", Description: "Issue priority (0-4)"}, + {Text: "--assignee", Description: "Issue assignee"}, } // updateFlags is a list of prompt suggestions for the update command flags. @@ -47,7 +49,8 @@ var updateFlags = []prompt.Suggest{ {Text: "--desc", Description: "New issue description"}, {Text: "--status", Description: "New issue status (open, closed, in_progress)"}, {Text: "--type", Description: "New issue type (bug, feature, task)"}, - {Text: "--priority", Description: "New issue priority (0-5)"}, + {Text: "--priority", Description: "New issue priority (0-4)"}, + {Text: "--assignee", Description: "New issue assignee"}, } // listFlags is a list of prompt suggestions for the list command flags. @@ -56,8 +59,9 @@ var listFlags = []prompt.Suggest{ {Text: "--desc", Description: "Filter by description"}, {Text: "--status", Description: "Filter by status (open, closed, in_progress)"}, {Text: "--type", Description: "Filter by type (bug, feature, task)"}, - {Text: "--priority", Description: "Filter by priority (0-5)"}, + {Text: "--priority", Description: "Filter by priority (0-4)"}, {Text: "--limit", Description: "Limit number of results"}, + {Text: "--assignee", Description: "Filter by assignee"}, } var deleteFlags = []prompt.Suggest{ @@ -81,18 +85,19 @@ var statusValues = []prompt.Suggest{ // typeValues is a list of prompt suggestions for issue types var typeValues = []prompt.Suggest{ + {Text: "task", Description: "Task issue type"}, {Text: "bug", Description: "Bug issue type"}, {Text: "feature", Description: "Feature issue type"}, - {Text: "task", Description: "Task issue type"}, + {Text: "chore", Description: "Chore issue type"}, } // priorityValues is a list of prompt suggestions for issue priority levels var priorityValues = []prompt.Suggest{ - {Text: "0", Description: "Lowest priority"}, + {Text: "0", Description: "Irrelevant"}, {Text: "1", Description: "Low priority"}, - {Text: "2", Description: "Medium-low priority"}, - {Text: "3", Description: "Medium priority"}, - {Text: "4", Description: "High priority"}, + {Text: "2", Description: "Normal priority"}, + {Text: "3", Description: "High priority"}, + {Text: "4", Description: "Critical priority"}, } // isIDCommand maps command names to a boolean indicating whether they expect an issue ID as an argument.