Remove deprecated bd repos command
Amp-Thread-ID: https://ampcode.com/threads/T-3fe46a3f-979f-48dd-9bb3-ee0b9fde46c2 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -278,25 +278,7 @@ func (c *Client) Batch(args *BatchArgs) (*Response, error) {
|
||||
return c.Execute(OpBatch, args)
|
||||
}
|
||||
|
||||
// ReposList lists all cached repositories
|
||||
func (c *Client) ReposList() (*Response, error) {
|
||||
return c.Execute(OpReposList, struct{}{})
|
||||
}
|
||||
|
||||
// ReposReady gets ready work across all repositories
|
||||
func (c *Client) ReposReady(args *ReposReadyArgs) (*Response, error) {
|
||||
return c.Execute(OpReposReady, args)
|
||||
}
|
||||
|
||||
// ReposStats gets combined statistics across all repositories
|
||||
func (c *Client) ReposStats() (*Response, error) {
|
||||
return c.Execute(OpReposStats, struct{}{})
|
||||
}
|
||||
|
||||
// ReposClearCache clears the repository cache
|
||||
func (c *Client) ReposClearCache() (*Response, error) {
|
||||
return c.Execute(OpReposClearCache, struct{}{})
|
||||
}
|
||||
|
||||
// Export exports the database to JSONL format
|
||||
func (c *Client) Export(args *ExportArgs) (*Response, error) {
|
||||
|
||||
@@ -217,115 +217,6 @@ func TestBatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReposList(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
defer client.Close()
|
||||
|
||||
resp, err := client.ReposList()
|
||||
if err != nil {
|
||||
t.Fatalf("ReposList failed: %v", err)
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Errorf("ReposList failed: %s", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReposReady(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
defer client.Close()
|
||||
|
||||
args := &ReposReadyArgs{}
|
||||
resp, err := client.ReposReady(args)
|
||||
if err != nil {
|
||||
t.Fatalf("ReposReady failed: %v", err)
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Errorf("ReposReady failed: %s", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReposStats(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
defer client.Close()
|
||||
|
||||
// Create a test issue to populate stats
|
||||
createArgs := &CreateArgs{
|
||||
Title: "Test Issue for Stats",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
_, err := client.Create(createArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
resp, err := client.ReposStats()
|
||||
if err != nil {
|
||||
t.Fatalf("ReposStats failed: %v", err)
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Errorf("ReposStats failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
// Verify response structure
|
||||
var statsResp ReposStatsResponse
|
||||
if err := json.Unmarshal(resp.Data, &statsResp); err != nil {
|
||||
t.Fatalf("Failed to unmarshal stats response: %v", err)
|
||||
}
|
||||
|
||||
if statsResp.Total.TotalIssues == 0 {
|
||||
t.Error("Expected TotalIssues > 0 in aggregated stats")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReposClearCache(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
defer client.Close()
|
||||
|
||||
// Create an issue to populate cache
|
||||
createArgs := &CreateArgs{
|
||||
Title: "Test Issue",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
_, err := client.Create(createArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
// Clear the cache
|
||||
resp, err := client.ReposClearCache()
|
||||
if err != nil {
|
||||
t.Fatalf("ReposClearCache failed: %v", err)
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Errorf("ReposClearCache failed: %s", resp.Error)
|
||||
}
|
||||
|
||||
// Verify we can still operate after cache clear (cache should rebuild)
|
||||
createArgs2 := &CreateArgs{
|
||||
Title: "Post-Clear Issue",
|
||||
IssueType: "task",
|
||||
Priority: 2,
|
||||
}
|
||||
createResp, err := client.Create(createArgs2)
|
||||
if err != nil {
|
||||
t.Fatalf("Create after cache clear failed: %v", err)
|
||||
}
|
||||
|
||||
if !createResp.Success {
|
||||
t.Errorf("Expected create to succeed after cache clear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEpicStatus(t *testing.T) {
|
||||
_, client, cleanup := setupTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -2,8 +2,6 @@ package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// Operation constants for all bd commands
|
||||
@@ -26,10 +24,7 @@ const (
|
||||
OpCommentList = "comment_list"
|
||||
OpCommentAdd = "comment_add"
|
||||
OpBatch = "batch"
|
||||
OpReposList = "repos_list"
|
||||
OpReposReady = "repos_ready"
|
||||
OpReposStats = "repos_stats"
|
||||
OpReposClearCache = "repos_clear_cache"
|
||||
|
||||
OpCompact = "compact"
|
||||
OpCompactStats = "compact_stats"
|
||||
OpExport = "export"
|
||||
@@ -209,41 +204,6 @@ type BatchResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ReposReadyArgs represents arguments for repos ready operation
|
||||
type ReposReadyArgs struct {
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
GroupByRepo bool `json:"group_by_repo,omitempty"`
|
||||
}
|
||||
|
||||
// RepoInfo represents information about a cached repository
|
||||
type RepoInfo struct {
|
||||
Path string `json:"path"`
|
||||
Prefix string `json:"prefix"`
|
||||
IssueCount int `json:"issue_count"`
|
||||
LastAccess string `json:"last_access"`
|
||||
}
|
||||
|
||||
// RepoReadyWork represents ready work for a single repository
|
||||
type RepoReadyWork struct {
|
||||
RepoPath string `json:"repo_path"`
|
||||
Issues []*types.Issue `json:"issues"`
|
||||
}
|
||||
|
||||
// ReposReadyIssue represents an issue with repo context
|
||||
type ReposReadyIssue struct {
|
||||
RepoPath string `json:"repo_path"`
|
||||
Issue *types.Issue `json:"issue"`
|
||||
}
|
||||
|
||||
// ReposStatsResponse contains combined statistics across repos
|
||||
type ReposStatsResponse struct {
|
||||
Total types.Statistics `json:"total"`
|
||||
PerRepo map[string]types.Statistics `json:"per_repo"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// CompactArgs represents arguments for the compact operation
|
||||
type CompactArgs struct {
|
||||
IssueID string `json:"issue_id,omitempty"` // Empty for --all
|
||||
|
||||
@@ -650,14 +650,7 @@ func (s *Server) handleRequest(req *Request) Response {
|
||||
resp = s.handleCommentAdd(req)
|
||||
case OpBatch:
|
||||
resp = s.handleBatch(req)
|
||||
case OpReposList:
|
||||
resp = s.handleReposList(req)
|
||||
case OpReposReady:
|
||||
resp = s.handleReposReady(req)
|
||||
case OpReposStats:
|
||||
resp = s.handleReposStats(req)
|
||||
case OpReposClearCache:
|
||||
resp = s.handleReposClearCache(req)
|
||||
|
||||
case OpCompact:
|
||||
resp = s.handleCompact(req)
|
||||
case OpCompactStats:
|
||||
@@ -1560,206 +1553,6 @@ func (s *Server) writeResponse(writer *bufio.Writer, resp Response) {
|
||||
_ = writer.Flush()
|
||||
}
|
||||
|
||||
// Multi-repo handlers
|
||||
|
||||
func (s *Server) handleReposList(_ *Request) Response {
|
||||
// Keep read lock during iteration to prevent stores from being closed mid-query
|
||||
s.cacheMu.RLock()
|
||||
defer s.cacheMu.RUnlock()
|
||||
|
||||
repos := make([]RepoInfo, 0, len(s.storageCache))
|
||||
for path, entry := range s.storageCache {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
stats, err := entry.store.GetStatistics(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract prefix from a sample issue
|
||||
filter := types.IssueFilter{Limit: 1}
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
issues, err := entry.store.SearchIssues(ctx2, "", filter)
|
||||
cancel2()
|
||||
prefix := ""
|
||||
if err == nil && len(issues) > 0 && len(issues[0].ID) > 0 {
|
||||
// Extract prefix (everything before the last hyphen and number)
|
||||
id := issues[0].ID
|
||||
for i := len(id) - 1; i >= 0; i-- {
|
||||
if id[i] == '-' {
|
||||
prefix = id[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repos = append(repos, RepoInfo{
|
||||
Path: path,
|
||||
Prefix: prefix,
|
||||
IssueCount: stats.TotalIssues,
|
||||
LastAccess: entry.lastAccess.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(repos)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleReposReady(req *Request) Response {
|
||||
var args ReposReadyArgs
|
||||
if err := json.Unmarshal(req.Args, &args); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("invalid args: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// Keep read lock during iteration to prevent stores from being closed mid-query
|
||||
s.cacheMu.RLock()
|
||||
defer s.cacheMu.RUnlock()
|
||||
|
||||
if args.GroupByRepo {
|
||||
result := make([]RepoReadyWork, 0, len(s.storageCache))
|
||||
for path, entry := range s.storageCache {
|
||||
filter := types.WorkFilter{
|
||||
Status: types.StatusOpen,
|
||||
Limit: args.Limit,
|
||||
}
|
||||
if args.Priority != nil {
|
||||
filter.Priority = args.Priority
|
||||
}
|
||||
if args.Assignee != "" {
|
||||
filter.Assignee = &args.Assignee
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
issues, err := entry.store.GetReadyWork(ctx, filter)
|
||||
cancel()
|
||||
if err != nil || len(issues) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, RepoReadyWork{
|
||||
RepoPath: path,
|
||||
Issues: issues,
|
||||
})
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(result)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// Flat list of all ready issues across all repos
|
||||
allIssues := make([]ReposReadyIssue, 0)
|
||||
for path, entry := range s.storageCache {
|
||||
filter := types.WorkFilter{
|
||||
Status: types.StatusOpen,
|
||||
Limit: args.Limit,
|
||||
}
|
||||
if args.Priority != nil {
|
||||
filter.Priority = args.Priority
|
||||
}
|
||||
if args.Assignee != "" {
|
||||
filter.Assignee = &args.Assignee
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
issues, err := entry.store.GetReadyWork(ctx, filter)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
allIssues = append(allIssues, ReposReadyIssue{
|
||||
RepoPath: path,
|
||||
Issue: issue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(allIssues)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleReposStats(_ *Request) Response {
|
||||
// Keep read lock during iteration to prevent stores from being closed mid-query
|
||||
s.cacheMu.RLock()
|
||||
defer s.cacheMu.RUnlock()
|
||||
|
||||
total := types.Statistics{}
|
||||
perRepo := make(map[string]types.Statistics)
|
||||
errors := make(map[string]string)
|
||||
|
||||
for path, entry := range s.storageCache {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
stats, err := entry.store.GetStatistics(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
errors[path] = err.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
perRepo[path] = *stats
|
||||
|
||||
// Aggregate totals
|
||||
total.TotalIssues += stats.TotalIssues
|
||||
total.OpenIssues += stats.OpenIssues
|
||||
total.InProgressIssues += stats.InProgressIssues
|
||||
total.ClosedIssues += stats.ClosedIssues
|
||||
total.BlockedIssues += stats.BlockedIssues
|
||||
total.ReadyIssues += stats.ReadyIssues
|
||||
total.EpicsEligibleForClosure += stats.EpicsEligibleForClosure
|
||||
}
|
||||
|
||||
result := ReposStatsResponse{
|
||||
Total: total,
|
||||
PerRepo: perRepo,
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
result.Errors = errors
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(result)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleReposClearCache(_ *Request) Response {
|
||||
// Copy stores under write lock, clear cache, then close outside lock
|
||||
// to avoid holding lock during potentially slow Close() operations
|
||||
s.cacheMu.Lock()
|
||||
stores := make([]storage.Storage, 0, len(s.storageCache))
|
||||
for _, entry := range s.storageCache {
|
||||
stores = append(stores, entry.store)
|
||||
}
|
||||
s.storageCache = make(map[string]*StorageCacheEntry)
|
||||
s.cacheMu.Unlock()
|
||||
|
||||
// Close all storage connections without holding lock
|
||||
for _, store := range stores {
|
||||
if err := store.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to close storage: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: json.RawMessage(`{"message":"Cache cleared successfully"}`),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleCompact(req *Request) Response {
|
||||
var args CompactArgs
|
||||
if err := json.Unmarshal(req.Args, &args); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user