contacts_export.go

  1package cli
  2
  3import (
  4	"encoding/csv"
  5	"encoding/json"
  6	"flag"
  7	"fmt"
  8	"os"
  9	"path/filepath"
 10	"strings"
 11
 12	"github.com/floatpane/matcha/config"
 13	"golang.org/x/term"
 14)
 15
 16func RunContactsExport(args []string) error {
 17	fs := flag.NewFlagSet("contacts export", flag.ExitOnError)
 18	format := fs.String("f", "json", "output format: json or csv")
 19	output := fs.String("o", "", "output file path (default: stdout)")
 20	help := fs.Bool("h", false, "show help")
 21
 22	if err := fs.Parse(args); err != nil {
 23		return err
 24	}
 25
 26	if *help {
 27		fmt.Println("Usage: matcha contacts export [flags]")
 28		fmt.Println("")
 29		fmt.Println("Export contacts from cache to JSON or CSV format.")
 30		fmt.Println("")
 31		fmt.Println("Flags:")
 32		fs.PrintDefaults()
 33		fmt.Println("")
 34		fmt.Println("Examples:")
 35		fmt.Println("  matcha contacts export              # JSON to stdout")
 36		fmt.Println("  matcha contacts export -f csv       # CSV to stdout")
 37		fmt.Println("  matcha contacts export -o out.json  # JSON to file")
 38		return nil
 39	}
 40
 41	formatStr := strings.ToLower(*format)
 42	if formatStr != "json" && formatStr != "csv" {
 43		return fmt.Errorf("invalid format '%s': must be 'json' or 'csv'", *format)
 44	}
 45
 46	return runExportContacts(formatStr, *output)
 47}
 48
 49func runExportContacts(format, outputPath string) error {
 50	var contacts []config.Contact
 51	var err error
 52
 53	if config.IsSecureModeEnabled() {
 54		password, err := promptForPassword()
 55		if err != nil {
 56			return fmt.Errorf("password prompt failed: %w", err)
 57		}
 58
 59		key, err := config.VerifyPassword(password)
 60		if err != nil {
 61			return fmt.Errorf("incorrect password")
 62		}
 63
 64		config.SetSessionKey(key)
 65	}
 66
 67	contactsCache, err := config.LoadContactsCache()
 68	if err != nil {
 69		path, pathErr := config.GetContactsCachePath()
 70		if pathErr != nil {
 71			return fmt.Errorf("contacts cache not found")
 72		}
 73		return fmt.Errorf("contacts cache not found at %s", path)
 74	}
 75
 76	contacts = contactsCache.Contacts
 77
 78	if len(contacts) == 0 {
 79		fmt.Fprintln(os.Stderr, "No contacts found in cache")
 80		return nil
 81	}
 82
 83	var outputData []byte
 84	if format == "json" {
 85		outputData, err = exportToJSON(contacts)
 86		if err != nil {
 87			return fmt.Errorf("failed to export to JSON: %w", err)
 88		}
 89	} else {
 90		outputData, err = exportToCSV(contacts)
 91		if err != nil {
 92			return fmt.Errorf("failed to export to CSV: %w", err)
 93		}
 94	}
 95
 96	if outputPath != "" {
 97		dir := filepath.Dir(outputPath)
 98		if dir != "." {
 99			if err := os.MkdirAll(dir, 0755); err != nil {
100				return fmt.Errorf("failed to create output directory: %w", err)
101			}
102		}
103		if err := os.WriteFile(outputPath, outputData, 0644); err != nil {
104			return fmt.Errorf("failed to write output file: %w", err)
105		}
106		fmt.Fprintf(os.Stderr, "Exported %d contacts to %s\n", len(contacts), outputPath)
107	} else {
108		fmt.Println(string(outputData))
109	}
110
111	return nil
112}
113
114func exportToJSON(contacts []config.Contact) ([]byte, error) {
115	return json.MarshalIndent(contacts, "", "  ")
116}
117
118func exportToCSV(contacts []config.Contact) ([]byte, error) {
119	var buf strings.Builder
120	writer := csv.NewWriter(&buf)
121
122	if err := writer.Write([]string{"name", "email", "last_used", "use_count"}); err != nil {
123		return nil, err
124	}
125
126	for _, c := range contacts {
127		record := []string{
128			escapeCSV(c.Name),
129			escapeCSV(c.Email),
130			c.LastUsed.Format("2006-01-02T15:04:05Z07:00"),
131			fmt.Sprintf("%d", c.UseCount),
132		}
133		if err := writer.Write(record); err != nil {
134			return nil, err
135		}
136	}
137
138	writer.Flush()
139	return []byte(buf.String()), writer.Error()
140}
141
142func escapeCSV(s string) string {
143	if strings.ContainsAny(s, `,"`) {
144		return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
145	}
146	return s
147}
148
149func promptForPassword() (string, error) {
150	fmt.Print("Enter your password: ")
151	return readPassword()
152}
153
154func readPassword() (string, error) {
155	fmt.Print("(masked input) ")
156	password, err := term.ReadPassword(int(os.Stdin.Fd()))
157	fmt.Println() // newline after password
158	if err != nil {
159		return "", err
160	}
161	return string(password), nil
162}