Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beans/beans-0ajg--beans-complete-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-0ajg
title: beans complete command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:04Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-18db--beans-milestones-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-18db
title: beans milestones command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:05Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-jvkq--beans-start-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-jvkq
title: beans start command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:04Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-m364--beans-progress-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-m364
title: beans progress command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:05Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-mmyp--workflow-cli-commands.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-mmyp
title: Workflow CLI commands
status: todo
status: completed
type: epic
priority: normal
created_at: 2025-12-27T21:43:38Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-p17z--beans-next-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-p17z
title: beans next command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:04Z
Expand Down
2 changes: 1 addition & 1 deletion .beans/beans-r780--beans-scrap-command.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# beans-r780
title: beans scrap command
status: todo
status: completed
type: task
priority: normal
created_at: 2025-12-27T21:44:04Z
Expand Down
90 changes: 90 additions & 0 deletions internal/commands/blocked.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package commands

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
)

var (
blockedJSON bool
blockedQuiet bool
)

type blockedEntry struct {
Bean *bean.Bean `json:"bean"`
Blockers []*bean.Bean `json:"blockers"`
}

var blockedCmd = &cobra.Command{
Use: "blocked",
Short: "List beans that are blocked",
Long: `Lists beans that are blocked by other beans, showing what blocks each one.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
isBlocked := true
filter := &model.BeanFilter{
IsBlocked: &isBlocked,
ExcludeStatus: []string{"completed", "scrapped"},
}

resolver := &graph.Resolver{Core: core}
beans, err := resolver.Query().Beans(context.Background(), filter)
if err != nil {
return cmdError(blockedJSON, output.ErrValidation, "querying beans: %v", err)
}

sortBeans(beans, "", cfg)

if blockedJSON {
entries := make([]blockedEntry, 0, len(beans))
for _, b := range beans {
blockers := core.FindActiveBlockers(b.ID)
entries = append(entries, blockedEntry{Bean: b, Blockers: blockers})
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(entries)
}

if blockedQuiet {
for _, b := range beans {
fmt.Println(b.ID)
}
return nil
}

if len(beans) == 0 {
fmt.Println(ui.Muted.Render("No blocked beans."))
return nil
}

for _, b := range beans {
blockers := core.FindActiveBlockers(b.ID)
var blockerStrs []string
for _, bl := range blockers {
blockerStrs = append(blockerStrs, ui.ID.Render(bl.ID)+" "+ui.Muted.Render(bl.Title))
}

fmt.Println(ui.ID.Render(b.ID) + " " + b.Title)
fmt.Println(" " + ui.Warning.Render("Blocked by: ") + strings.Join(blockerStrs, ", "))
}

return nil
},
}

func RegisterBlockedCmd(root *cobra.Command) {
blockedCmd.Flags().BoolVar(&blockedJSON, "json", false, "Output as JSON")
blockedCmd.Flags().BoolVarP(&blockedQuiet, "quiet", "q", false, "Only output IDs")
root.AddCommand(blockedCmd)
}
76 changes: 76 additions & 0 deletions internal/commands/complete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package commands

import (
"context"
"fmt"

"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
)

var (
completeSummary string
completeJSON bool
)

var completeCmd = &cobra.Command{
Use: "complete <id> [id...]",
Short: "Mark one or more beans as completed",
Long: `Sets the status of one or more beans to "completed". Optionally appends a summary of changes.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
resolver := &graph.Resolver{Core: core}
var results []*result
var errs []error

for _, id := range args {
b, _, err := resolveBean(resolver, id, completeJSON)
if err != nil {
errs = append(errs, err)
continue
}

if b.Status == "completed" {
if !completeJSON {
fmt.Println(ui.Warning.Render("Already completed: ") + ui.ID.Render(b.ID))
}
results = append(results, &result{bean: b, warning: "already completed"})
continue
}

status := "completed"
input := model.UpdateBeanInput{
Status: &status,
}

if completeSummary != "" {
appendText := "## Summary of Changes\n\n" + completeSummary
input.BodyMod = &model.BodyModification{
Append: &appendText,
}
}

b, err = resolver.Mutation().UpdateBean(context.Background(), b.ID, input)
if err != nil {
errs = append(errs, mutationError(completeJSON, err))
continue
}

results = append(results, &result{bean: b})
}

if len(errs) > 0 && len(results) == 0 {
return errs[0]
}

return outputResults(results, errs, completeJSON, "completed", "Completed")
},
}

func RegisterCompleteCmd(root *cobra.Command) {
completeCmd.Flags().StringVarP(&completeSummary, "summary", "m", "", "Summary of changes to append")
completeCmd.Flags().BoolVar(&completeJSON, "json", false, "Output as JSON")
root.AddCommand(completeCmd)
}
70 changes: 70 additions & 0 deletions internal/commands/content.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package commands

import (
"context"
"fmt"
"io"
"os"
"strings"

"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
)

// resolveContent returns content from a direct value or file flag.
Expand Down Expand Up @@ -94,6 +97,73 @@ func applyBodyAppend(body, text string) string {
return bean.AppendWithSeparator(body, text)
}

// resolveBean finds a bean by ID, checking the archive if needed.
// Returns the bean and whether it was unarchived.
func resolveBean(resolver *graph.Resolver, id string, jsonMode bool) (*bean.Bean, bool, error) {
b, err := resolver.Query().Bean(context.Background(), id)
if err != nil {
return nil, false, cmdError(jsonMode, output.ErrNotFound, "failed to find bean: %v", err)
}

if b == nil {
unarchived, unarchiveErr := core.LoadAndUnarchive(id)
if unarchiveErr != nil {
return nil, false, cmdError(jsonMode, output.ErrNotFound, "bean not found: %s", id)
}
b, err = resolver.Query().Bean(context.Background(), unarchived.ID)
if err != nil || b == nil {
return nil, false, cmdError(jsonMode, output.ErrNotFound, "bean not found: %s", id)
}
return b, true, nil
}

return b, false, nil
}

// result holds the outcome of a workflow command for a single bean.
type result struct {
bean *bean.Bean
warning string
}

// outputResults handles JSON and human output for multi-ID workflow commands.
func outputResults(results []*result, errs []error, jsonMode bool, pastTense, pastTenseCapitalized string) error {
beans := make([]*bean.Bean, 0, len(results))
var warnings []string
for _, r := range results {
beans = append(beans, r.bean)
if r.warning != "" {
warnings = append(warnings, fmt.Sprintf("%s: %s", r.bean.ID, r.warning))
}
}
for _, e := range errs {
warnings = append(warnings, e.Error())
}

if jsonMode {
if len(beans) == 1 && len(warnings) == 0 {
return output.Success(beans[0], "Bean "+pastTense)
}
resp := output.Response{
Success: true,
Beans: beans,
Count: len(beans),
Message: fmt.Sprintf("%d bean(s) %s", len(beans), pastTense),
}
if len(warnings) > 0 {
resp.Warnings = warnings
}
return output.JSON(resp)
}

for _, r := range results {
if r.warning == "" {
fmt.Println(ui.Success.Render(pastTenseCapitalized+" ") + ui.ID.Render(r.bean.ID))
}
}
return nil
}

// resolveAppendContent handles --append value, supporting stdin with "-".
func resolveAppendContent(value string) (string, error) {
if value == "-" {
Expand Down
Loading