feat: add Mermaid.js format for dependency tree visualization (#191)
Adds `bd dep tree --format mermaid` to export dependency trees as Mermaid.js flowcharts. Features: - Status indicators: ☐ open, ◧ in_progress, ⚠ blocked, ☑ closed - Theme-agnostic design - Works with --reverse flag - Comprehensive unit tests following TDD Co-authored-by: David Laing <david@davidlaing.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -25,7 +26,6 @@ var depAddCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
depType, _ := cmd.Flags().GetString("type")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -148,7 +148,6 @@ var depRemoveCmd = &cobra.Command{
|
||||
Short: "Remove a dependency",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
@@ -240,7 +239,6 @@ var depTreeCmd = &cobra.Command{
|
||||
Short: "Show dependency tree",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial ID first
|
||||
@@ -276,6 +274,7 @@ var depTreeCmd = &cobra.Command{
|
||||
showAllPaths, _ := cmd.Flags().GetBool("show-all-paths")
|
||||
maxDepth, _ := cmd.Flags().GetInt("max-depth")
|
||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||
formatStr, _ := cmd.Flags().GetString("format")
|
||||
|
||||
if maxDepth < 1 {
|
||||
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
|
||||
@@ -288,6 +287,12 @@ var depTreeCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Handle mermaid format
|
||||
if formatStr == "mermaid" {
|
||||
outputMermaidTree(tree, args[0])
|
||||
return
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
// Always output array, even if empty
|
||||
if tree == nil {
|
||||
@@ -341,8 +346,6 @@ var depCyclesCmd = &cobra.Command{
|
||||
Use: "cycles",
|
||||
Short: "Detect dependency cycles",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
// If daemon is running but doesn't support this command, use direct storage
|
||||
if daemonClient != nil && store == nil {
|
||||
var err error
|
||||
@@ -388,19 +391,71 @@ var depCyclesCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// outputMermaidTree outputs a dependency tree in Mermaid.js flowchart format
|
||||
func outputMermaidTree(tree []*types.TreeNode, rootID string) {
|
||||
if len(tree) == 0 {
|
||||
fmt.Println("flowchart TD")
|
||||
fmt.Printf(" %s[\"No dependencies\"]\n", rootID)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("flowchart TD")
|
||||
|
||||
// Output nodes
|
||||
nodesSeen := make(map[string]bool)
|
||||
for _, node := range tree {
|
||||
if !nodesSeen[node.ID] {
|
||||
emoji := getStatusEmoji(node.Status)
|
||||
label := fmt.Sprintf("%s %s: %s", emoji, node.ID, node.Title)
|
||||
// Escape quotes and backslashes in label
|
||||
label = strings.ReplaceAll(label, "\\", "\\\\")
|
||||
label = strings.ReplaceAll(label, "\"", "\\\"")
|
||||
fmt.Printf(" %s[\"%s\"]\n", node.ID, label)
|
||||
|
||||
nodesSeen[node.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Output edges - use explicit parent relationships from ParentID
|
||||
for _, node := range tree {
|
||||
if node.ParentID != "" && node.ParentID != node.ID {
|
||||
fmt.Printf(" %s --> %s\n", node.ParentID, node.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getStatusEmoji returns a symbol indicator for a given status
|
||||
func getStatusEmoji(status types.Status) string {
|
||||
switch status {
|
||||
case types.StatusOpen:
|
||||
return "☐" // U+2610 Ballot Box
|
||||
case types.StatusInProgress:
|
||||
return "◧" // U+25E7 Square Left Half Black
|
||||
case types.StatusBlocked:
|
||||
return "⚠" // U+26A0 Warning Sign
|
||||
case types.StatusClosed:
|
||||
return "☑" // U+2611 Ballot Box with Check
|
||||
default:
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)")
|
||||
depAddCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
|
||||
depRemoveCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
|
||||
depTreeCmd.Flags().Bool("show-all-paths", false, "Show all paths to nodes (no deduplication for diamond dependencies)")
|
||||
depTreeCmd.Flags().IntP("max-depth", "d", 50, "Maximum tree depth to display (safety limit)")
|
||||
depTreeCmd.Flags().Bool("reverse", false, "Show dependent tree (what was discovered from this) instead of dependency tree (what blocks this)")
|
||||
depTreeCmd.Flags().String("format", "", "Output format: 'mermaid' for Mermaid.js flowchart")
|
||||
depTreeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
|
||||
depCyclesCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
|
||||
depCmd.AddCommand(depAddCmd)
|
||||
depCmd.AddCommand(depRemoveCmd)
|
||||
depCmd.AddCommand(depTreeCmd)
|
||||
|
||||
Reference in New Issue
Block a user