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
|
// 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -267,7 +273,13 @@ Example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the template subgraph
|
// 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error loading template: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -314,7 +326,12 @@ Example:
|
|||||||
Actor: actor,
|
Actor: actor,
|
||||||
Wisp: false,
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -436,6 +453,184 @@ func loadDescendants(ctx context.Context, s storage.Storage, subgraph *TemplateS
|
|||||||
return nil
|
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
|
// extractVariables finds all {{variable}} patterns in text
|
||||||
func extractVariables(text string) []string {
|
func extractVariables(text string) []string {
|
||||||
matches := variablePattern.FindAllStringSubmatch(text, -1)
|
matches := variablePattern.FindAllStringSubmatch(text, -1)
|
||||||
|
|||||||
Reference in New Issue
Block a user