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:
@@ -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
79
cmd/bd/export.go
Normal 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
133
cmd/bd/import.go
Normal 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
70
cmd/bd/init.go
Normal 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)
|
||||
}
|
||||
@@ -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
94
cmd/bd/quickstart.go
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user