3 Commits

Author SHA1 Message Date
Robin Olsen
9ad1c5535c fix 2026-03-26 22:59:11 +01:00
viljarb
d983427ca9 forgot to add cmd/pm/init.go 2026-03-25 14:28:30 +01:00
viljarb
13d54d9e81 implementing dependency management in the cli 2026-03-25 14:27:38 +01:00
5 changed files with 230 additions and 54 deletions

View File

@@ -101,23 +101,4 @@ chown -R abc:abc /config/Desktop /config/.config
git config --global user.name "LazyPM"
git config --global user.email "lazy@pm.com"
mkdir -p /config/.config/xfce4
mkdir -p /usr/share/xfce4/helpers
cat > /usr/share/xfce4/helpers/chromium.desktop <<'EOF'
[Desktop Entry]
Version=1.0
Type=X-XFCE-Helper
Name=Chromium
X-XFCE-Category=WebBrowser
X-XFCE-Commands=chromium;
X-XFCE-CommandsWithParameter=chromium "%s";
Icon=chromium
EOF
cat > /config/.config/xfce4/helpers.rc <<'EOF'
WebBrowser=chromium
EOF
chown abc:abc /config/.config/xfce4/helpers.rc
echo "**** Desktop setup complete ****"

View File

@@ -72,6 +72,7 @@ func init() {
issues.UpdateCmd.GroupID = "issues"
issues.CloseCmd.GroupID = "issues"
issues.DeleteCmd.GroupID = "issues"
issues.DepCmd.GroupID = "issues"
RootCmd.AddCommand(issues.CreateCmd)
RootCmd.AddCommand(issues.GetCmd)
@@ -79,6 +80,7 @@ func init() {
RootCmd.AddCommand(issues.UpdateCmd)
RootCmd.AddCommand(issues.CloseCmd)
RootCmd.AddCommand(issues.DeleteCmd)
RootCmd.AddCommand(issues.DepCmd)
RootCmd.AddGroup(&cobra.Group{ID: "comment", Title: "Comment Management"})
issues.CommentCmd.GroupID = "comment"

View File

@@ -14,9 +14,12 @@ const dependencyManagementDescription = `You are tasked with managing issue depe
Several issues in your project have dependencies on other issues.
You need to:
1. Find the 2 issues that depend on 'Setup database connection'.
2. Add the 'Setup database connection' issue as a dependency to the 2 issues.
3. Set the 2 issues' status to "Blocked".`
1. Find 2 issues that mention dependencies in their description.
- Set their status to "Blocked".
2. Find the issue that is mentioned by the other issues:
- Set priority to 3.
- Set status to in-progress.
- Assign to yourself as "Me".`
type DependencyManagementTask struct {
done bool
@@ -70,33 +73,27 @@ func (t *DependencyManagementTask) Setup(ctx context.Context) error {
NewIssueBuilder().
WithTitle("Setup database connection").
WithDescription("Configure database connection pool.").
WithPriority(3).
WithStatus(models.StatusInProgress).
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("Implement Authentication System").
WithDescription("Add login/logout functionality.").
WithDescription("Add login/logout functionality. Depends on 'Setup database connection' issue.").
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("Add user management operations").
WithDescription("Add operations for user management.").
WithPriority(2).
WithDescription("Add operations for user management. Depends on 'Setup database connection' issue.").
WithPriority(3).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
}
if err := t.app.Issues.CreateIssues(ctx, t.depIssues, ""); err != nil {
return err
}
return nil
return t.app.Issues.CreateIssues(ctx, t.depIssues, "")
}
func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedback {
@@ -111,29 +108,25 @@ func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedb
for _, depIssue := range t.depIssues[1:] {
if issue.Title == depIssue.Title {
expect.Equal(issue.Status, models.StatusBlocked,
fmt.Sprintf("%s status", issue.Title,))
fmt.Sprintf("%s status", issue.Title))
}
}
}
for _, depIssue := range t.depIssues[1:] {
deps, _ := t.app.Issues.GetDependencies(ctx, depIssue.ID)
expect.Equal(len(deps), 1,
fmt.Sprintf("%s - %d dependencies", depIssue.Title, len(deps)))
if len(deps) == 1 {
expect.Equal(deps[0].ID, t.depIssues[0].ID,
fmt.Sprintf("%s dependency", depIssue.Title))
}
}
if !expect.Valid() {
return expect.ValidationFeedback
}
for _, issue := range issues {
for _, foundationalIssue := range t.depIssues[:1] {
if issue.Title == foundationalIssue.Title {
expect.Equal(issue.Priority, 3,
fmt.Sprintf("%s priority", issue.Title))
expect.NotEmptyAndEqual(issue.Assignee, "Me",
fmt.Sprintf("%s assignee", issue.Title))
expect.Equal(issue.Status, models.StatusInProgress,
fmt.Sprintf("%s status", issue.Title))
}
}
}
return expect.Complete()
}

View File

@@ -0,0 +1,123 @@
package issues
import (
"fmt"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/spf13/cobra"
)
const depCmdExample = `pm dep ISSUE-1
pm dep view ISSUE-1
pm dep add ISSUE-1 ISSUE-2
pm dep remove ISSUE-1 ISSUE-2`
// DepCmd manages dependencies for issues.
// Default behavior: view dependencies of an issue.
var DepCmd = &cobra.Command{
Use: "dep [issue id]",
Short: "Manage dependencies for an issue",
Long: `Manage dependencies between issues.`,
Aliases: []string{"dependencies"},
Example: depCmdExample,
// This command runs only when no subcommand is provided.
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeIssues,
RunE: runDepViewCmd,
}
var DepViewCmd = &cobra.Command{
Use: "view [issue id]",
Short: "View dependencies of an issue",
Aliases: []string{"show"},
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeIssues,
RunE: runDepViewCmd,
}
var DepAddCmd = &cobra.Command{
Use: "add [issue id] [depends-on id]",
Short: "Add a dependency: issue depends on depends-on issue",
Args: cobra.ExactArgs(2),
ValidArgsFunction: completeIssues,
RunE: runDepAddCmd,
}
var DepRemoveCmd = &cobra.Command{
Use: "remove [issue id] [depends-on id]",
Short: "Remove an existing dependency",
Aliases: []string{"rm"},
Args: cobra.ExactArgs(2),
ValidArgsFunction: completeIssues,
RunE: runDepRemoveCmd,
}
func init() {
DepCmd.AddCommand(DepViewCmd)
DepCmd.AddCommand(DepAddCmd)
DepCmd.AddCommand(DepRemoveCmd)
}
func runDepViewCmd(cmd *cobra.Command, args []string) error {
issueID := args[0]
app := AppFromContext(cmd.Context())
deps, err := app.Issues.GetDependencies(cmd.Context(), issueID)
if err != nil {
return fmt.Errorf("failed to load dependencies for %s: %w", issueID, err)
}
if len(deps) == 0 {
cmd.Printf("Issue %s has no dependencies.\n", issueID)
return nil
}
cmd.Printf("Dependencies for %s:\n", issueID)
for _, d := range deps {
if d == nil {
continue
}
if d.Title != "" {
cmd.Printf("- %s: %s\n", d.ID, d.Title)
} else {
cmd.Printf("- %s\n", d.ID)
}
}
return nil
}
func runDepAddCmd(cmd *cobra.Command, args []string) error {
issueID := args[0]
dependsOnID := args[1]
app := AppFromContext(cmd.Context())
dep := &models.Dependency{
IssueID: issueID,
DependsOnID: dependsOnID,
Type: models.DepBlocks,
}
if err := app.Issues.AddDependency(cmd.Context(), dep, "cli"); err != nil {
return fmt.Errorf("failed to add dependency: %s depends on %s: %w", issueID, dependsOnID, err)
}
cmd.Printf("Added dependency: %s depends on %s\n", issueID, dependsOnID)
return runDepViewCmd(cmd, []string{issueID})
}
func runDepRemoveCmd(cmd *cobra.Command, args []string) error {
issueID := args[0]
dependsOnID := args[1]
app := AppFromContext(cmd.Context())
if err := app.Issues.RemoveDependency(cmd.Context(), issueID, dependsOnID, "cli"); err != nil {
return fmt.Errorf("failed to remove dependency: %s depends on %s: %w", issueID, dependsOnID, err)
}
cmd.Printf("Removed dependency: %s no longer depends on %s\n", issueID, dependsOnID)
return runDepViewCmd(cmd, []string{issueID})
}

View File

@@ -23,6 +23,7 @@ var rootSuggestions = []prompt.Suggest{
var baseSuggestions = []prompt.Suggest{
{Text: "help", Description: "Show help information"},
{Text: "sprint", Description: "Manage sprints"},
{Text: "dep", Description: "Manage dependencies"},
{Text: "create", Description: "Create a new issue with title"},
{Text: "list", Description: "List all issues"},
{Text: "read", Description: "Read issue details by ID"},
@@ -41,6 +42,7 @@ var baseSuggestions = []prompt.Suggest{
{Text: "rm", Description: "Alias for delete command"},
{Text: "del", Description: "Alias for delete command"},
{Text: "get", Description: "Alias for read command"},
{Text: "dependencies", Description: "Alias for dep"},
}
// createFlags is a list of prompt suggestions for the create command flags.
@@ -123,6 +125,14 @@ var sprintSubcommands = []prompt.Suggest{
{Text: "del", Description: "Alias for delete subcommand"},
}
var depSubcommands = []prompt.Suggest{
{Text: "view", Description: "View dependencies of an issue"},
{Text: "show", Description: "Alias for view"},
{Text: "add", Description: "Add a dependency"},
{Text: "remove", Description: "Remove a dependency"},
{Text: "rm", Description: "Alias for remove"},
}
// statusValues is a list of prompt suggestions for status types
var statusValues = []prompt.Suggest{
{Text: "open", Description: "Open status"},
@@ -195,6 +205,73 @@ func commandSuggestions(words []string) []prompt.Suggest {
func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest {
lastWord, prevWord := parseWords(words, text)
// Custom completion logic for `pm dep ...`
if cmd == "dep" {
switch len(words) {
case 1:
// `pm dep ` => next token could be a subcommand; default view is handled
// once the user types an issue id
if strings.HasSuffix(text, " ") {
return depSubcommands
}
return nil
case 2:
// either:
// - pm dep <issueID> (default view)
// - pm dep <subcommand>
if strings.HasSuffix(text, " ") {
sub := words[1]
switch sub {
case "view", "show", "add", "remove", "rm":
// After a valid subcommand we expect the issue id next.
return issueIDSuggestions("", true)
default:
// Treat it as an issue id already; no more suggestions.
return nil
}
}
// Typing the 2nd token: suggest subcommands and issue IDs that match.
suggests := make([]prompt.Suggest, 0, len(depSubcommands))
suggests = append(suggests, filterByPrefix(depSubcommands, lastWord)...)
//suggests = append(suggests, issueIDSuggestions(lastWord, true)...)
return suggests
case 3:
// If subcommand is `add/remove`, third token is the issue id.
// If subcommand is show/list, third token is the issue id.
sub := words[1]
switch sub {
case "add", "remove", "rm":
if strings.HasSuffix(text, " ") {
// `pm dep add <issueID> ` => next token is dependsOnID.
return issueIDSuggestions("", true)
}
return issueIDSuggestions(lastWord, true)
default:
// show/list/show => completing the issue id.
if strings.HasSuffix(text, " ") {
return nil
}
return issueIDSuggestions(lastWord, true)
}
case 4:
// add/remove/rm have a 4th token: dependsOnID.
sub := words[1]
if sub == "add" || sub == "remove" || sub == "rm" {
if strings.HasSuffix(text, " ") {
return nil
}
return issueIDSuggestions(lastWord, true)
}
return nil
}
return nil
}
if values := getFlagValues(prevWord); values != nil {
return filterByPrefix(values, lastWord)
}