Files
beads/cmd/bd/import.go
Steve Yegge a86f3e139e Add native Windows support (#91)
- Native Windows daemon using TCP loopback endpoints
- Direct-mode fallback for CLI/daemon compatibility
- Comment operations over RPC
- PowerShell installer script
- Go 1.24 requirement
- Cross-OS testing documented

Co-authored-by: danshapiro <danshapiro@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-c6230265-055f-4af1-9712-4481061886db
Co-authored-by: Amp <amp@ampcode.com>
2025-10-20 21:08:49 -07:00

171 lines
5.2 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/types"
)
var importCmd = &cobra.Command{
Use: "import",
Short: "Import issues from JSONL format",
Long: `Import issues from JSON Lines format (one JSON object per line).
Reads from stdin by default, or use -i flag for file input.
Behavior:
- Existing issues (same ID) are updated
- New issues are created
- Collisions (same ID, different content) are detected
- Use --resolve-collisions to automatically remap colliding issues
- Use --dry-run to preview changes without applying them`,
Run: func(cmd *cobra.Command, args []string) {
input, _ := cmd.Flags().GetString("input")
skipUpdate, _ := cmd.Flags().GetBool("skip-existing")
strict, _ := cmd.Flags().GetBool("strict")
resolveCollisions, _ := cmd.Flags().GetBool("resolve-collisions")
dryRun, _ := cmd.Flags().GetBool("dry-run")
// Open input
in := os.Stdin
if input != "" {
f, err := os.Open(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
os.Exit(1)
}
defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close input file: %v\n", err)
}
}()
in = f
}
// Phase 1: Read and parse all JSONL
ctx := context.Background()
scanner := bufio.NewScanner(in)
var allIssues []*types.Issue
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
// Skip empty lines
if line == "" {
continue
}
// Parse JSON
var issue types.Issue
if err := json.Unmarshal([]byte(line), &issue); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
os.Exit(1)
}
allIssues = append(allIssues, &issue)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
}
// Phase 2: Use shared import logic
opts := ImportOptions{
ResolveCollisions: resolveCollisions,
DryRun: dryRun,
SkipUpdate: skipUpdate,
Strict: strict,
}
result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts)
// Handle errors and special cases
if err != nil {
// Check if it's a collision error when not resolving
if !resolveCollisions && result != nil && len(result.CollisionIDs) > 0 {
// Print collision report before exiting
fmt.Fprintf(os.Stderr, "\n=== Collision Detection Report ===\n")
fmt.Fprintf(os.Stderr, "COLLISIONS DETECTED: %d\n\n", result.Collisions)
fmt.Fprintf(os.Stderr, "Colliding issue IDs: %v\n", result.CollisionIDs)
fmt.Fprintf(os.Stderr, "\nCollision detected! Use --resolve-collisions to automatically remap colliding issues.\n")
fmt.Fprintf(os.Stderr, "Or use --dry-run to preview without making changes.\n")
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Import failed: %v\n", err)
os.Exit(1)
}
// Handle dry-run mode
if dryRun {
if result.Collisions > 0 {
fmt.Fprintf(os.Stderr, "\n=== Collision Detection Report ===\n")
fmt.Fprintf(os.Stderr, "COLLISIONS DETECTED: %d\n", result.Collisions)
fmt.Fprintf(os.Stderr, "Colliding issue IDs: %v\n", result.CollisionIDs)
} else {
fmt.Fprintf(os.Stderr, "No collisions detected.\n")
}
fmt.Fprintf(os.Stderr, "Would create %d new issues, update %d existing issues\n",
result.Created, result.Updated)
fmt.Fprintf(os.Stderr, "\nDry-run mode: no changes made\n")
os.Exit(0)
}
// Print remapping report if collisions were resolved
if len(result.IDMapping) > 0 {
fmt.Fprintf(os.Stderr, "\n=== Remapping Report ===\n")
fmt.Fprintf(os.Stderr, "Issues remapped: %d\n\n", len(result.IDMapping))
// Sort by old ID for consistent output
type mapping struct {
oldID string
newID string
}
mappings := make([]mapping, 0, len(result.IDMapping))
for oldID, newID := range result.IDMapping {
mappings = append(mappings, mapping{oldID, newID})
}
sort.Slice(mappings, func(i, j int) bool {
return mappings[i].oldID < mappings[j].oldID
})
fmt.Fprintf(os.Stderr, "Remappings:\n")
for _, m := range mappings {
fmt.Fprintf(os.Stderr, " %s → %s\n", m.oldID, m.newID)
}
fmt.Fprintf(os.Stderr, "\nAll text and dependency references have been updated.\n")
}
// Schedule auto-flush after import completes
markDirtyAndScheduleFlush()
// Print summary
fmt.Fprintf(os.Stderr, "Import complete: %d created, %d updated", result.Created, result.Updated)
if result.Skipped > 0 {
fmt.Fprintf(os.Stderr, ", %d skipped", result.Skipped)
}
if len(result.IDMapping) > 0 {
fmt.Fprintf(os.Stderr, ", %d issues remapped", len(result.IDMapping))
}
fmt.Fprintf(os.Stderr, "\n")
},
}
func init() {
importCmd.Flags().StringP("input", "i", "", "Input file (default: stdin)")
importCmd.Flags().BoolP("skip-existing", "s", false, "Skip existing issues instead of updating them")
importCmd.Flags().Bool("strict", false, "Fail on dependency errors instead of treating them as warnings")
importCmd.Flags().Bool("resolve-collisions", false, "Automatically resolve ID collisions by remapping")
importCmd.Flags().Bool("dry-run", false, "Preview collision detection without making changes")
rootCmd.AddCommand(importCmd)
}