Fix template commands failing with daemon mode (bd-indn)

Added daemon-compatible functions for template operations:
- loadTemplateSubgraphViaDaemon: loads template subgraph via RPC calls
- cloneSubgraphViaDaemon: creates new issues from template via RPC
- Updated templateShowCmd and templateInstantiateCmd to use daemon path

The template commands were using storage.Storage directly even when
daemon was connected, causing "no database connection" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 22:40:25 -08:00
parent 9354dbd9f2
commit bbb08d6d8d

View File

@@ -170,7 +170,13 @@ var templateShowCmd = &cobra.Command{
}
// Load and show Beads template
subgraph, err := loadTemplateSubgraph(ctx, store, templateID)
var subgraph *TemplateSubgraph
var err error
if daemonClient != nil {
subgraph, err = loadTemplateSubgraphViaDaemon(daemonClient, templateID)
} else {
subgraph, err = loadTemplateSubgraph(ctx, store, templateID)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
os.Exit(1)
@@ -267,7 +273,13 @@ Example:
}
// Load the template subgraph
subgraph, err := loadTemplateSubgraph(ctx, store, templateID)
var subgraph *TemplateSubgraph
var err error
if daemonClient != nil {
subgraph, err = loadTemplateSubgraphViaDaemon(daemonClient, templateID)
} else {
subgraph, err = loadTemplateSubgraph(ctx, store, templateID)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
os.Exit(1)
@@ -314,7 +326,12 @@ Example:
Actor: actor,
Wisp: false,
}
result, err := cloneSubgraph(ctx, store, subgraph, opts)
var result *InstantiateResult
if daemonClient != nil {
result, err = cloneSubgraphViaDaemon(daemonClient, subgraph, opts)
} else {
result, err = cloneSubgraph(ctx, store, subgraph, opts)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
os.Exit(1)
@@ -436,6 +453,184 @@ func loadDescendants(ctx context.Context, s storage.Storage, subgraph *TemplateS
return nil
}
// =============================================================================
// Daemon-compatible Template Functions
// =============================================================================
// IssueDetailsFromShow represents the response structure from daemon Show RPC
type IssueDetailsFromShow struct {
types.Issue
Labels []string `json:"labels,omitempty"`
Dependencies []*types.IssueWithDependencyMetadata `json:"dependencies,omitempty"`
Dependents []*types.IssueWithDependencyMetadata `json:"dependents,omitempty"`
}
// loadTemplateSubgraphViaDaemon loads a template subgraph using daemon RPC calls
func loadTemplateSubgraphViaDaemon(client *rpc.Client, templateID string) (*TemplateSubgraph, error) {
// Get root issue with dependencies/dependents
resp, err := client.Show(&rpc.ShowArgs{ID: templateID})
if err != nil {
return nil, fmt.Errorf("failed to get template: %w", err)
}
var rootDetails IssueDetailsFromShow
if err := json.Unmarshal(resp.Data, &rootDetails); err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
root := &rootDetails.Issue
subgraph := &TemplateSubgraph{
Root: root,
Issues: []*types.Issue{root},
IssueMap: map[string]*types.Issue{root.ID: root},
}
// Find children from dependents (those with parent-child relationship)
// and recursively load them
if err := loadDescendantsViaDaemon(client, subgraph, rootDetails.Dependents); err != nil {
return nil, err
}
// Now build dependencies list by examining each issue's dependencies
// We need to get the dependency records, which Show provides
for _, issue := range subgraph.Issues {
resp, err := client.Show(&rpc.ShowArgs{ID: issue.ID})
if err != nil {
continue
}
var details IssueDetailsFromShow
if err := json.Unmarshal(resp.Data, &details); err != nil {
continue
}
// Dependencies are issues that THIS issue depends on
for _, dep := range details.Dependencies {
// Only include if the dependency target is also in the subgraph
if _, ok := subgraph.IssueMap[dep.Issue.ID]; ok {
subgraph.Dependencies = append(subgraph.Dependencies, &types.Dependency{
IssueID: issue.ID,
DependsOnID: dep.Issue.ID,
Type: dep.DependencyType,
})
}
}
}
return subgraph, nil
}
// loadDescendantsViaDaemon recursively loads child issues via daemon RPC
func loadDescendantsViaDaemon(client *rpc.Client, subgraph *TemplateSubgraph, dependents []*types.IssueWithDependencyMetadata) error {
for _, dep := range dependents {
// Check if this is a child (parent-child relationship)
if dep.DependencyType != types.DepParentChild {
continue
}
if _, exists := subgraph.IssueMap[dep.Issue.ID]; exists {
continue // Already in subgraph
}
// Add to subgraph
issue := &dep.Issue
subgraph.Issues = append(subgraph.Issues, issue)
subgraph.IssueMap[issue.ID] = issue
// Get this issue's dependents for recursion
resp, err := client.Show(&rpc.ShowArgs{ID: issue.ID})
if err != nil {
continue
}
var details IssueDetailsFromShow
if err := json.Unmarshal(resp.Data, &details); err != nil {
continue
}
// Recurse on children
if err := loadDescendantsViaDaemon(client, subgraph, details.Dependents); err != nil {
return err
}
}
return nil
}
// cloneSubgraphViaDaemon creates new issues from the template using daemon RPC calls
func cloneSubgraphViaDaemon(client *rpc.Client, subgraph *TemplateSubgraph, opts CloneOptions) (*InstantiateResult, error) {
// Generate new IDs and create mapping
idMapping := make(map[string]string)
// First pass: create all issues with new IDs
for _, oldIssue := range subgraph.Issues {
// Determine assignee: use override for root epic, otherwise keep template's
issueAssignee := oldIssue.Assignee
if oldIssue.ID == subgraph.Root.ID && opts.Assignee != "" {
issueAssignee = opts.Assignee
}
// Build create args
createArgs := &rpc.CreateArgs{
Title: substituteVariables(oldIssue.Title, opts.Vars),
Description: substituteVariables(oldIssue.Description, opts.Vars),
IssueType: string(oldIssue.IssueType),
Priority: oldIssue.Priority,
Design: substituteVariables(oldIssue.Design, opts.Vars),
AcceptanceCriteria: substituteVariables(oldIssue.AcceptanceCriteria, opts.Vars),
Assignee: issueAssignee,
EstimatedMinutes: oldIssue.EstimatedMinutes,
Wisp: opts.Wisp,
}
// Generate custom ID for dynamic bonding if ParentID is set
if opts.ParentID != "" {
bondedID, err := generateBondedID(oldIssue.ID, subgraph.Root.ID, opts)
if err != nil {
return nil, fmt.Errorf("failed to generate bonded ID for %s: %w", oldIssue.ID, err)
}
createArgs.ID = bondedID
}
resp, err := client.Create(createArgs)
if err != nil {
return nil, fmt.Errorf("failed to create issue from %s: %w", oldIssue.ID, err)
}
// Parse response to get the new issue ID
var newIssue types.Issue
if err := json.Unmarshal(resp.Data, &newIssue); err != nil {
return nil, fmt.Errorf("failed to parse created issue: %w", err)
}
idMapping[oldIssue.ID] = newIssue.ID
}
// Second pass: recreate dependencies with new IDs
for _, dep := range subgraph.Dependencies {
newFromID, ok1 := idMapping[dep.IssueID]
newToID, ok2 := idMapping[dep.DependsOnID]
if !ok1 || !ok2 {
continue // Skip if either end is outside the subgraph
}
_, err := client.AddDependency(&rpc.DepAddArgs{
FromID: newFromID,
ToID: newToID,
DepType: string(dep.Type),
})
if err != nil {
return nil, fmt.Errorf("failed to create dependency: %w", err)
}
}
return &InstantiateResult{
NewEpicID: idMapping[subgraph.Root.ID],
IDMapping: idMapping,
Created: len(subgraph.Issues),
}, nil
}
// extractVariables finds all {{variable}} patterns in text
func extractVariables(text string) []string {
matches := variablePattern.FindAllStringSubmatch(text, -1)