Better enable go extensions (#14)
* deps: run go mod tidy * beads: Add public Go API for bd extensions Implements a minimal public API to enable Go-based extensions without exposing internal packages: **New beads.go package:** - Exports essential types: Issue, Status, IssueType, WorkFilter - Provides status and issue type constants - Exposes NewSQLiteStorage() as main entry point for extensions - Includes comprehensive package documentation **Updated EXTENDING.md:** - Replaced internal package imports with public beads package - Updated function calls to use new public API - Changed sqlite.New() to beads.NewSQLiteStorage() - Updated GetReady() to GetReadyWork() with WorkFilter This enables clean Go-based orchestration extensions while maintaining API stability and hiding internal implementation details. * beads: Refine Go extensions API and documentation Updates to the public Go API implementation following initial commit: - Enhanced beads.go with refined extension interface - Updated EXTENDING.md with clearer documentation - Modified cmd/bd/main.go to support extension loading Continues work on enabling Go-based bd extensions. * Fix EXTENDING.md to use beads.WorkFilter instead of types.WorkFilter The public API exports WorkFilter as beads.WorkFilter, not types.WorkFilter. This fixes the code example to match the imports shown. --------- Co-authored-by: Steve Yegge <steve.yegge@gmail.com>
This commit is contained in:
20
EXTENDING.md
20
EXTENDING.md
@@ -99,12 +99,11 @@ func InitializeMyAppSchema(dbPath string) error {
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
// Open bd's storage
|
||||
store, err := sqlite.New(dbPath)
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -115,7 +114,7 @@ if err := InitializeMyAppSchema(dbPath); err != nil {
|
||||
}
|
||||
|
||||
// Use bd to find ready work
|
||||
readyIssues, err := store.GetReady(ctx, types.IssueFilter{Limit: 10})
|
||||
readyIssues, err := store.GetReadyWork(ctx, beads.WorkFilter{Limit: 10})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -410,10 +409,17 @@ You can always access bd's database directly:
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
// Auto-discover bd's database path
|
||||
dbPath := beads.FindDatabasePath()
|
||||
if dbPath == "" {
|
||||
log.Fatal("No bd database found. Run 'bd init' first.")
|
||||
}
|
||||
|
||||
// Open the same database bd uses
|
||||
db, err := sql.Open("sqlite3", ".beads/myapp.db")
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -429,6 +435,10 @@ err = db.QueryRow(`
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO myapp_executions (issue_id, status) VALUES (?, ?)
|
||||
`, issueID, "running")
|
||||
|
||||
// Find corresponding JSONL path (for git hooks, monitoring, etc.)
|
||||
jsonlPath := beads.FindJSONLPath(dbPath)
|
||||
fmt.Printf("BD exports to: %s\n", jsonlPath)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
137
beads.go
Normal file
137
beads.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Package beads provides a minimal public API for extending bd with custom orchestration.
|
||||
//
|
||||
// Most extensions should use direct SQL queries against bd's database.
|
||||
// This package exports only the essential types and functions needed for
|
||||
// Go-based extensions that want to use bd's storage layer programmatically.
|
||||
//
|
||||
// For detailed guidance on extending bd, see EXTENDING.md.
|
||||
package beads
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// Core types for working with issues
|
||||
type (
|
||||
Issue = types.Issue
|
||||
Status = types.Status
|
||||
IssueType = types.IssueType
|
||||
WorkFilter = types.WorkFilter
|
||||
)
|
||||
|
||||
// Status constants
|
||||
const (
|
||||
StatusOpen = types.StatusOpen
|
||||
StatusInProgress = types.StatusInProgress
|
||||
StatusClosed = types.StatusClosed
|
||||
StatusBlocked = types.StatusBlocked
|
||||
)
|
||||
|
||||
// IssueType constants
|
||||
const (
|
||||
TypeBug = types.TypeBug
|
||||
TypeFeature = types.TypeFeature
|
||||
TypeTask = types.TypeTask
|
||||
TypeEpic = types.TypeEpic
|
||||
TypeChore = types.TypeChore
|
||||
)
|
||||
|
||||
// Storage provides the minimal interface for extension orchestration
|
||||
type Storage = storage.Storage
|
||||
|
||||
// NewSQLiteStorage opens a bd SQLite database for programmatic access.
|
||||
// Most extensions should use this to query ready work and update issue status.
|
||||
func NewSQLiteStorage(dbPath string) (Storage, error) {
|
||||
return sqlite.New(dbPath)
|
||||
}
|
||||
|
||||
// FindDatabasePath discovers the bd database path using bd's standard search order:
|
||||
// 1. $BEADS_DB environment variable
|
||||
// 2. .beads/*.db in current directory or ancestors
|
||||
// 3. ~/.beads/default.db (fallback)
|
||||
//
|
||||
// Returns empty string if no database is found at (1) or (2) and (3) doesn't exist.
|
||||
func FindDatabasePath() string {
|
||||
// 1. Check environment variable
|
||||
if envDB := os.Getenv("BEADS_DB"); envDB != "" {
|
||||
return envDB
|
||||
}
|
||||
|
||||
// 2. Search for .beads/*.db in current directory and ancestors
|
||||
if foundDB := findDatabaseInTree(); foundDB != "" {
|
||||
return foundDB
|
||||
}
|
||||
|
||||
// 3. Try home directory default
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
defaultDB := filepath.Join(home, ".beads", "default.db")
|
||||
// Only return if it exists
|
||||
if _, err := os.Stat(defaultDB); err == nil {
|
||||
return defaultDB
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// FindJSONLPath returns the expected JSONL file path for the given database path.
|
||||
// It searches for existing *.jsonl files in the database directory and returns
|
||||
// the first one found, or defaults to "issues.jsonl".
|
||||
//
|
||||
// This function does not create directories or files - it only discovers paths.
|
||||
// Use this when you need to know where bd stores its JSONL export.
|
||||
func FindJSONLPath(dbPath string) string {
|
||||
if dbPath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get the directory containing the database
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
|
||||
// Look for existing .jsonl files in the .beads directory
|
||||
pattern := filepath.Join(dbDir, "*.jsonl")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil && len(matches) > 0 {
|
||||
// Return the first .jsonl file found
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
// Default to issues.jsonl
|
||||
return filepath.Join(dbDir, "issues.jsonl")
|
||||
}
|
||||
|
||||
// findDatabaseInTree walks up the directory tree looking for .beads/*.db
|
||||
func findDatabaseInTree() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Walk up directory tree
|
||||
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 ""
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
@@ -58,15 +59,11 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Initialize storage
|
||||
if dbPath == "" {
|
||||
// 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 != "" {
|
||||
// Use public API to find database (same logic as extensions)
|
||||
if foundDB := beads.FindDatabasePath(); foundDB != "" {
|
||||
dbPath = foundDB
|
||||
} else {
|
||||
// Fallback to default location (will be created by init command)
|
||||
home, _ := os.UserHomeDir()
|
||||
dbPath = filepath.Join(home, ".beads", "default.db")
|
||||
}
|
||||
@@ -128,37 +125,6 @@ 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)
|
||||
@@ -171,26 +137,19 @@ func outputJSON(v interface{}) {
|
||||
|
||||
// findJSONLPath finds the JSONL file path for the current database
|
||||
func findJSONLPath() string {
|
||||
// Get the directory containing the database
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
// Use public API for path discovery
|
||||
jsonlPath := beads.FindJSONLPath(dbPath)
|
||||
|
||||
// Ensure the directory exists (important for new databases)
|
||||
// This is the only difference from the public API - we create the directory
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||
// If we can't create the directory, return default path anyway
|
||||
// If we can't create the directory, return discovered path anyway
|
||||
// (the subsequent write will fail with a clearer error)
|
||||
return filepath.Join(dbDir, "issues.jsonl")
|
||||
return jsonlPath
|
||||
}
|
||||
|
||||
// Look for existing .jsonl files in the .beads directory
|
||||
pattern := filepath.Join(dbDir, "*.jsonl")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil && len(matches) > 0 {
|
||||
// Return the first .jsonl file found
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
// Default to issues.jsonl
|
||||
return filepath.Join(dbDir, "issues.jsonl")
|
||||
return jsonlPath
|
||||
}
|
||||
|
||||
// autoImportIfNewer checks if JSONL is newer than DB and imports if so
|
||||
|
||||
20
go.mod
20
go.mod
@@ -3,23 +3,15 @@ module github.com/steveyegge/beads
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/spf13/cobra v1.10.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/cobra v1.10.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
22
go.sum
22
go.sum
@@ -1,10 +1,6 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -14,33 +10,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
Reference in New Issue
Block a user