package main import ( "fmt" "os" "path/filepath" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/routing" "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/ui" ) var refileCmd = &cobra.Command{ Use: "refile ", GroupID: "issues", Short: "Move an issue to a different rig", Long: `Move an issue from one rig to another. This creates a new issue in the target rig with the same content, then closes the source issue with a reference to the new location. The target rig can be specified as: - A rig name: beads, gastown - A prefix: bd-, gt- - A prefix without hyphen: bd, gt Examples: bd refile bd-8hea gastown # Move to gastown by rig name bd refile bd-8hea gt- # Move to gastown by prefix bd refile bd-8hea gt # Move to gastown (prefix without hyphen)`, Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { CheckReadonly("refile") sourceID := args[0] targetRig := args[1] keepOpen, _ := cmd.Flags().GetBool("keep-open") ctx := rootCtx // Step 1: Get the source issue (via routing if needed) result, err := resolveAndGetIssueWithRouting(ctx, store, sourceID) if err != nil { FatalError("failed to find source issue: %v", err) } if result == nil || result.Issue == nil { FatalError("source issue %s not found", sourceID) } defer result.Close() sourceIssue := result.Issue resolvedSourceID := result.ResolvedID // Warn if source issue is already closed if sourceIssue.Status == types.StatusClosed { fmt.Fprintf(os.Stderr, "%s Source issue %s is already closed\n", ui.RenderWarn("⚠"), resolvedSourceID) } // Step 2: Find the town-level beads directory townBeadsDir, err := findTownBeadsDir() if err != nil { FatalError("cannot refile: %v", err) } // Step 3: Resolve the target rig's beads directory targetBeadsDir, targetPrefix, err := routing.ResolveBeadsDirForRig(targetRig, townBeadsDir) if err != nil { FatalError("%v", err) } // Check we're not refiling to the same rig sourcePrefix := routing.ExtractPrefix(resolvedSourceID) if sourcePrefix == targetPrefix { FatalError("source issue %s is already in rig %q", resolvedSourceID, targetRig) } // Step 4: Open storage for the target rig targetDBPath := filepath.Join(targetBeadsDir, "beads.db") targetStore, err := sqlite.New(ctx, targetDBPath) if err != nil { FatalError("failed to open target rig database: %v", err) } defer func() { if err := targetStore.Close(); err != nil { fmt.Fprintf(os.Stderr, "warning: failed to close target rig database: %v\n", err) } }() // Step 5: Create the new issue in target rig (copy all fields) newIssue := &types.Issue{ // Don't copy ID - let target rig generate new one Title: sourceIssue.Title, Description: sourceIssue.Description, Design: sourceIssue.Design, AcceptanceCriteria: sourceIssue.AcceptanceCriteria, Status: types.StatusOpen, // Always start as open Priority: sourceIssue.Priority, IssueType: sourceIssue.IssueType, Assignee: sourceIssue.Assignee, ExternalRef: sourceIssue.ExternalRef, EstimatedMinutes: sourceIssue.EstimatedMinutes, SourceRepo: sourceIssue.SourceRepo, Ephemeral: sourceIssue.Ephemeral, MolType: sourceIssue.MolType, RoleType: sourceIssue.RoleType, Rig: sourceIssue.Rig, CreatedBy: actor, } // Append refiled note to description if newIssue.Description != "" { newIssue.Description += "\n\n" } newIssue.Description += fmt.Sprintf("(Refiled from %s)", resolvedSourceID) if err := targetStore.CreateIssue(ctx, newIssue, actor); err != nil { FatalError("failed to create issue in target rig: %v", err) } // Step 6: Copy labels if any labels, err := result.Store.GetLabels(ctx, resolvedSourceID) if err == nil && len(labels) > 0 { for _, label := range labels { if err := targetStore.AddLabel(ctx, newIssue.ID, label, actor); err != nil { WarnError("failed to copy label %s: %v", label, err) } } } // Step 7: Close the source issue (unless --keep-open) if !keepOpen { closeReason := fmt.Sprintf("Refiled to %s", newIssue.ID) if err := result.Store.CloseIssue(ctx, resolvedSourceID, closeReason, actor, ""); err != nil { WarnError("failed to close source issue: %v", err) } // Schedule auto-flush if source was local store if !result.Routed { markDirtyAndScheduleFlush() } } // Output if jsonOutput { outputJSON(map[string]interface{}{ "source": resolvedSourceID, "target": newIssue.ID, "closed": !keepOpen, }) } else { fmt.Printf("%s Refiled %s → %s\n", ui.RenderPass("✓"), resolvedSourceID, newIssue.ID) if !keepOpen { fmt.Printf(" Source issue closed\n") } } }, } func init() { refileCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)") refileCmd.ValidArgsFunction = issueIDCompletion rootCmd.AddCommand(refileCmd) }