package client import ( "context" "fmt" "log/slog" "time" "git.franklin.lab/steve.cliff/pcli/model" ) func (c *Client) ExportProject(ctx context.Context, projectID string, boardFilter string) (*model.ExportEnvelope, error) { // Fetch users for comment attribution userMap, err := c.buildUserMap(ctx) if err != nil { c.Logger.Warn("Failed to fetch users for comment attribution, will use 'unknown user'", slog.String("error", err.Error()), ) userMap = make(map[string]string) } // Get project details project, err := c.GetProject(ctx, projectID) if err != nil { return nil, fmt.Errorf("failed to get project: %w", err) } // Get all boards for the project boards, err := c.ListBoards(ctx) if err != nil { return nil, fmt.Errorf("failed to list boards: %w", err) } // Filter boards to this project var projectBoards []model.Board for _, b := range boards { if b.ProjectID == projectID { projectBoards = append(projectBoards, b) } } // Apply board filter if specified if boardFilter != "" { var filtered []model.Board for _, b := range projectBoards { if b.ID == boardFilter || b.Name == boardFilter { filtered = append(filtered, b) } } if len(filtered) == 0 { return nil, fmt.Errorf("board %q not found in project %q", boardFilter, project.Name) } projectBoards = filtered } // Export each board var exportBoards []model.ExportBoard for _, b := range projectBoards { c.Logger.Info("Exporting board", slog.String("board", b.Name)) eb, err := c.exportBoard(ctx, b.ID, userMap) if err != nil { return nil, fmt.Errorf("failed to export board %q: %w", b.Name, err) } exportBoards = append(exportBoards, *eb) } return &model.ExportEnvelope{ Version: 1, ExportedAt: time.Now().UTC().Format(time.RFC3339), Project: model.ExportProject{ Name: project.Name, Description: project.Description, Boards: exportBoards, }, }, nil } func (c *Client) buildUserMap(ctx context.Context) (map[string]string, error) { users, err := c.ListUsers(ctx) if err != nil { return nil, err } userMap := make(map[string]string, len(users)) for _, u := range users { userMap[u.ID] = u.Name } return userMap, nil } func (c *Client) exportBoard(ctx context.Context, boardID string, userMap map[string]string) (*model.ExportBoard, error) { board, err := c.GetBoard(ctx, boardID) if err != nil { return nil, err } // Build lookup maps from board included data listMap := make(map[string]string) // id -> name systemListIDs := make(map[string]bool) for _, l := range board.Lists { if l.Type == "archive" || l.Type == "trash" { systemListIDs[l.ID] = true continue } name := "" if l.Name != nil { name = *l.Name } listMap[l.ID] = name } labelMap := make(map[string]string) // id -> name for _, l := range board.Labels { name := "" if l.Name != nil { name = *l.Name } labelMap[l.ID] = name } // Build card -> label names map cardLabels := make(map[string][]string) for _, cl := range board.CardLabels { if name, ok := labelMap[cl.LabelID]; ok { cardLabels[cl.CardID] = append(cardLabels[cl.CardID], name) } } // Export lists (skip system lists like archive and trash) var exportLists []model.ExportList for _, l := range board.Lists { if l.Type == "archive" || l.Type == "trash" { continue } name := "" if l.Name != nil { name = *l.Name } pos := float64(0) if l.Position != nil { pos = *l.Position } exportLists = append(exportLists, model.ExportList{ Name: name, Position: pos, Type: l.Type, Color: l.Color, }) } // Export labels var exportLabels []model.ExportLabel for _, l := range board.Labels { exportLabels = append(exportLabels, model.ExportLabel{ Name: l.Name, Position: l.Position, Color: l.Color, }) } // Export cards with per-card details (skip cards in archive/trash lists) var exportCards []model.ExportCard for _, card := range board.Cards { if systemListIDs[card.ListID] { continue } c.Logger.Debug("Exporting card", slog.String("card", card.Name)) ec, err := c.exportCard(ctx, card, listMap[card.ListID], cardLabels[card.ID], userMap) if err != nil { return nil, fmt.Errorf("failed to export card %q: %w", card.Name, err) } exportCards = append(exportCards, *ec) } return &model.ExportBoard{ Name: board.Name, Position: board.Position, Lists: exportLists, Labels: exportLabels, Cards: exportCards, }, nil } func (c *Client) exportCard(ctx context.Context, card model.Card, listName string, labelNames []string, userMap map[string]string) (*model.ExportCard, error) { // Fetch card details (task lists + tasks) detail, err := c.GetCard(ctx, card.ID) if err != nil { return nil, fmt.Errorf("failed to get card details: %w", err) } // Fetch comments comments, err := c.ListComments(ctx, card.ID, 0) if err != nil { return nil, fmt.Errorf("failed to get card comments: %w", err) } // Build task list ID -> tasks map tasksByList := make(map[string][]model.Task) for _, t := range detail.Tasks { tasksByList[t.TaskListID] = append(tasksByList[t.TaskListID], t) } // Export task lists var exportTaskLists []model.ExportTaskList for _, tl := range detail.TaskLists { var exportTasks []model.ExportTask for _, t := range tasksByList[tl.ID] { exportTasks = append(exportTasks, model.ExportTask{ Name: t.Name, Position: t.Position, IsCompleted: t.IsCompleted, }) } exportTaskLists = append(exportTaskLists, model.ExportTaskList{ Name: tl.Name, Position: tl.Position, Tasks: exportTasks, }) } // Export comments with attribution var exportComments []model.ExportComment for _, cm := range comments { author := "unknown user" if cm.UserID != nil { if name, ok := userMap[*cm.UserID]; ok { author = name } } exportComments = append(exportComments, model.ExportComment{ Text: fmt.Sprintf("(Original comment by %s)\n%s", author, cm.Text), }) } return &model.ExportCard{ Name: card.Name, Position: card.Position, ListName: listName, LabelNames: labelNames, Description: card.Description, DueDate: card.DueDate, IsClosed: card.IsClosed, Type: card.Type, TaskLists: exportTaskLists, Comments: exportComments, }, nil }