feat(setup): refactor to recipe-based architecture (bd-i3ed)
Replace tool-specific setup commands with a generic recipe-based system. New tools become config entries, not code changes. Changes: - Add internal/recipes/ package with Recipe type and built-in recipes - Add --list flag to show available recipes - Add --print flag to output template to stdout - Add -o flag to write template to arbitrary path - Add --add flag to save custom recipes to .beads/recipes.toml - Add built-in recipes: windsurf, cody, kilocode (new) - Legacy recipes (cursor, claude, gemini, aider, factory) continue to work The recipe system enables: - Adding new tool support without code changes - User-defined recipes in .beads/recipes.toml - Shared template across all file-based integrations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
393
cmd/bd/setup.go
393
cmd/bd/setup.go
@@ -1,8 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/cmd/bd/setup"
|
"github.com/steveyegge/beads/cmd/bd/setup"
|
||||||
|
"github.com/steveyegge/beads/internal/recipes"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -10,162 +17,310 @@ var (
|
|||||||
setupCheck bool
|
setupCheck bool
|
||||||
setupRemove bool
|
setupRemove bool
|
||||||
setupStealth bool
|
setupStealth bool
|
||||||
|
setupPrint bool
|
||||||
|
setupOutput string
|
||||||
|
setupList bool
|
||||||
|
setupAdd string
|
||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup [recipe]",
|
||||||
GroupID: "setup",
|
GroupID: "setup",
|
||||||
Short: "Setup integration with AI editors",
|
Short: "Setup integration with AI editors",
|
||||||
Long: `Setup integration files for AI editors like Claude Code, Cursor, Aider, and Factory.ai Droid.`,
|
Long: `Setup integration files for AI editors and coding assistants.
|
||||||
|
|
||||||
|
Recipes define where beads workflow instructions are written. Built-in recipes
|
||||||
|
include cursor, claude, gemini, aider, factory, windsurf, cody, and kilocode.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
bd setup cursor # Install Cursor IDE integration
|
||||||
|
bd setup --list # Show all available recipes
|
||||||
|
bd setup --print # Print the template to stdout
|
||||||
|
bd setup -o rules.md # Write template to custom path
|
||||||
|
bd setup --add myeditor .myeditor/rules.md # Add custom recipe
|
||||||
|
|
||||||
|
Use 'bd setup <recipe> --check' to verify installation status.
|
||||||
|
Use 'bd setup <recipe> --remove' to uninstall.`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runSetup,
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupCursorCmd = &cobra.Command{
|
func runSetup(cmd *cobra.Command, args []string) {
|
||||||
Use: "cursor",
|
// Handle --list flag
|
||||||
Short: "Setup Cursor IDE integration",
|
if setupList {
|
||||||
Long: `Install Beads workflow rules for Cursor IDE.
|
listRecipes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Creates .cursor/rules/beads.mdc with bd workflow context.
|
// Handle --print flag (no recipe needed)
|
||||||
Uses BEGIN/END markers for safe idempotent updates.`,
|
if setupPrint {
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
fmt.Print(recipes.Template)
|
||||||
if setupCheck {
|
return
|
||||||
setup.CheckCursor()
|
}
|
||||||
return
|
|
||||||
|
// Handle -o flag (write to arbitrary path)
|
||||||
|
if setupOutput != "" {
|
||||||
|
if err := writeToPath(setupOutput); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("✓ Wrote template to %s\n", setupOutput)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if setupRemove {
|
// Handle --add flag (save custom recipe)
|
||||||
setup.RemoveCursor()
|
if setupAdd != "" {
|
||||||
return
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: --add requires a path argument")
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: bd setup --add <name> <path>")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if err := addRecipe(setupAdd, args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setup.InstallCursor()
|
// Require a recipe name for install/check/remove
|
||||||
},
|
if len(args) == 0 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeName := strings.ToLower(args[0])
|
||||||
|
runRecipe(recipeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupAiderCmd = &cobra.Command{
|
func listRecipes() {
|
||||||
Use: "aider",
|
beadsDir := findBeadsDir()
|
||||||
Short: "Setup Aider integration",
|
allRecipes, err := recipes.GetAllRecipes(beadsDir)
|
||||||
Long: `Install Beads workflow configuration for Aider.
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading recipes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
Creates .aider.conf.yml with bd workflow instructions.
|
// Sort recipe names
|
||||||
The AI will suggest bd commands for you to run via /run.
|
names := make([]string, 0, len(allRecipes))
|
||||||
|
for name := range allRecipes {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
Note: Aider requires explicit command execution - the AI cannot
|
fmt.Println("Available recipes:")
|
||||||
run commands autonomously. It will suggest bd commands which you
|
fmt.Println()
|
||||||
must confirm using Aider's /run command.`,
|
for _, name := range names {
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
r := allRecipes[name]
|
||||||
if setupCheck {
|
source := "built-in"
|
||||||
setup.CheckAider()
|
if !recipes.IsBuiltin(name) {
|
||||||
return
|
source = "user"
|
||||||
}
|
}
|
||||||
|
fmt.Printf(" %-12s %-25s (%s)\n", name, r.Description, source)
|
||||||
if setupRemove {
|
}
|
||||||
setup.RemoveAider()
|
fmt.Println()
|
||||||
return
|
fmt.Println("Use 'bd setup <recipe>' to install.")
|
||||||
}
|
fmt.Println("Use 'bd setup --add <name> <path>' to add a custom recipe.")
|
||||||
|
|
||||||
setup.InstallAider()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupFactoryCmd = &cobra.Command{
|
func writeToPath(path string) error {
|
||||||
Use: "factory",
|
// Ensure parent directory exists
|
||||||
Short: "Setup Factory.ai (Droid) integration",
|
dir := filepath.Dir(path)
|
||||||
Long: `Install Beads workflow configuration for Factory.ai Droid.
|
if dir != "." && dir != "" {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
Creates or updates AGENTS.md with bd workflow instructions.
|
return fmt.Errorf("create directory: %w", err)
|
||||||
Factory Droids automatically read AGENTS.md on session start.
|
|
||||||
|
|
||||||
AGENTS.md is the standard format used across AI coding assistants
|
|
||||||
(Factory, Cursor, Aider, Gemini CLI, Jules, and more).`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if setupCheck {
|
|
||||||
setup.CheckFactory()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if setupRemove {
|
if err := os.WriteFile(path, []byte(recipes.Template), 0o644); err != nil {
|
||||||
setup.RemoveFactory()
|
return fmt.Errorf("write file: %w", err)
|
||||||
return
|
}
|
||||||
}
|
return nil
|
||||||
|
|
||||||
setup.InstallFactory()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupClaudeCmd = &cobra.Command{
|
func addRecipe(name, path string) error {
|
||||||
Use: "claude",
|
beadsDir := findBeadsDir()
|
||||||
Short: "Setup Claude Code integration",
|
if beadsDir == "" {
|
||||||
Long: `Install Claude Code hooks that auto-inject bd workflow context.
|
beadsDir = ".beads"
|
||||||
|
}
|
||||||
|
|
||||||
By default, installs hooks globally (~/.claude/settings.json).
|
if err := recipes.SaveUserRecipe(beadsDir, name, path); err != nil {
|
||||||
Use --project flag to install only for this project.
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Hooks call 'bd prime' on SessionStart and PreCompact events to prevent
|
fmt.Printf("✓ Added recipe '%s' → %s\n", name, path)
|
||||||
agents from forgetting bd workflow after context compaction.`,
|
fmt.Printf(" Config: %s/recipes.toml\n", beadsDir)
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
fmt.Println()
|
||||||
if setupCheck {
|
fmt.Printf("Install with: bd setup %s\n", name)
|
||||||
setup.CheckClaude()
|
return nil
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if setupRemove {
|
|
||||||
setup.RemoveClaude(setupProject)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setup.InstallClaude(setupProject, setupStealth)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupGeminiCmd = &cobra.Command{
|
func runRecipe(name string) {
|
||||||
Use: "gemini",
|
// Check for legacy recipes that need special handling
|
||||||
Short: "Setup Gemini CLI integration",
|
switch name {
|
||||||
Long: `Install Gemini CLI hooks that auto-inject bd workflow context.
|
case "claude":
|
||||||
|
runClaudeRecipe()
|
||||||
|
return
|
||||||
|
case "gemini":
|
||||||
|
runGeminiRecipe()
|
||||||
|
return
|
||||||
|
case "factory":
|
||||||
|
runFactoryRecipe()
|
||||||
|
return
|
||||||
|
case "aider":
|
||||||
|
runAiderRecipe()
|
||||||
|
return
|
||||||
|
case "cursor":
|
||||||
|
runCursorRecipe()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
By default, installs hooks globally (~/.gemini/settings.json).
|
// For all other recipes (built-in or user), use generic file-based install
|
||||||
Use --project flag to install only for this project.
|
beadsDir := findBeadsDir()
|
||||||
|
recipe, err := recipes.GetRecipe(name, beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "Use 'bd setup --list' to see available recipes.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
Hooks call 'bd prime' on SessionStart and PreCompress events to prevent
|
if recipe.Type != recipes.TypeFile {
|
||||||
agents from forgetting bd workflow after context compaction.`,
|
fmt.Fprintf(os.Stderr, "Error: recipe '%s' has type '%s' which requires special handling\n", name, recipe.Type)
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
os.Exit(1)
|
||||||
if setupCheck {
|
}
|
||||||
setup.CheckGemini()
|
|
||||||
return
|
// Handle --check
|
||||||
|
if setupCheck {
|
||||||
|
if _, err := os.Stat(recipe.Path); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("✗ %s integration not installed\n", recipe.Name)
|
||||||
|
fmt.Printf(" Run: bd setup %s\n", name)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("✓ %s integration installed: %s\n", recipe.Name, recipe.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if setupRemove {
|
// Handle --remove
|
||||||
setup.RemoveGemini(setupProject)
|
if setupRemove {
|
||||||
return
|
if err := os.Remove(recipe.Path); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("No integration file found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
fmt.Printf("✓ Removed %s integration\n", recipe.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setup.InstallGemini(setupProject, setupStealth)
|
// Install
|
||||||
},
|
fmt.Printf("Installing %s integration...\n", recipe.Name)
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
dir := filepath.Dir(recipe.Path)
|
||||||
|
if dir != "." && dir != "" {
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: create directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(recipe.Path, []byte(recipes.Template), 0o644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: write file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n✓ %s integration installed\n", recipe.Name)
|
||||||
|
fmt.Printf(" File: %s\n", recipe.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy recipe handlers that delegate to existing implementations
|
||||||
|
|
||||||
|
func runCursorRecipe() {
|
||||||
|
if setupCheck {
|
||||||
|
setup.CheckCursor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if setupRemove {
|
||||||
|
setup.RemoveCursor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.InstallCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runClaudeRecipe() {
|
||||||
|
if setupCheck {
|
||||||
|
setup.CheckClaude()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if setupRemove {
|
||||||
|
setup.RemoveClaude(setupProject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.InstallClaude(setupProject, setupStealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGeminiRecipe() {
|
||||||
|
if setupCheck {
|
||||||
|
setup.CheckGemini()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if setupRemove {
|
||||||
|
setup.RemoveGemini(setupProject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.InstallGemini(setupProject, setupStealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFactoryRecipe() {
|
||||||
|
if setupCheck {
|
||||||
|
setup.CheckFactory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if setupRemove {
|
||||||
|
setup.RemoveFactory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.InstallFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAiderRecipe() {
|
||||||
|
if setupCheck {
|
||||||
|
setup.CheckAider()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if setupRemove {
|
||||||
|
setup.RemoveAider()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setup.InstallAider()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBeadsDir() string {
|
||||||
|
// Check for .beads in current directory
|
||||||
|
if info, err := os.Stat(".beads"); err == nil && info.IsDir() {
|
||||||
|
return ".beads"
|
||||||
|
}
|
||||||
|
// Check for redirected beads directory
|
||||||
|
redirectPath := ".beads/.redirect"
|
||||||
|
if data, err := os.ReadFile(redirectPath); err == nil {
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
return ".beads"
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
setupFactoryCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Factory.ai integration is installed")
|
// Global flags for the setup command
|
||||||
setupFactoryCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd section from AGENTS.md")
|
setupCmd.Flags().BoolVar(&setupList, "list", false, "List all available recipes")
|
||||||
|
setupCmd.Flags().BoolVar(&setupPrint, "print", false, "Print the template to stdout")
|
||||||
|
setupCmd.Flags().StringVarP(&setupOutput, "output", "o", "", "Write template to custom path")
|
||||||
|
setupCmd.Flags().StringVar(&setupAdd, "add", "", "Add a custom recipe with given name")
|
||||||
|
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)")
|
// Per-recipe flags
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Claude integration is installed")
|
setupCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if integration is installed")
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Claude settings")
|
setupCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove the integration")
|
||||||
setupClaudeCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use 'bd prime --stealth' (flush only, no git operations)")
|
setupCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (claude/gemini)")
|
||||||
|
setupCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use stealth mode (claude/gemini)")
|
||||||
|
|
||||||
setupCursorCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Cursor integration is installed")
|
|
||||||
setupCursorCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd rules from Cursor")
|
|
||||||
|
|
||||||
setupAiderCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Aider integration is installed")
|
|
||||||
setupAiderCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd config from Aider")
|
|
||||||
|
|
||||||
setupGeminiCmd.Flags().BoolVar(&setupProject, "project", false, "Install for this project only (not globally)")
|
|
||||||
setupGeminiCmd.Flags().BoolVar(&setupCheck, "check", false, "Check if Gemini CLI integration is installed")
|
|
||||||
setupGeminiCmd.Flags().BoolVar(&setupRemove, "remove", false, "Remove bd hooks from Gemini CLI settings")
|
|
||||||
setupGeminiCmd.Flags().BoolVar(&setupStealth, "stealth", false, "Use 'bd prime --stealth' (flush only, no git operations)")
|
|
||||||
|
|
||||||
setupCmd.AddCommand(setupFactoryCmd)
|
|
||||||
setupCmd.AddCommand(setupClaudeCmd)
|
|
||||||
setupCmd.AddCommand(setupCursorCmd)
|
|
||||||
setupCmd.AddCommand(setupAiderCmd)
|
|
||||||
setupCmd.AddCommand(setupGeminiCmd)
|
|
||||||
rootCmd.AddCommand(setupCmd)
|
rootCmd.AddCommand(setupCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
107
docs/SETUP.md
107
docs/SETUP.md
@@ -1,36 +1,53 @@
|
|||||||
# Setup Command Reference
|
# Setup Command Reference
|
||||||
|
|
||||||
**For:** Setting up beads integration with AI coding tools
|
**For:** Setting up beads integration with AI coding tools
|
||||||
**Version:** 0.29.0+
|
**Version:** 0.30.0+
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The `bd setup` command configures beads integration with AI coding tools. It supports five integrations:
|
The `bd setup` command uses a **recipe-based architecture** to configure beads integration with AI coding tools. Recipes define where workflow instructions are written—built-in recipes handle popular tools, and you can add custom recipes for any tool.
|
||||||
|
|
||||||
| Tool | Command | Integration Type |
|
### Built-in Recipes
|
||||||
|------|---------|-----------------|
|
|
||||||
| [Factory.ai (Droid)](#factoryai-droid) | `bd setup factory` | AGENTS.md file (universal standard) |
|
| Recipe | Path | Integration Type |
|
||||||
| [Claude Code](#claude-code) | `bd setup claude` | SessionStart/PreCompact hooks |
|
|--------|------|-----------------|
|
||||||
| [Gemini CLI](#gemini-cli) | `bd setup gemini` | SessionStart/PreCompress hooks |
|
| `cursor` | `.cursor/rules/beads.mdc` | Rules file |
|
||||||
| [Cursor IDE](#cursor-ide) | `bd setup cursor` | Rules file (.cursor/rules/beads.mdc) |
|
| `windsurf` | `.windsurf/rules/beads.md` | Rules file |
|
||||||
| [Aider](#aider) | `bd setup aider` | Config file (.aider.conf.yml) |
|
| `cody` | `.cody/rules/beads.md` | Rules file |
|
||||||
|
| `kilocode` | `.kilocode/rules/beads.md` | Rules file |
|
||||||
|
| `claude` | `~/.claude/settings.json` | SessionStart/PreCompact hooks |
|
||||||
|
| `gemini` | `~/.gemini/settings.json` | SessionStart/PreCompress hooks |
|
||||||
|
| `factory` | `AGENTS.md` | Marked section |
|
||||||
|
| `aider` | `.aider.conf.yml` + `.aider/` | Multi-file config |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# List all available recipes
|
||||||
|
bd setup --list
|
||||||
|
|
||||||
# Install integration for your tool
|
# Install integration for your tool
|
||||||
bd setup factory # For Factory.ai Droid (and other AGENTS.md-compatible tools)
|
bd setup cursor # Cursor IDE
|
||||||
bd setup claude # For Claude Code
|
bd setup windsurf # Windsurf
|
||||||
bd setup gemini # For Gemini CLI
|
bd setup kilocode # Kilo Code
|
||||||
bd setup cursor # For Cursor IDE
|
bd setup claude # Claude Code
|
||||||
bd setup aider # For Aider
|
bd setup gemini # Gemini CLI
|
||||||
|
bd setup factory # Factory.ai Droid
|
||||||
|
bd setup aider # Aider
|
||||||
|
|
||||||
# Verify installation
|
# Verify installation
|
||||||
bd setup factory --check
|
|
||||||
bd setup claude --check
|
|
||||||
bd setup gemini --check
|
|
||||||
bd setup cursor --check
|
bd setup cursor --check
|
||||||
bd setup aider --check
|
bd setup claude --check
|
||||||
|
|
||||||
|
# Print template to stdout (for inspection)
|
||||||
|
bd setup --print
|
||||||
|
|
||||||
|
# Write template to custom path
|
||||||
|
bd setup -o .my-editor/rules.md
|
||||||
|
|
||||||
|
# Add a custom recipe
|
||||||
|
bd setup --add myeditor .myeditor/rules.md
|
||||||
|
bd setup myeditor # Now you can use it
|
||||||
```
|
```
|
||||||
|
|
||||||
## Factory.ai (Droid)
|
## Factory.ai (Droid)
|
||||||
@@ -396,6 +413,60 @@ bd setup claude --remove
|
|||||||
bd setup claude --project
|
bd setup claude --project
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom Recipes
|
||||||
|
|
||||||
|
You can add custom recipes for editors/tools not included in the built-in list.
|
||||||
|
|
||||||
|
### Adding a Custom Recipe
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a recipe that writes to a specific path
|
||||||
|
bd setup --add myeditor .myeditor/rules.md
|
||||||
|
|
||||||
|
# Install it
|
||||||
|
bd setup myeditor
|
||||||
|
|
||||||
|
# Check it
|
||||||
|
bd setup myeditor --check
|
||||||
|
|
||||||
|
# Remove it
|
||||||
|
bd setup myeditor --remove
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Recipes File
|
||||||
|
|
||||||
|
Custom recipes are stored in `.beads/recipes.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[recipes.myeditor]
|
||||||
|
name = "myeditor"
|
||||||
|
path = ".myeditor/rules.md"
|
||||||
|
type = "file"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Arbitrary Paths
|
||||||
|
|
||||||
|
For one-off installs without saving a recipe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Write template to any path
|
||||||
|
bd setup -o .my-custom-location/beads.md
|
||||||
|
|
||||||
|
# Inspect the template first
|
||||||
|
bd setup --print
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recipe Types
|
||||||
|
|
||||||
|
| Type | Description | Example |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `file` | Write template to a single file | cursor, windsurf, cody, kilocode |
|
||||||
|
| `hooks` | Modify JSON settings to add hooks | claude, gemini |
|
||||||
|
| `section` | Inject marked section into existing file | factory |
|
||||||
|
| `multifile` | Write multiple files | aider |
|
||||||
|
|
||||||
|
Custom recipes added via `--add` are always type `file`.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) - Design decisions for Claude Code integration
|
- [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) - Design decisions for Claude Code integration
|
||||||
|
|||||||
244
internal/recipes/recipes.go
Normal file
244
internal/recipes/recipes.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// Package recipes provides recipe-based configuration for bd setup.
|
||||||
|
// Recipes define where beads workflow instructions are written for different AI tools.
|
||||||
|
package recipes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecipeType indicates how the recipe is installed.
|
||||||
|
type RecipeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TypeFile writes template to a file path (simple case)
|
||||||
|
TypeFile RecipeType = "file"
|
||||||
|
// TypeHooks modifies JSON settings to add hooks (claude, gemini)
|
||||||
|
TypeHooks RecipeType = "hooks"
|
||||||
|
// TypeSection injects a marked section into existing file (factory)
|
||||||
|
TypeSection RecipeType = "section"
|
||||||
|
// TypeMultiFile writes multiple files (aider)
|
||||||
|
TypeMultiFile RecipeType = "multifile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recipe defines an AI tool integration.
|
||||||
|
type Recipe struct {
|
||||||
|
Name string `toml:"name"` // Display name (e.g., "Cursor IDE")
|
||||||
|
Path string `toml:"path"` // Primary file path (for TypeFile)
|
||||||
|
Type RecipeType `toml:"type"` // How to install
|
||||||
|
Description string `toml:"description"` // Brief description
|
||||||
|
// Optional fields for complex recipes
|
||||||
|
GlobalPath string `toml:"global_path"` // Global settings path (for hooks)
|
||||||
|
ProjectPath string `toml:"project_path"` // Project settings path (for hooks)
|
||||||
|
Paths []string `toml:"paths"` // Multiple paths (for multifile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuiltinRecipes contains the default recipe definitions.
|
||||||
|
// These are compiled into the binary.
|
||||||
|
var BuiltinRecipes = map[string]Recipe{
|
||||||
|
"cursor": {
|
||||||
|
Name: "Cursor IDE",
|
||||||
|
Path: ".cursor/rules/beads.mdc",
|
||||||
|
Type: TypeFile,
|
||||||
|
Description: "Cursor IDE rules file",
|
||||||
|
},
|
||||||
|
"windsurf": {
|
||||||
|
Name: "Windsurf",
|
||||||
|
Path: ".windsurf/rules/beads.md",
|
||||||
|
Type: TypeFile,
|
||||||
|
Description: "Windsurf editor rules file",
|
||||||
|
},
|
||||||
|
"cody": {
|
||||||
|
Name: "Sourcegraph Cody",
|
||||||
|
Path: ".cody/rules/beads.md",
|
||||||
|
Type: TypeFile,
|
||||||
|
Description: "Cody AI rules file",
|
||||||
|
},
|
||||||
|
"kilocode": {
|
||||||
|
Name: "Kilo Code",
|
||||||
|
Path: ".kilocode/rules/beads.md",
|
||||||
|
Type: TypeFile,
|
||||||
|
Description: "Kilo Code rules file",
|
||||||
|
},
|
||||||
|
"claude": {
|
||||||
|
Name: "Claude Code",
|
||||||
|
Type: TypeHooks,
|
||||||
|
Description: "Claude Code hooks (SessionStart, PreCompact)",
|
||||||
|
GlobalPath: "~/.claude/settings.json",
|
||||||
|
ProjectPath: ".claude/settings.local.json",
|
||||||
|
},
|
||||||
|
"gemini": {
|
||||||
|
Name: "Gemini CLI",
|
||||||
|
Type: TypeHooks,
|
||||||
|
Description: "Gemini CLI hooks (SessionStart, PreCompress)",
|
||||||
|
GlobalPath: "~/.gemini/settings.json",
|
||||||
|
ProjectPath: ".gemini/settings.json",
|
||||||
|
},
|
||||||
|
"factory": {
|
||||||
|
Name: "Factory.ai (Droid)",
|
||||||
|
Path: "AGENTS.md",
|
||||||
|
Type: TypeSection,
|
||||||
|
Description: "Factory Droid AGENTS.md section",
|
||||||
|
},
|
||||||
|
"aider": {
|
||||||
|
Name: "Aider",
|
||||||
|
Type: TypeMultiFile,
|
||||||
|
Description: "Aider config and instruction files",
|
||||||
|
Paths: []string{".aider.conf.yml", ".aider/BEADS.md", ".aider/README.md"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRecipes holds recipes loaded from user config file.
|
||||||
|
type UserRecipes struct {
|
||||||
|
Recipes map[string]Recipe `toml:"recipes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadUserRecipes loads recipes from .beads/recipes.toml if it exists.
|
||||||
|
func LoadUserRecipes(beadsDir string) (map[string]Recipe, error) {
|
||||||
|
path := filepath.Join(beadsDir, "recipes.toml")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil // No user recipes, that's fine
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userRecipes UserRecipes
|
||||||
|
if err := toml.Unmarshal(data, &userRecipes); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults for user recipes
|
||||||
|
for name, recipe := range userRecipes.Recipes {
|
||||||
|
if recipe.Type == "" {
|
||||||
|
recipe.Type = TypeFile
|
||||||
|
}
|
||||||
|
if recipe.Name == "" {
|
||||||
|
recipe.Name = name
|
||||||
|
}
|
||||||
|
userRecipes.Recipes[name] = recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRecipes.Recipes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRecipes returns merged built-in and user recipes.
|
||||||
|
// User recipes override built-in recipes with the same name.
|
||||||
|
func GetAllRecipes(beadsDir string) (map[string]Recipe, error) {
|
||||||
|
result := make(map[string]Recipe)
|
||||||
|
|
||||||
|
// Start with built-in recipes
|
||||||
|
for name, recipe := range BuiltinRecipes {
|
||||||
|
result[name] = recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and merge user recipes
|
||||||
|
userRecipes, err := LoadUserRecipes(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for name, recipe := range userRecipes {
|
||||||
|
result[name] = recipe
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecipe looks up a recipe by name, checking user recipes first.
|
||||||
|
func GetRecipe(name string, beadsDir string) (*Recipe, error) {
|
||||||
|
// Normalize name (lowercase, strip leading/trailing hyphens)
|
||||||
|
name = strings.ToLower(strings.Trim(name, "-"))
|
||||||
|
|
||||||
|
recipes, err := GetAllRecipes(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recipe, ok := recipes[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown recipe: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &recipe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUserRecipe adds or updates a recipe in .beads/recipes.toml.
|
||||||
|
func SaveUserRecipe(beadsDir, name, path string) error {
|
||||||
|
recipesPath := filepath.Join(beadsDir, "recipes.toml")
|
||||||
|
|
||||||
|
// Load existing user recipes
|
||||||
|
var userRecipes UserRecipes
|
||||||
|
data, err := os.ReadFile(recipesPath)
|
||||||
|
if err == nil {
|
||||||
|
if err := toml.Unmarshal(data, &userRecipes); err != nil {
|
||||||
|
return fmt.Errorf("parse recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("read recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userRecipes.Recipes == nil {
|
||||||
|
userRecipes.Recipes = make(map[string]Recipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update the recipe
|
||||||
|
userRecipes.Recipes[name] = Recipe{
|
||||||
|
Name: name,
|
||||||
|
Path: path,
|
||||||
|
Type: TypeFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if err := os.MkdirAll(beadsDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create beads dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
f, err := os.Create(recipesPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
encoder := toml.NewEncoder(f)
|
||||||
|
if err := encoder.Encode(userRecipes); err != nil {
|
||||||
|
return fmt.Errorf("encode recipes.toml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRecipeNames returns sorted list of all recipe names.
|
||||||
|
func ListRecipeNames(beadsDir string) ([]string, error) {
|
||||||
|
recipes, err := GetAllRecipes(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(recipes))
|
||||||
|
for name := range recipes {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
for i := 0; i < len(names); i++ {
|
||||||
|
for j := i + 1; j < len(names); j++ {
|
||||||
|
if names[i] > names[j] {
|
||||||
|
names[i], names[j] = names[j], names[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBuiltin returns true if the recipe is a built-in (not user-defined).
|
||||||
|
func IsBuiltin(name string) bool {
|
||||||
|
_, ok := BuiltinRecipes[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
188
internal/recipes/recipes_test.go
Normal file
188
internal/recipes/recipes_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package recipes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuiltinRecipes(t *testing.T) {
|
||||||
|
// Ensure all expected built-in recipes exist
|
||||||
|
expected := []string{"cursor", "windsurf", "cody", "kilocode", "claude", "gemini", "factory", "aider"}
|
||||||
|
|
||||||
|
for _, name := range expected {
|
||||||
|
recipe, ok := BuiltinRecipes[name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing built-in recipe: %s", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if recipe.Name == "" {
|
||||||
|
t.Errorf("recipe %s has empty Name", name)
|
||||||
|
}
|
||||||
|
if recipe.Type == "" {
|
||||||
|
t.Errorf("recipe %s has empty Type", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecipe(t *testing.T) {
|
||||||
|
// Test getting a built-in recipe
|
||||||
|
recipe, err := GetRecipe("cursor", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRecipe(cursor): %v", err)
|
||||||
|
}
|
||||||
|
if recipe.Name != "Cursor IDE" {
|
||||||
|
t.Errorf("got Name=%q, want 'Cursor IDE'", recipe.Name)
|
||||||
|
}
|
||||||
|
if recipe.Path != ".cursor/rules/beads.mdc" {
|
||||||
|
t.Errorf("got Path=%q, want '.cursor/rules/beads.mdc'", recipe.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test unknown recipe
|
||||||
|
_, err = GetRecipe("nonexistent", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetRecipe(nonexistent) should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBuiltin(t *testing.T) {
|
||||||
|
if !IsBuiltin("cursor") {
|
||||||
|
t.Error("cursor should be builtin")
|
||||||
|
}
|
||||||
|
if IsBuiltin("myeditor") {
|
||||||
|
t.Error("myeditor should not be builtin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListRecipeNames(t *testing.T) {
|
||||||
|
names, err := ListRecipeNames("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListRecipeNames: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that it's sorted
|
||||||
|
for i := 1; i < len(names); i++ {
|
||||||
|
if names[i-1] > names[i] {
|
||||||
|
t.Errorf("names not sorted: %v", names)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expected recipes present
|
||||||
|
found := make(map[string]bool)
|
||||||
|
for _, name := range names {
|
||||||
|
found[name] = true
|
||||||
|
}
|
||||||
|
for _, expected := range []string{"cursor", "claude", "aider"} {
|
||||||
|
if !found[expected] {
|
||||||
|
t.Errorf("expected recipe %s not in list", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserRecipes(t *testing.T) {
|
||||||
|
// Create temp directory
|
||||||
|
tmpDir, err := os.MkdirTemp("", "recipes-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Save a user recipe
|
||||||
|
if err := SaveUserRecipe(tmpDir, "myeditor", ".myeditor/rules.md"); err != nil {
|
||||||
|
t.Fatalf("SaveUserRecipe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file was created
|
||||||
|
recipesPath := filepath.Join(tmpDir, "recipes.toml")
|
||||||
|
if _, err := os.Stat(recipesPath); os.IsNotExist(err) {
|
||||||
|
t.Error("recipes.toml was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user recipes
|
||||||
|
userRecipes, err := LoadUserRecipes(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadUserRecipes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recipe, ok := userRecipes["myeditor"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("myeditor recipe not found")
|
||||||
|
}
|
||||||
|
if recipe.Path != ".myeditor/rules.md" {
|
||||||
|
t.Errorf("got Path=%q, want '.myeditor/rules.md'", recipe.Path)
|
||||||
|
}
|
||||||
|
if recipe.Type != TypeFile {
|
||||||
|
t.Errorf("got Type=%q, want 'file'", recipe.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecipe should find user recipe
|
||||||
|
r, err := GetRecipe("myeditor", tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRecipe(myeditor): %v", err)
|
||||||
|
}
|
||||||
|
if r.Path != ".myeditor/rules.md" {
|
||||||
|
t.Errorf("got Path=%q, want '.myeditor/rules.md'", r.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserRecipeOverride(t *testing.T) {
|
||||||
|
// Create temp directory
|
||||||
|
tmpDir, err := os.MkdirTemp("", "recipes-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Save a user recipe that overrides cursor
|
||||||
|
if err := SaveUserRecipe(tmpDir, "cursor", ".my-cursor/rules.md"); err != nil {
|
||||||
|
t.Fatalf("SaveUserRecipe: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecipe should return user's version
|
||||||
|
r, err := GetRecipe("cursor", tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRecipe(cursor): %v", err)
|
||||||
|
}
|
||||||
|
if r.Path != ".my-cursor/rules.md" {
|
||||||
|
t.Errorf("user override failed: got Path=%q, want '.my-cursor/rules.md'", r.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadUserRecipesNoFile(t *testing.T) {
|
||||||
|
// Should return nil, nil when no file exists
|
||||||
|
recipes, err := LoadUserRecipes("/nonexistent/path")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("LoadUserRecipes should not error on missing file: %v", err)
|
||||||
|
}
|
||||||
|
if recipes != nil {
|
||||||
|
t.Error("LoadUserRecipes should return nil for missing file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemplate(t *testing.T) {
|
||||||
|
// Basic sanity check that template is not empty
|
||||||
|
if len(Template) < 100 {
|
||||||
|
t.Error("Template is suspiciously short")
|
||||||
|
}
|
||||||
|
// Check for key content
|
||||||
|
if !containsAll(Template, "Beads", "bd ready", "bd create", "bd close") {
|
||||||
|
t.Error("Template missing expected content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsAll(s string, substrs ...string) bool {
|
||||||
|
for _, sub := range substrs {
|
||||||
|
found := false
|
||||||
|
for i := 0; i <= len(s)-len(sub); i++ {
|
||||||
|
if s[i:i+len(sub)] == sub {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
59
internal/recipes/template.go
Normal file
59
internal/recipes/template.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package recipes
|
||||||
|
|
||||||
|
// Template is the universal beads workflow template.
|
||||||
|
// This content is written to all file-based recipes.
|
||||||
|
const Template = `# Beads Issue Tracking
|
||||||
|
|
||||||
|
This project uses [Beads (bd)](https://github.com/steveyegge/beads) for issue tracking.
|
||||||
|
|
||||||
|
## Core Rules
|
||||||
|
|
||||||
|
- Track ALL work in bd (never use markdown TODOs or comment-based task lists)
|
||||||
|
- Use ` + "`bd ready`" + ` to find available work
|
||||||
|
- Use ` + "`bd create`" + ` to track new issues/tasks/bugs
|
||||||
|
- Use ` + "`bd sync`" + ` at end of session to sync with git remote
|
||||||
|
- Git hooks auto-sync on commit/merge
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
` + "```bash" + `
|
||||||
|
bd prime # Load complete workflow context
|
||||||
|
bd ready # Show issues ready to work (no blockers)
|
||||||
|
bd list --status=open # List all open issues
|
||||||
|
bd create --title="..." --type=task # Create new issue
|
||||||
|
bd update <id> --status=in_progress # Claim work
|
||||||
|
bd close <id> # Mark complete
|
||||||
|
bd dep add <issue> <depends-on> # Add dependency
|
||||||
|
bd sync # Sync with git remote
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Check for ready work: ` + "`bd ready`" + `
|
||||||
|
2. Claim an issue: ` + "`bd update <id> --status=in_progress`" + `
|
||||||
|
3. Do the work
|
||||||
|
4. Mark complete: ` + "`bd close <id>`" + `
|
||||||
|
5. Sync: ` + "`bd sync`" + ` (or let git hooks handle it)
|
||||||
|
|
||||||
|
## Issue Types
|
||||||
|
|
||||||
|
- ` + "`bug`" + ` - Something broken
|
||||||
|
- ` + "`feature`" + ` - New functionality
|
||||||
|
- ` + "`task`" + ` - Work item (tests, docs, refactoring)
|
||||||
|
- ` + "`epic`" + ` - Large feature with subtasks
|
||||||
|
- ` + "`chore`" + ` - Maintenance (dependencies, tooling)
|
||||||
|
|
||||||
|
## Priorities
|
||||||
|
|
||||||
|
- ` + "`0`" + ` - Critical (security, data loss, broken builds)
|
||||||
|
- ` + "`1`" + ` - High (major features, important bugs)
|
||||||
|
- ` + "`2`" + ` - Medium (default, nice-to-have)
|
||||||
|
- ` + "`3`" + ` - Low (polish, optimization)
|
||||||
|
- ` + "`4`" + ` - Backlog (future ideas)
|
||||||
|
|
||||||
|
## Context Loading
|
||||||
|
|
||||||
|
Run ` + "`bd prime`" + ` to get complete workflow documentation in AI-optimized format.
|
||||||
|
|
||||||
|
For detailed docs: see AGENTS.md, QUICKSTART.md, or run ` + "`bd --help`" + `
|
||||||
|
`
|
||||||
Reference in New Issue
Block a user