init.go (6609B)
1 package cmd 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/spf13/cobra" 10 11 "github.com/louloulibs/esync/internal/config" 12 ) 13 14 // --------------------------------------------------------------------------- 15 // Default patterns (already present in DefaultTOML) 16 // --------------------------------------------------------------------------- 17 18 // defaultIgnorePatterns lists patterns that DefaultTOML() already includes 19 // in settings.ignore, so we can skip them when merging from .gitignore. 20 var defaultIgnorePatterns = map[string]bool{ 21 ".git": true, 22 ".git/": true, 23 "node_modules": true, 24 "node_modules/": true, 25 ".DS_Store": true, 26 } 27 28 // commonDirs lists directories to auto-detect and exclude. 29 var commonDirs = []string{ 30 ".git", 31 "node_modules", 32 "__pycache__", 33 "build", 34 ".venv", 35 "dist", 36 ".tox", 37 ".mypy_cache", 38 } 39 40 // --------------------------------------------------------------------------- 41 // Flags 42 // --------------------------------------------------------------------------- 43 44 var initRemote string 45 46 // --------------------------------------------------------------------------- 47 // Command 48 // --------------------------------------------------------------------------- 49 50 var initCmd = &cobra.Command{ 51 Use: "init", 52 Short: "Generate an .esync.toml configuration file", 53 Long: "Inspect the current directory to generate a smart .esync.toml with .gitignore import and common directory exclusion.", 54 RunE: runInit, 55 } 56 57 func init() { 58 initCmd.Flags().StringVarP(&initRemote, "remote", "r", "", "pre-fill remote destination") 59 rootCmd.AddCommand(initCmd) 60 } 61 62 // --------------------------------------------------------------------------- 63 // Main logic 64 // --------------------------------------------------------------------------- 65 66 func runInit(cmd *cobra.Command, args []string) error { 67 // 1. Determine output path 68 outPath := cfgFile 69 if outPath == "" { 70 outPath = "./.esync.toml" 71 } 72 73 // 2. If file exists, prompt for overwrite confirmation 74 if _, err := os.Stat(outPath); err == nil { 75 fmt.Printf("File %s already exists. Overwrite? [y/N] ", outPath) 76 reader := bufio.NewReader(os.Stdin) 77 answer, _ := reader.ReadString('\n') 78 answer = strings.TrimSpace(strings.ToLower(answer)) 79 if answer != "y" && answer != "yes" { 80 fmt.Println("Aborted.") 81 return nil 82 } 83 } 84 85 // 3. Start with default TOML content 86 content := config.DefaultTOML() 87 88 // 4. Read .gitignore patterns 89 gitignorePatterns := readGitignore() 90 91 // 5. Detect common directories that exist and aren't already in defaults 92 detectedDirs := detectCommonDirs() 93 94 // 6. Remote destination: use flag or prompt 95 remote := initRemote 96 if remote == "" { 97 fmt.Print("Remote destination (e.g. user@host:/path/to/dest): ") 98 reader := bufio.NewReader(os.Stdin) 99 line, _ := reader.ReadString('\n') 100 remote = strings.TrimSpace(line) 101 } 102 103 // Replace remote in TOML content if provided 104 if remote != "" { 105 content = strings.Replace( 106 content, 107 `remote = "user@host:/path/to/dest"`, 108 fmt.Sprintf(`remote = %q`, remote), 109 1, 110 ) 111 } 112 113 // 7. Merge extra ignore patterns into TOML content 114 var extraPatterns []string 115 extraPatterns = append(extraPatterns, gitignorePatterns...) 116 extraPatterns = append(extraPatterns, detectedDirs...) 117 118 // Deduplicate: remove any that are already in defaults or duplicated 119 seen := make(map[string]bool) 120 for k := range defaultIgnorePatterns { 121 seen[k] = true 122 } 123 var uniqueExtras []string 124 for _, p := range extraPatterns { 125 // Normalize: strip trailing slash for comparison 126 normalized := strings.TrimSuffix(p, "/") 127 if seen[normalized] || seen[normalized+"/"] || seen[p] { 128 continue 129 } 130 seen[normalized] = true 131 seen[normalized+"/"] = true 132 uniqueExtras = append(uniqueExtras, p) 133 } 134 135 if len(uniqueExtras) > 0 { 136 // Build the new ignore list: default patterns + extras 137 var quoted []string 138 // Start with the defaults already in the TOML 139 for _, d := range []string{".git", "node_modules", ".DS_Store"} { 140 quoted = append(quoted, fmt.Sprintf("%q", d)) 141 } 142 for _, p := range uniqueExtras { 143 quoted = append(quoted, fmt.Sprintf("%q", p)) 144 } 145 newIgnoreLine := "ignore = [" + strings.Join(quoted, ", ") + "]" 146 content = strings.Replace( 147 content, 148 `ignore = [".git", "node_modules", ".DS_Store"]`, 149 newIgnoreLine, 150 1, 151 ) 152 } 153 154 // 8. Write to file 155 if err := os.WriteFile(outPath, []byte(content), 0644); err != nil { 156 return fmt.Errorf("writing config file: %w", err) 157 } 158 159 // 9. Print summary 160 fmt.Println() 161 fmt.Printf("Created %s\n", outPath) 162 fmt.Println() 163 if len(gitignorePatterns) > 0 { 164 fmt.Printf(" Imported %d pattern(s) from .gitignore\n", len(gitignorePatterns)) 165 } 166 if len(detectedDirs) > 0 { 167 fmt.Printf(" Auto-excluded %d common dir(s): %s\n", 168 len(detectedDirs), strings.Join(detectedDirs, ", ")) 169 } 170 if len(uniqueExtras) > 0 { 171 fmt.Printf(" Total extra ignore patterns: %d\n", len(uniqueExtras)) 172 } 173 fmt.Println() 174 fmt.Println("Next steps:") 175 fmt.Println(" esync check — validate your configuration") 176 fmt.Println(" esync edit — open the config in your editor") 177 178 return nil 179 } 180 181 // --------------------------------------------------------------------------- 182 // Helpers 183 // --------------------------------------------------------------------------- 184 185 // readGitignore reads .gitignore in the current directory and returns 186 // patterns, skipping comments, empty lines, and patterns already present 187 // in the default ignore list. 188 func readGitignore() []string { 189 f, err := os.Open(".gitignore") 190 if err != nil { 191 return nil 192 } 193 defer f.Close() 194 195 var patterns []string 196 scanner := bufio.NewScanner(f) 197 for scanner.Scan() { 198 line := strings.TrimSpace(scanner.Text()) 199 200 // Skip empty lines and comments 201 if line == "" || strings.HasPrefix(line, "#") { 202 continue 203 } 204 205 // Skip patterns already in the defaults 206 normalized := strings.TrimSuffix(line, "/") 207 if defaultIgnorePatterns[line] || defaultIgnorePatterns[normalized] || defaultIgnorePatterns[normalized+"/"] { 208 continue 209 } 210 211 patterns = append(patterns, line) 212 } 213 214 return patterns 215 } 216 217 // detectCommonDirs checks for common directories that should typically be 218 // excluded, returns the ones that exist on disk and aren't already in the 219 // default ignore list. 220 func detectCommonDirs() []string { 221 var found []string 222 for _, dir := range commonDirs { 223 // Skip if already in defaults 224 if defaultIgnorePatterns[dir] || defaultIgnorePatterns[dir+"/"] { 225 continue 226 } 227 228 // Check if directory actually exists 229 info, err := os.Stat(dir) 230 if err != nil || !info.IsDir() { 231 continue 232 } 233 234 found = append(found, dir) 235 } 236 return found 237 }