feat: add Junie agent integration
Add support for JetBrains Junie AI agent: - Create .junie/guidelines.md with workflow instructions - Create .junie/mcp/mcp.json for MCP server configuration - Add 'junie' to BuiltinRecipes in recipes.go - Add runJunieRecipe() handler in setup.go - Add website documentation - Add integrations/junie/README.md Usage: bd setup junie
This commit is contained in:
committed by
Jan-Niklas W.
parent
279192c5fb
commit
d475e424c2
@@ -172,6 +172,9 @@ func runRecipe(name string) {
|
||||
case "cursor":
|
||||
runCursorRecipe()
|
||||
return
|
||||
case "junie":
|
||||
runJunieRecipe()
|
||||
return
|
||||
}
|
||||
|
||||
// For all other recipes (built-in or user), use generic file-based install
|
||||
@@ -296,6 +299,18 @@ func runAiderRecipe() {
|
||||
setup.InstallAider()
|
||||
}
|
||||
|
||||
func runJunieRecipe() {
|
||||
if setupCheck {
|
||||
setup.CheckJunie()
|
||||
return
|
||||
}
|
||||
if setupRemove {
|
||||
setup.RemoveJunie()
|
||||
return
|
||||
}
|
||||
setup.InstallJunie()
|
||||
}
|
||||
|
||||
func findBeadsDir() string {
|
||||
// Check for .beads in current directory
|
||||
if info, err := os.Stat(".beads"); err == nil && info.IsDir() {
|
||||
|
||||
241
cmd/bd/setup/junie.go
Normal file
241
cmd/bd/setup/junie.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const junieGuidelinesTemplate = `# Beads Issue Tracking Instructions
|
||||
|
||||
This project uses **Beads (bd)** for issue tracking. Use the bd CLI or MCP tools for all task management.
|
||||
|
||||
## Core Workflow Rules
|
||||
|
||||
1. **Track ALL work in bd** - Never use markdown TODOs or comment-based task lists
|
||||
2. **Check ready work first** - Run ` + "`bd ready`" + ` to find unblocked issues
|
||||
3. **Always include descriptions** - Provide meaningful context when creating issues
|
||||
4. **Link discovered work** - Use ` + "`discovered-from`" + ` dependencies for issues found during work
|
||||
5. **Sync at session end** - Run ` + "`bd sync`" + ` before ending your session
|
||||
|
||||
## Quick Command Reference
|
||||
|
||||
### Finding Work
|
||||
` + "```bash" + `
|
||||
bd ready # Show unblocked issues ready for work
|
||||
bd list --status open # List all open issues
|
||||
bd show <id> # View issue details
|
||||
bd blocked # Show blocked issues and their blockers
|
||||
` + "```" + `
|
||||
|
||||
### Creating Issues
|
||||
` + "```bash" + `
|
||||
bd create "Title" --description="Details" -t bug|feature|task -p 0-4 --json
|
||||
bd create "Found bug" --description="Details" --deps discovered-from:bd-42 --json
|
||||
` + "```" + `
|
||||
|
||||
### Working on Issues
|
||||
` + "```bash" + `
|
||||
bd update <id> --status in_progress # Claim work
|
||||
bd update <id> --priority 1 # Change priority
|
||||
bd close <id> --reason "Completed" # Mark complete
|
||||
` + "```" + `
|
||||
|
||||
### Dependencies
|
||||
` + "```bash" + `
|
||||
bd dep add <issue> <depends-on> # Add dependency (issue depends on depends-on)
|
||||
bd dep add <issue> <depends-on> --type=related # Soft link
|
||||
` + "```" + `
|
||||
|
||||
### Syncing
|
||||
` + "```bash" + `
|
||||
bd sync # ALWAYS run at session end - commits and pushes changes
|
||||
` + "```" + `
|
||||
|
||||
## Issue Types
|
||||
|
||||
- ` + "`bug`" + ` - Something broken that needs fixing
|
||||
- ` + "`feature`" + ` - New functionality
|
||||
- ` + "`task`" + ` - Work item (tests, docs, refactoring)
|
||||
- ` + "`epic`" + ` - Large feature composed of multiple issues
|
||||
- ` + "`chore`" + ` - Maintenance work (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)
|
||||
|
||||
## MCP Tools Available
|
||||
|
||||
If the MCP server is configured, you can use these tools directly:
|
||||
- ` + "`mcp_beads_ready`" + ` - Find ready tasks
|
||||
- ` + "`mcp_beads_list`" + ` - List issues with filters
|
||||
- ` + "`mcp_beads_show`" + ` - Show issue details
|
||||
- ` + "`mcp_beads_create`" + ` - Create new issues
|
||||
- ` + "`mcp_beads_update`" + ` - Update issue status/priority
|
||||
- ` + "`mcp_beads_close`" + ` - Close completed issues
|
||||
- ` + "`mcp_beads_dep`" + ` - Manage dependencies
|
||||
- ` + "`mcp_beads_blocked`" + ` - Show blocked issues
|
||||
- ` + "`mcp_beads_stats`" + ` - Get issue statistics
|
||||
|
||||
## Important Rules
|
||||
|
||||
- ✅ Use bd for ALL task tracking
|
||||
- ✅ Always use ` + "`--json`" + ` flag for programmatic use
|
||||
- ✅ Link discovered work with ` + "`discovered-from`" + ` dependencies
|
||||
- ✅ Check ` + "`bd ready`" + ` before asking "what should I work on?"
|
||||
- ✅ Run ` + "`bd sync`" + ` at end of session
|
||||
- ❌ Do NOT create markdown TODO lists
|
||||
- ❌ Do NOT use external issue trackers
|
||||
- ❌ Do NOT duplicate tracking systems
|
||||
|
||||
For more details, run ` + "`bd --help`" + ` or see the project's AGENTS.md file.
|
||||
`
|
||||
|
||||
// junieMCPConfig generates the MCP configuration for Junie
|
||||
func junieMCPConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"mcpServers": map[string]interface{}{
|
||||
"beads": map[string]interface{}{
|
||||
"command": "bd",
|
||||
"args": []string{"mcp"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InstallJunie installs Junie integration
|
||||
func InstallJunie() {
|
||||
guidelinesPath := ".junie/guidelines.md"
|
||||
mcpPath := ".junie/mcp/mcp.json"
|
||||
|
||||
fmt.Println("Installing Junie integration...")
|
||||
|
||||
// Ensure .junie directory exists
|
||||
if err := EnsureDir(".junie", 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ensure .junie/mcp directory exists
|
||||
if err := EnsureDir(".junie/mcp", 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write guidelines file
|
||||
if err := atomicWriteFile(guidelinesPath, []byte(junieGuidelinesTemplate)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write guidelines: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Write MCP config file
|
||||
mcpConfig := junieMCPConfig()
|
||||
mcpData, err := json.MarshalIndent(mcpConfig, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: marshal MCP config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := atomicWriteFile(mcpPath, mcpData); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: write MCP config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Junie integration installed\n")
|
||||
fmt.Printf(" Guidelines: %s (agent instructions)\n", guidelinesPath)
|
||||
fmt.Printf(" MCP Config: %s (MCP server configuration)\n", mcpPath)
|
||||
fmt.Println("\nJunie will automatically read these files on session start.")
|
||||
fmt.Println("The MCP server provides direct access to beads tools.")
|
||||
}
|
||||
|
||||
// CheckJunie checks if Junie integration is installed
|
||||
func CheckJunie() {
|
||||
guidelinesPath := ".junie/guidelines.md"
|
||||
mcpPath := ".junie/mcp/mcp.json"
|
||||
|
||||
guidelinesExists := false
|
||||
mcpExists := false
|
||||
|
||||
if _, err := os.Stat(guidelinesPath); err == nil {
|
||||
guidelinesExists = true
|
||||
}
|
||||
if _, err := os.Stat(mcpPath); err == nil {
|
||||
mcpExists = true
|
||||
}
|
||||
|
||||
if guidelinesExists && mcpExists {
|
||||
fmt.Println("✓ Junie integration installed")
|
||||
fmt.Printf(" Guidelines: %s\n", guidelinesPath)
|
||||
fmt.Printf(" MCP Config: %s\n", mcpPath)
|
||||
return
|
||||
}
|
||||
|
||||
if guidelinesExists {
|
||||
fmt.Println("⚠ Partial Junie integration (guidelines only)")
|
||||
fmt.Printf(" Guidelines: %s\n", guidelinesPath)
|
||||
fmt.Println(" Missing: MCP config")
|
||||
fmt.Println(" Run: bd setup junie (to complete installation)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if mcpExists {
|
||||
fmt.Println("⚠ Partial Junie integration (MCP only)")
|
||||
fmt.Printf(" MCP Config: %s\n", mcpPath)
|
||||
fmt.Println(" Missing: Guidelines")
|
||||
fmt.Println(" Run: bd setup junie (to complete installation)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✗ Junie integration not installed")
|
||||
fmt.Println(" Run: bd setup junie")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// RemoveJunie removes Junie integration
|
||||
func RemoveJunie() {
|
||||
guidelinesPath := ".junie/guidelines.md"
|
||||
mcpPath := ".junie/mcp/mcp.json"
|
||||
mcpDir := ".junie/mcp"
|
||||
junieDir := ".junie"
|
||||
|
||||
fmt.Println("Removing Junie integration...")
|
||||
|
||||
removed := false
|
||||
|
||||
// Remove guidelines
|
||||
if err := os.Remove(guidelinesPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to remove guidelines: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
|
||||
// Remove MCP config
|
||||
if err := os.Remove(mcpPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to remove MCP config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
removed = true
|
||||
}
|
||||
|
||||
// Try to remove .junie/mcp directory if empty
|
||||
_ = os.Remove(mcpDir)
|
||||
|
||||
// Try to remove .junie directory if empty
|
||||
_ = os.Remove(junieDir)
|
||||
|
||||
if !removed {
|
||||
fmt.Println("No Junie integration files found")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("✓ Removed Junie integration")
|
||||
}
|
||||
501
cmd/bd/setup/junie_test.go
Normal file
501
cmd/bd/setup/junie_test.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJunieGuidelinesTemplate(t *testing.T) {
|
||||
requiredContent := []string{
|
||||
"bd ready",
|
||||
"bd create",
|
||||
"bd update",
|
||||
"bd close",
|
||||
"bd sync",
|
||||
"mcp_beads_ready",
|
||||
"mcp_beads_list",
|
||||
"mcp_beads_create",
|
||||
"bug",
|
||||
"feature",
|
||||
"task",
|
||||
"epic",
|
||||
}
|
||||
|
||||
for _, req := range requiredContent {
|
||||
if !strings.Contains(junieGuidelinesTemplate, req) {
|
||||
t.Errorf("junieGuidelinesTemplate missing required content: %q", req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJunieMCPConfig(t *testing.T) {
|
||||
config := junieMCPConfig()
|
||||
|
||||
// Verify structure
|
||||
mcpServers, ok := config["mcpServers"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("mcpServers key missing or wrong type")
|
||||
}
|
||||
|
||||
beads, ok := mcpServers["beads"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("beads server config missing or wrong type")
|
||||
}
|
||||
|
||||
command, ok := beads["command"].(string)
|
||||
if !ok || command != "bd" {
|
||||
t.Errorf("Expected command 'bd', got %v", beads["command"])
|
||||
}
|
||||
|
||||
args, ok := beads["args"].([]string)
|
||||
if !ok || len(args) != 1 || args[0] != "mcp" {
|
||||
t.Errorf("Expected args ['mcp'], got %v", beads["args"])
|
||||
}
|
||||
|
||||
// Verify it's valid JSON
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
t.Errorf("MCP config should be valid JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify it can be unmarshaled back
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Errorf("MCP config JSON should be parseable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallJunie(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
InstallJunie()
|
||||
|
||||
// Verify guidelines file was created
|
||||
guidelinesPath := ".junie/guidelines.md"
|
||||
if !FileExists(guidelinesPath) {
|
||||
t.Errorf("File was not created: %s", guidelinesPath)
|
||||
} else {
|
||||
data, err := os.ReadFile(guidelinesPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read %s: %v", guidelinesPath, err)
|
||||
} else if string(data) != junieGuidelinesTemplate {
|
||||
t.Errorf("File %s content doesn't match expected template", guidelinesPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify MCP config file was created
|
||||
mcpPath := ".junie/mcp/mcp.json"
|
||||
if !FileExists(mcpPath) {
|
||||
t.Errorf("File was not created: %s", mcpPath)
|
||||
} else {
|
||||
data, err := os.ReadFile(mcpPath)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to read %s: %v", mcpPath, err)
|
||||
} else {
|
||||
// Verify it's valid JSON
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Errorf("MCP config should be valid JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify structure
|
||||
mcpServers, ok := parsed["mcpServers"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("mcpServers key missing or wrong type")
|
||||
} else if _, ok := mcpServers["beads"]; !ok {
|
||||
t.Error("beads server config missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallJunie_ExistingDirectory(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Pre-create the directories
|
||||
if err := os.MkdirAll(".junie/mcp", 0755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
|
||||
// Should not fail
|
||||
InstallJunie()
|
||||
|
||||
// Verify files were created
|
||||
if !FileExists(".junie/guidelines.md") {
|
||||
t.Error("guidelines.md not created")
|
||||
}
|
||||
if !FileExists(".junie/mcp/mcp.json") {
|
||||
t.Error("mcp.json not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallJunieIdempotent(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Run twice
|
||||
InstallJunie()
|
||||
firstGuidelines, _ := os.ReadFile(".junie/guidelines.md")
|
||||
firstMCP, _ := os.ReadFile(".junie/mcp/mcp.json")
|
||||
|
||||
InstallJunie()
|
||||
secondGuidelines, _ := os.ReadFile(".junie/guidelines.md")
|
||||
secondMCP, _ := os.ReadFile(".junie/mcp/mcp.json")
|
||||
|
||||
if string(firstGuidelines) != string(secondGuidelines) {
|
||||
t.Error("InstallJunie should be idempotent for guidelines")
|
||||
}
|
||||
if string(firstMCP) != string(secondMCP) {
|
||||
t.Error("InstallJunie should be idempotent for MCP config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveJunie(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Install first
|
||||
InstallJunie()
|
||||
|
||||
// Verify files exist
|
||||
files := []string{".junie/guidelines.md", ".junie/mcp/mcp.json"}
|
||||
for _, f := range files {
|
||||
if !FileExists(f) {
|
||||
t.Fatalf("File should exist before removal: %s", f)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove
|
||||
RemoveJunie()
|
||||
|
||||
// Verify files are gone
|
||||
for _, f := range files {
|
||||
if FileExists(f) {
|
||||
t.Errorf("File should have been removed: %s", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveJunie_NoFiles(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Should not panic when files don't exist
|
||||
RemoveJunie()
|
||||
}
|
||||
|
||||
func TestRemoveJunie_PartialFiles(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create only the guidelines file
|
||||
if err := os.MkdirAll(".junie", 0755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(".junie/guidelines.md", []byte(junieGuidelinesTemplate), 0644); err != nil {
|
||||
t.Fatalf("failed to create guidelines file: %v", err)
|
||||
}
|
||||
|
||||
// Should not panic
|
||||
RemoveJunie()
|
||||
|
||||
// Guidelines should be removed
|
||||
if FileExists(".junie/guidelines.md") {
|
||||
t.Error("Guidelines file should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveJunie_DirectoryCleanup(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Install
|
||||
InstallJunie()
|
||||
|
||||
// Remove
|
||||
RemoveJunie()
|
||||
|
||||
// Directories should be cleaned up if empty
|
||||
if DirExists(".junie/mcp") {
|
||||
t.Error(".junie/mcp directory should be removed when empty")
|
||||
}
|
||||
if DirExists(".junie") {
|
||||
t.Error(".junie directory should be removed when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveJunie_DirectoryWithOtherFiles(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Install
|
||||
InstallJunie()
|
||||
|
||||
// Add another file to .junie directory
|
||||
if err := os.WriteFile(".junie/other.txt", []byte("keep me"), 0644); err != nil {
|
||||
t.Fatalf("failed to create other file: %v", err)
|
||||
}
|
||||
|
||||
// Remove
|
||||
RemoveJunie()
|
||||
|
||||
// Directory should still exist (has other files)
|
||||
if !DirExists(".junie") {
|
||||
t.Error("Directory should not be removed when it has other files")
|
||||
}
|
||||
|
||||
// Other file should still exist
|
||||
if !FileExists(".junie/other.txt") {
|
||||
t.Error("Other files should be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckJunie_NotInstalled(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// CheckJunie calls os.Exit(1) when not installed
|
||||
// We can't easily test that, but we document expected behavior
|
||||
}
|
||||
|
||||
func TestCheckJunie_Installed(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Install first
|
||||
InstallJunie()
|
||||
|
||||
// Should not panic or exit
|
||||
CheckJunie()
|
||||
}
|
||||
|
||||
func TestCheckJunie_PartialInstall_GuidelinesOnly(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create only guidelines
|
||||
if err := os.MkdirAll(".junie", 0755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(".junie/guidelines.md", []byte(junieGuidelinesTemplate), 0644); err != nil {
|
||||
t.Fatalf("failed to create guidelines file: %v", err)
|
||||
}
|
||||
|
||||
// CheckJunie calls os.Exit(1) for partial installation
|
||||
// We can't easily test that, but we document expected behavior
|
||||
}
|
||||
|
||||
func TestCheckJunie_PartialInstall_MCPOnly(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create only MCP config
|
||||
if err := os.MkdirAll(".junie/mcp", 0755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
mcpConfig := junieMCPConfig()
|
||||
mcpData, _ := json.MarshalIndent(mcpConfig, "", " ")
|
||||
if err := os.WriteFile(".junie/mcp/mcp.json", mcpData, 0644); err != nil {
|
||||
t.Fatalf("failed to create MCP config file: %v", err)
|
||||
}
|
||||
|
||||
// CheckJunie calls os.Exit(1) for partial installation
|
||||
// We can't easily test that, but we document expected behavior
|
||||
}
|
||||
|
||||
func TestJunieFilePaths(t *testing.T) {
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change to temp directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Fatalf("failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
InstallJunie()
|
||||
|
||||
// Check expected file paths
|
||||
expectedPaths := []string{
|
||||
".junie/guidelines.md",
|
||||
".junie/mcp/mcp.json",
|
||||
}
|
||||
|
||||
for _, path := range expectedPaths {
|
||||
if !FileExists(path) {
|
||||
t.Errorf("Expected file at %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJunieGuidelinesWorkflowPattern(t *testing.T) {
|
||||
// Verify guidelines contain the workflow patterns Junie users need
|
||||
guidelines := junieGuidelinesTemplate
|
||||
|
||||
// Should mention core workflow commands
|
||||
if !strings.Contains(guidelines, "bd ready") {
|
||||
t.Error("Should mention bd ready")
|
||||
}
|
||||
if !strings.Contains(guidelines, "bd sync") {
|
||||
t.Error("Should mention bd sync")
|
||||
}
|
||||
|
||||
// Should explain MCP tools
|
||||
if !strings.Contains(guidelines, "MCP Tools Available") {
|
||||
t.Error("Should have MCP Tools section")
|
||||
}
|
||||
}
|
||||
89
integrations/junie/README.md
Normal file
89
integrations/junie/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Junie Integration for Beads
|
||||
|
||||
Integration for [Junie](https://www.jetbrains.com/junie/) (JetBrains AI Agent) with beads issue tracking.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# Install beads
|
||||
curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash
|
||||
|
||||
# Initialize beads in your project
|
||||
bd init
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bd setup junie
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `.junie/guidelines.md` - Agent instructions for beads workflow
|
||||
- `.junie/mcp/mcp.json` - MCP server configuration
|
||||
|
||||
## What Gets Installed
|
||||
|
||||
### Guidelines (`.junie/guidelines.md`)
|
||||
|
||||
Junie automatically reads this file on session start. It contains:
|
||||
- Core workflow rules for using beads
|
||||
- Command reference for the `bd` CLI
|
||||
- Issue types and priorities
|
||||
- MCP tool documentation
|
||||
|
||||
### MCP Config (`.junie/mcp/mcp.json`)
|
||||
|
||||
Configures the beads MCP server so Junie can use beads tools directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"beads": {
|
||||
"command": "bd",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, Junie will:
|
||||
1. Read workflow instructions from `.junie/guidelines.md`
|
||||
2. Have access to beads MCP tools for direct issue management
|
||||
3. Be able to use `bd` CLI commands
|
||||
|
||||
### MCP Tools Available
|
||||
|
||||
- `mcp_beads_ready` - Find tasks ready for work
|
||||
- `mcp_beads_list` - List issues with filters
|
||||
- `mcp_beads_show` - Show issue details
|
||||
- `mcp_beads_create` - Create new issues
|
||||
- `mcp_beads_update` - Update issue status/priority
|
||||
- `mcp_beads_close` - Close completed issues
|
||||
- `mcp_beads_dep` - Manage dependencies
|
||||
- `mcp_beads_blocked` - Show blocked issues
|
||||
- `mcp_beads_stats` - Get issue statistics
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
bd setup junie --check
|
||||
```
|
||||
|
||||
## Removal
|
||||
|
||||
```bash
|
||||
bd setup junie --remove
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `bd prime` - Get full workflow context
|
||||
- `bd ready` - Find unblocked work
|
||||
- `bd sync` - Sync changes to git (run at session end)
|
||||
|
||||
## License
|
||||
|
||||
Same as beads (see repository root).
|
||||
@@ -90,6 +90,12 @@ var BuiltinRecipes = map[string]Recipe{
|
||||
Description: "Aider config and instruction files",
|
||||
Paths: []string{".aider.conf.yml", ".aider/BEADS.md", ".aider/README.md"},
|
||||
},
|
||||
"junie": {
|
||||
Name: "Junie",
|
||||
Type: TypeMultiFile,
|
||||
Description: "Junie guidelines and MCP configuration",
|
||||
Paths: []string{".junie/guidelines.md", ".junie/mcp/mcp.json"},
|
||||
},
|
||||
}
|
||||
|
||||
// UserRecipes holds recipes loaded from user config file.
|
||||
|
||||
220
website/docs/integrations/junie.md
Normal file
220
website/docs/integrations/junie.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
id: junie
|
||||
title: Junie
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Junie Integration
|
||||
|
||||
How to use beads with Junie (JetBrains AI Agent).
|
||||
|
||||
## Setup
|
||||
|
||||
### Quick Setup
|
||||
|
||||
```bash
|
||||
bd setup junie
|
||||
```
|
||||
|
||||
This creates:
|
||||
- **`.junie/guidelines.md`** - Agent instructions for beads workflow
|
||||
- **`.junie/mcp/mcp.json`** - MCP server configuration
|
||||
|
||||
### Verify Setup
|
||||
|
||||
```bash
|
||||
bd setup junie --check
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Session starts** → Junie reads `.junie/guidelines.md` for workflow context
|
||||
2. **MCP tools available** → Junie can use beads MCP tools directly
|
||||
3. **You work** → Use `bd` CLI commands or MCP tools
|
||||
4. **Session ends** → Run `bd sync` to save work to git
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Guidelines (`.junie/guidelines.md`)
|
||||
|
||||
Contains workflow instructions that Junie reads automatically:
|
||||
- Core workflow rules
|
||||
- Command reference
|
||||
- Issue types and priorities
|
||||
- MCP tool documentation
|
||||
|
||||
### MCP Config (`.junie/mcp/mcp.json`)
|
||||
|
||||
Configures the beads MCP server:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"beads": {
|
||||
"command": "bd",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Tools
|
||||
|
||||
With MCP configured, Junie can use these tools directly:
|
||||
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `mcp_beads_ready` | Find tasks ready for work |
|
||||
| `mcp_beads_list` | List issues with filters |
|
||||
| `mcp_beads_show` | Show issue details |
|
||||
| `mcp_beads_create` | Create new issues |
|
||||
| `mcp_beads_update` | Update issue status/priority |
|
||||
| `mcp_beads_close` | Close completed issues |
|
||||
| `mcp_beads_dep` | Manage dependencies |
|
||||
| `mcp_beads_blocked` | Show blocked issues |
|
||||
| `mcp_beads_stats` | Get issue statistics |
|
||||
|
||||
## CLI Commands
|
||||
|
||||
You can also use the `bd` CLI directly:
|
||||
|
||||
### Creating Issues
|
||||
|
||||
```bash
|
||||
# Always include description for context
|
||||
bd create "Fix authentication bug" \
|
||||
--description="Login fails with special characters in password" \
|
||||
-t bug -p 1 --json
|
||||
|
||||
# Link discovered issues
|
||||
bd create "Found SQL injection" \
|
||||
--description="User input not sanitized in query builder" \
|
||||
--deps discovered-from:bd-42 --json
|
||||
```
|
||||
|
||||
### Working on Issues
|
||||
|
||||
```bash
|
||||
# Find ready work
|
||||
bd ready --json
|
||||
|
||||
# Start work
|
||||
bd update bd-42 --status in_progress --json
|
||||
|
||||
# Complete work
|
||||
bd close bd-42 --reason "Fixed in commit abc123" --json
|
||||
```
|
||||
|
||||
### Querying
|
||||
|
||||
```bash
|
||||
# List open issues
|
||||
bd list --status open --json
|
||||
|
||||
# Show issue details
|
||||
bd show bd-42 --json
|
||||
|
||||
# Check blocked issues
|
||||
bd blocked --json
|
||||
```
|
||||
|
||||
### Syncing
|
||||
|
||||
```bash
|
||||
# ALWAYS run at session end
|
||||
bd sync
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Always Use `--json`
|
||||
|
||||
```bash
|
||||
bd list --json # Parse programmatically
|
||||
bd create "Task" --json # Get issue ID from output
|
||||
bd show bd-42 --json # Structured data
|
||||
```
|
||||
|
||||
### Always Include Descriptions
|
||||
|
||||
```bash
|
||||
# Good
|
||||
bd create "Fix auth bug" \
|
||||
--description="Login fails when password contains quotes" \
|
||||
-t bug -p 1 --json
|
||||
|
||||
# Bad - no context for future work
|
||||
bd create "Fix auth bug" -t bug -p 1 --json
|
||||
```
|
||||
|
||||
### Link Related Work
|
||||
|
||||
```bash
|
||||
# When you discover issues during work
|
||||
bd create "Found related bug" \
|
||||
--deps discovered-from:bd-current --json
|
||||
```
|
||||
|
||||
### Sync Before Session End
|
||||
|
||||
```bash
|
||||
# ALWAYS run before ending
|
||||
bd sync
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Guidelines not loaded
|
||||
|
||||
```bash
|
||||
# Check setup
|
||||
bd setup junie --check
|
||||
|
||||
# Reinstall if needed
|
||||
bd setup junie
|
||||
```
|
||||
|
||||
### MCP tools not available
|
||||
|
||||
```bash
|
||||
# Verify MCP config exists
|
||||
cat .junie/mcp/mcp.json
|
||||
|
||||
# Test MCP server
|
||||
bd mcp --help
|
||||
```
|
||||
|
||||
### Changes not syncing
|
||||
|
||||
```bash
|
||||
# Force sync
|
||||
bd sync
|
||||
|
||||
# Check daemon
|
||||
bd info
|
||||
bd daemons health
|
||||
```
|
||||
|
||||
### Database not found
|
||||
|
||||
```bash
|
||||
# Initialize beads
|
||||
bd init --quiet
|
||||
```
|
||||
|
||||
## Removing Integration
|
||||
|
||||
```bash
|
||||
bd setup junie --remove
|
||||
```
|
||||
|
||||
This removes:
|
||||
- `.junie/guidelines.md`
|
||||
- `.junie/mcp/mcp.json`
|
||||
- Empty `.junie/mcp/` and `.junie/` directories
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP Server](/integrations/mcp-server) - MCP server details
|
||||
- [Claude Code](/integrations/claude-code) - Similar hook-based integration
|
||||
- [IDE Setup](/getting-started/ide-setup) - Other editors
|
||||
@@ -91,6 +91,7 @@ const sidebars: SidebarsConfig = {
|
||||
'integrations/claude-code',
|
||||
'integrations/mcp-server',
|
||||
'integrations/aider',
|
||||
'integrations/junie',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user