diff --git a/cmd/bd/template.go b/cmd/bd/template.go index 1aedfda0..545ced27 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -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)