esync

Directory watching and remote syncing
Log | Files | Refs | README | LICENSE

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 }