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}