Implement JSONL export/import and shift to text-first architecture

This is a fundamental architectural shift from binary SQLite to JSONL as
the source of truth for git workflows.

## New Features

- `bd export --format=jsonl` - Export issues to JSON Lines format
- `bd import` - Import issues from JSONL (create new, update existing)
- `--skip-existing` flag for import to only create new issues

## Architecture Change

**Before:** Binary SQLite database committed to git
**After:** JSONL text files as source of truth, SQLite as ephemeral cache

Benefits:
- Git-friendly text format with clean diffs
- AI-resolvable merge conflicts (append-only is 95% conflict-free)
- Human-readable issue tracking in git
- No binary merge conflicts

## Documentation

- Updated README with JSONL-first workflow and git hooks
- Added TEXT_FORMATS.md analyzing JSONL vs CSV vs binary
- Updated GIT_WORKFLOW.md with historical context
- .gitignore now excludes *.db, includes .beads/*.jsonl

## Implementation Details

- Export sorts issues by ID for consistent diffs
- Import handles both creates and updates atomically
- Proper handling of pointer fields (EstimatedMinutes)
- All tests passing

## Breaking Changes

- Database files (*.db) should now be gitignored
- Use export/import workflow for git collaboration
- Git hooks recommended for automation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-12 01:17:50 -07:00
parent 9105059843
commit 15afb5ad17
25 changed files with 3322 additions and 129 deletions

View File

@@ -7,7 +7,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyackey/beads/internal/types"
"github.com/steveyegge/beads/internal/types"
)
var depCmd = &cobra.Command{
@@ -34,6 +34,16 @@ var depAddCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
outputJSON(map[string]interface{}{
"status": "added",
"issue_id": args[0],
"depends_on_id": args[1],
"type": depType,
})
return
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
green("✓"), args[0], args[1], depType)
@@ -51,6 +61,15 @@ var depRemoveCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
outputJSON(map[string]interface{}{
"status": "removed",
"issue_id": args[0],
"depends_on_id": args[1],
})
return
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
green("✓"), args[0], args[1])
@@ -69,6 +88,15 @@ var depTreeCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
// Always output array, even if empty
if tree == nil {
tree = []*types.TreeNode{}
}
outputJSON(tree)
return
}
if len(tree) == 0 {
fmt.Printf("\n%s has no dependencies\n", args[0])
return
@@ -110,6 +138,15 @@ var depCyclesCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
// Always output array, even if empty
if cycles == nil {
cycles = [][]*types.Issue{}
}
outputJSON(cycles)
return
}
if len(cycles) == 0 {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("\n%s No dependency cycles detected\n\n", green("✓"))
@@ -129,7 +166,7 @@ var depCyclesCmd = &cobra.Command{
}
func init() {
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child)")
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)")
depCmd.AddCommand(depAddCmd)
depCmd.AddCommand(depRemoveCmd)
depCmd.AddCommand(depTreeCmd)

79
cmd/bd/export.go Normal file
View File

@@ -0,0 +1,79 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
)
var exportCmd = &cobra.Command{
Use: "export",
Short: "Export issues to JSONL format",
Long: `Export all issues to JSON Lines format (one JSON object per line).
Issues are sorted by ID for consistent diffs.
Output to stdout by default, or use -o flag for file output.`,
Run: func(cmd *cobra.Command, args []string) {
format, _ := cmd.Flags().GetString("format")
output, _ := cmd.Flags().GetString("output")
statusFilter, _ := cmd.Flags().GetString("status")
if format != "jsonl" {
fmt.Fprintf(os.Stderr, "Error: only 'jsonl' format is currently supported\n")
os.Exit(1)
}
// Build filter
filter := types.IssueFilter{}
if statusFilter != "" {
status := types.Status(statusFilter)
filter.Status = &status
}
// Get all issues
ctx := context.Background()
issues, err := store.SearchIssues(ctx, "", filter)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
})
// Open output
out := os.Stdout
if output != "" {
f, err := os.Create(output)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
os.Exit(1)
}
defer f.Close()
out = f
}
// Write JSONL
encoder := json.NewEncoder(out)
for _, issue := range issues {
if err := encoder.Encode(issue); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding issue %s: %v\n", issue.ID, err)
os.Exit(1)
}
}
},
}
func init() {
exportCmd.Flags().StringP("format", "f", "jsonl", "Export format (jsonl)")
exportCmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
exportCmd.Flags().StringP("status", "s", "", "Filter by status")
rootCmd.AddCommand(exportCmd)
}

133
cmd/bd/import.go Normal file
View File

@@ -0,0 +1,133 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
)
var importCmd = &cobra.Command{
Use: "import",
Short: "Import issues from JSONL format",
Long: `Import issues from JSON Lines format (one JSON object per line).
Reads from stdin by default, or use -i flag for file input.
Behavior:
- Existing issues (same ID) are updated
- New issues are created
- Import is atomic (all or nothing)`,
Run: func(cmd *cobra.Command, args []string) {
input, _ := cmd.Flags().GetString("input")
skipUpdate, _ := cmd.Flags().GetBool("skip-existing")
// Open input
in := os.Stdin
if input != "" {
f, err := os.Open(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
os.Exit(1)
}
defer f.Close()
in = f
}
// Read and parse JSONL
ctx := context.Background()
scanner := bufio.NewScanner(in)
var created, updated, skipped int
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
// Skip empty lines
if line == "" {
continue
}
// Parse JSON
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
os.Exit(1)
}
// Check if issue exists
existing, err := store.GetIssue(ctx, issue.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error checking issue %s: %v\n", issue.ID, err)
os.Exit(1)
}
if existing != nil {
if skipUpdate {
skipped++
continue
}
// Update existing issue - convert to updates map
updates := make(map[string]interface{})
if issue.Title != "" {
updates["title"] = issue.Title
}
if issue.Description != "" {
updates["description"] = issue.Description
}
if issue.Status != "" {
updates["status"] = issue.Status
}
if issue.Priority != 0 {
updates["priority"] = issue.Priority
}
if issue.IssueType != "" {
updates["issue_type"] = issue.IssueType
}
if issue.Assignee != "" {
updates["assignee"] = issue.Assignee
}
if issue.EstimatedMinutes != nil {
updates["estimated_minutes"] = *issue.EstimatedMinutes
}
if err := store.UpdateIssue(ctx, issue.ID, updates, "import"); err != nil {
fmt.Fprintf(os.Stderr, "Error updating issue %s: %v\n", issue.ID, err)
os.Exit(1)
}
updated++
} else {
// Create new issue
if err := store.CreateIssue(ctx, &issue, "import"); err != nil {
fmt.Fprintf(os.Stderr, "Error creating issue %s: %v\n", issue.ID, err)
os.Exit(1)
}
created++
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
}
// Print summary
fmt.Fprintf(os.Stderr, "Import complete: %d created, %d updated", created, updated)
if skipped > 0 {
fmt.Fprintf(os.Stderr, ", %d skipped", skipped)
}
fmt.Fprintf(os.Stderr, "\n")
},
}
func init() {
importCmd.Flags().StringP("input", "i", "", "Input file (default: stdin)")
importCmd.Flags().BoolP("skip-existing", "s", false, "Skip existing issues instead of updating them")
rootCmd.AddCommand(importCmd)
}

70
cmd/bd/init.go Normal file
View File

@@ -0,0 +1,70 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize bd in the current directory",
Long: `Initialize bd in the current directory by creating a .beads/ directory
and database file. Optionally specify a custom issue prefix.`,
Run: func(cmd *cobra.Command, args []string) {
prefix, _ := cmd.Flags().GetString("prefix")
if prefix == "" {
// Auto-detect from directory name
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
os.Exit(1)
}
prefix = filepath.Base(cwd)
}
// Create .beads directory
beadsDir := ".beads"
if err := os.MkdirAll(beadsDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to create %s directory: %v\n", beadsDir, err)
os.Exit(1)
}
// Create database
dbPath := filepath.Join(beadsDir, prefix+".db")
store, err := sqlite.New(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to create database: %v\n", err)
os.Exit(1)
}
// Set the issue prefix in config
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", prefix); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
store.Close()
os.Exit(1)
}
store.Close()
green := color.New(color.FgGreen).SprintFunc()
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s bd initialized successfully!\n\n", green("✓"))
fmt.Printf(" Database: %s\n", cyan(dbPath))
fmt.Printf(" Issue prefix: %s\n", cyan(prefix))
fmt.Printf(" Issues will be named: %s\n\n", cyan(prefix+"-1, "+prefix+"-2, ..."))
fmt.Printf("Run %s to get started.\n\n", cyan("bd quickstart"))
},
}
func init() {
initCmd.Flags().StringP("prefix", "p", "", "Issue prefix (default: current directory name)")
rootCmd.AddCommand(initCmd)
}

View File

@@ -2,32 +2,49 @@ package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyackey/beads/internal/storage"
"github.com/steveyackey/beads/internal/storage/sqlite"
"github.com/steveyackey/beads/internal/types"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
var (
dbPath string
actor string
store storage.Storage
dbPath string
actor string
store storage.Storage
jsonOutput bool
)
var rootCmd = &cobra.Command{
Use: "beads",
Short: "Beads - Dependency-aware issue tracker",
Use: "bd",
Short: "bd - Dependency-aware issue tracker",
Long: `Issues chained together like beads. A lightweight issue tracker with first-class dependency support.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Skip database initialization for init command
if cmd.Name() == "init" {
return
}
// Initialize storage
if dbPath == "" {
home, _ := os.UserHomeDir()
dbPath = filepath.Join(home, ".beads", "beads.db")
// Try to find database in order:
// 1. $BEADS_DB environment variable
// 2. .beads/*.db in current directory or ancestors
// 3. ~/.beads/default.db
if envDB := os.Getenv("BEADS_DB"); envDB != "" {
dbPath = envDB
} else if foundDB := findDatabase(); foundDB != "" {
dbPath = foundDB
} else {
home, _ := os.UserHomeDir()
dbPath = filepath.Join(home, ".beads", "default.db")
}
}
var err error
@@ -52,9 +69,51 @@ var rootCmd = &cobra.Command{
},
}
// findDatabase searches for .beads/*.db in current directory and ancestors
func findDatabase() string {
dir, err := os.Getwd()
if err != nil {
return ""
}
// Walk up directory tree looking for .beads/ directory
for {
beadsDir := filepath.Join(dir, ".beads")
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
// Found .beads/ directory, look for *.db files
matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
if err == nil && len(matches) > 0 {
// Return first .db file found
return matches[0]
}
}
// Move up one directory
parent := filepath.Dir(dir)
if parent == dir {
// Reached filesystem root
break
}
dir = parent
}
return ""
}
// outputJSON outputs data as pretty-printed JSON
func outputJSON(v interface{}) {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(v); err != nil {
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Database path (default: ~/.beads/beads.db)")
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Database path (default: auto-discover .beads/*.db or ~/.beads/default.db)")
rootCmd.PersistentFlags().StringVar(&actor, "actor", "", "Actor name for audit trail (default: $USER)")
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
}
var createCmd = &cobra.Command{
@@ -95,11 +154,15 @@ var createCmd = &cobra.Command{
}
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Priority: P%d\n", issue.Priority)
fmt.Printf(" Status: %s\n", issue.Status)
if jsonOutput {
outputJSON(issue)
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Created issue: %s\n", green("✓"), issue.ID)
fmt.Printf(" Title: %s\n", issue.Title)
fmt.Printf(" Priority: P%d\n", issue.Priority)
fmt.Printf(" Status: %s\n", issue.Status)
}
},
}
@@ -130,6 +193,22 @@ var showCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
// Include labels and dependencies in JSON output
type IssueDetails struct {
*types.Issue
Labels []string `json:"labels,omitempty"`
Dependencies []*types.Issue `json:"dependencies,omitempty"`
Dependents []*types.Issue `json:"dependents,omitempty"`
}
details := &IssueDetails{Issue: issue}
details.Labels, _ = store.GetLabels(ctx, issue.ID)
details.Dependencies, _ = store.GetDependencies(ctx, issue.ID)
details.Dependents, _ = store.GetDependents(ctx, issue.ID)
outputJSON(details)
return
}
cyan := color.New(color.FgCyan).SprintFunc()
fmt.Printf("\n%s: %s\n", cyan(issue.ID), issue.Title)
fmt.Printf("Status: %s\n", issue.Status)
@@ -222,6 +301,11 @@ var listCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
outputJSON(issues)
return
}
fmt.Printf("\nFound %d issues:\n\n", len(issues))
for _, issue := range issues {
fmt.Printf("%s [P%d] %s\n", issue.ID, issue.Priority, issue.Status)
@@ -278,8 +362,14 @@ var updateCmd = &cobra.Command{
os.Exit(1)
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Updated issue: %s\n", green("✓"), args[0])
if jsonOutput {
// Fetch updated issue and output
issue, _ := store.GetIssue(ctx, args[0])
outputJSON(issue)
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Updated issue: %s\n", green("✓"), args[0])
}
},
}
@@ -302,13 +392,24 @@ var closeCmd = &cobra.Command{
}
ctx := context.Background()
closedIssues := []*types.Issue{}
for _, id := range args {
if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
continue
}
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
if jsonOutput {
issue, _ := store.GetIssue(ctx, id)
if issue != nil {
closedIssues = append(closedIssues, issue)
}
} else {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
}
}
if jsonOutput && len(closedIssues) > 0 {
outputJSON(closedIssues)
}
},
}

94
cmd/bd/quickstart.go Normal file
View File

@@ -0,0 +1,94 @@
package main
import (
"fmt"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var quickstartCmd = &cobra.Command{
Use: "quickstart",
Short: "Quick start guide for bd",
Long: `Display a quick start guide showing common bd workflows and patterns.`,
Run: func(cmd *cobra.Command, args []string) {
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
bold := color.New(color.Bold).SprintFunc()
fmt.Printf("\n%s\n\n", bold("bd - Dependency-Aware Issue Tracker"))
fmt.Printf("Issues chained together like beads.\n\n")
fmt.Printf("%s\n", bold("GETTING STARTED"))
fmt.Printf(" %s Initialize bd in your project\n", cyan("bd init"))
fmt.Printf(" Creates .beads/ directory with project-specific database\n")
fmt.Printf(" Auto-detects prefix from directory name (e.g., myapp-1, myapp-2)\n\n")
fmt.Printf(" %s Initialize with custom prefix\n", cyan("bd init --prefix api"))
fmt.Printf(" Issues will be named: api-1, api-2, ...\n\n")
fmt.Printf("%s\n", bold("CREATING ISSUES"))
fmt.Printf(" %s\n", cyan("bd create \"Fix login bug\""))
fmt.Printf(" %s\n", cyan("bd create \"Add auth\" -p 0 -t feature"))
fmt.Printf(" %s\n\n", cyan("bd create \"Write tests\" -d \"Unit tests for auth\" --assignee alice"))
fmt.Printf("%s\n", bold("VIEWING ISSUES"))
fmt.Printf(" %s List all issues\n", cyan("bd list"))
fmt.Printf(" %s List by status\n", cyan("bd list --status open"))
fmt.Printf(" %s List by priority (0-4, 0=highest)\n", cyan("bd list --priority 0"))
fmt.Printf(" %s Show issue details\n\n", cyan("bd show bd-1"))
fmt.Printf("%s\n", bold("MANAGING DEPENDENCIES"))
fmt.Printf(" %s Add dependency (bd-2 blocks bd-1)\n", cyan("bd dep add bd-1 bd-2"))
fmt.Printf(" %s Visualize dependency tree\n", cyan("bd dep tree bd-1"))
fmt.Printf(" %s Detect circular dependencies\n\n", cyan("bd dep cycles"))
fmt.Printf("%s\n", bold("DEPENDENCY TYPES"))
fmt.Printf(" %s Task B must complete before task A\n", yellow("blocks"))
fmt.Printf(" %s Soft connection, doesn't block progress\n", yellow("related"))
fmt.Printf(" %s Epic/subtask hierarchical relationship\n", yellow("parent-child"))
fmt.Printf(" %s Auto-created when AI discovers related work\n\n", yellow("discovered-from"))
fmt.Printf("%s\n", bold("READY WORK"))
fmt.Printf(" %s Show issues ready to work on\n", cyan("bd ready"))
fmt.Printf(" Ready = status is 'open' AND no blocking dependencies\n")
fmt.Printf(" Perfect for agents to claim next work!\n\n")
fmt.Printf("%s\n", bold("UPDATING ISSUES"))
fmt.Printf(" %s\n", cyan("bd update bd-1 --status in_progress"))
fmt.Printf(" %s\n", cyan("bd update bd-1 --priority 0"))
fmt.Printf(" %s\n\n", cyan("bd update bd-1 --assignee bob"))
fmt.Printf("%s\n", bold("CLOSING ISSUES"))
fmt.Printf(" %s\n", cyan("bd close bd-1"))
fmt.Printf(" %s\n\n", cyan("bd close bd-2 bd-3 --reason \"Fixed in PR #42\""))
fmt.Printf("%s\n", bold("DATABASE LOCATION"))
fmt.Printf(" bd automatically discovers your database:\n")
fmt.Printf(" 1. %s flag\n", cyan("--db /path/to/db.db"))
fmt.Printf(" 2. %s environment variable\n", cyan("$BEADS_DB"))
fmt.Printf(" 3. %s in current directory or ancestors\n", cyan(".beads/*.db"))
fmt.Printf(" 4. %s as fallback\n\n", cyan("~/.beads/default.db"))
fmt.Printf("%s\n", bold("AGENT INTEGRATION"))
fmt.Printf(" bd is designed for AI-supervised workflows:\n")
fmt.Printf(" • Agents create issues when discovering new work\n")
fmt.Printf(" • %s shows unblocked work ready to claim\n", cyan("bd ready"))
fmt.Printf(" • Use %s flags for programmatic parsing\n", cyan("--json"))
fmt.Printf(" • Dependencies prevent agents from duplicating effort\n\n")
fmt.Printf("%s\n", bold("DATABASE EXTENSION"))
fmt.Printf(" Applications can extend bd's SQLite database:\n")
fmt.Printf(" • Add your own tables (e.g., %s)\n", cyan("myapp_executions"))
fmt.Printf(" • Join with %s table for powerful queries\n", cyan("issues"))
fmt.Printf(" • See %s for integration patterns\n\n", cyan("EXTENDING.md"))
fmt.Printf("%s\n", green("Ready to start!"))
fmt.Printf("Run %s to create your first issue.\n\n", cyan("bd create \"My first issue\""))
},
}
func init() {
rootCmd.AddCommand(quickstartCmd)
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyackey/beads/internal/types"
"github.com/steveyegge/beads/internal/types"
)
var readyCmd = &cobra.Command{
@@ -37,6 +37,15 @@ var readyCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
// Always output array, even if empty
if issues == nil {
issues = []*types.Issue{}
}
outputJSON(issues)
return
}
if len(issues) == 0 {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Printf("\n%s No ready work found (all issues have blocking dependencies)\n\n",
@@ -71,6 +80,15 @@ var blockedCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
// Always output array, even if empty
if blocked == nil {
blocked = []*types.BlockedIssue{}
}
outputJSON(blocked)
return
}
if len(blocked) == 0 {
green := color.New(color.FgGreen).SprintFunc()
fmt.Printf("\n%s No blocked issues\n\n", green("✨"))
@@ -100,6 +118,11 @@ var statsCmd = &cobra.Command{
os.Exit(1)
}
if jsonOutput {
outputJSON(stats)
return
}
cyan := color.New(color.FgCyan).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()