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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user