package config import ( "flag" "fmt" "os" "path/filepath" "strings" ) const ( DefaultMaxSize = 512 * 1024 DefaultMaxDepth = 20 Version = "0.1.0" ) var DefaultIgnores = []string{ ".git", "node_modules", "vendor", "bin", "go.mod", "go.sum", "target", "build", "dist", "Cargo.lock", ".idea", ".vscode", "dependency-reduced-pom.xml", "__pycache__", "venv", "env", } var DefaultExts = []string{ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".ico", ".mp4", ".avi", ".mov", ".mkv", ".webm", ".mp3", ".wav", ".flac", ".ogg", ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", ".exe", ".dll", ".so", ".dylib", ".wasm", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".pem", ".key", ".cert", ".db", ".sqlite", ".sqlite3", ".class", ".jar", ".war", ".ear", ".pyc", ".pyo", } var FrontendExts = []string{ ".html", ".htm", ".gohtml", ".tmpl", ".css", ".scss", ".sass", ".less", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".vue", ".svelte", } type stringSlice []string func (s *stringSlice) String() string { return strings.Join(*s, ", ") } func (s *stringSlice) Set(v string) error { *s = append(*s, v) return nil } type Config struct { InputPath string OutputPath string AppendMode bool Force bool SingleFile bool MaxSize int64 MaxDepth int Ignores []string Extensions []string AbsRoot string ProtonDrive bool DrivePath string VerticalSlices bool SkipFrontend bool FrontendOnly bool OmitTest bool OmitMd bool } func PrintUsage() { fmt.Fprintf(os.Stderr, "dirmd v%s\n\n", Version) fmt.Fprintf(os.Stderr, "Usage: dirmd [flags] \n") fmt.Fprintf(os.Stderr, " dirmd -a -i -o \n\n") fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() } func Parse() (*Config, error) { var ( input string output string appendMd bool force bool maxSize int64 maxDepth int ignores stringSlice exts stringSlice showVer bool protonDrive bool drivePath string verticalSlices bool skipFrontend bool frontendOnly bool omitTest bool omitMd bool ) flag.StringVar(&input, "i", "", "single file input (append mode only)") flag.StringVar(&output, "o", "", "output markdown file path (required)") flag.BoolVar(&appendMd, "a", false, "append to existing output file") flag.BoolVar(&force, "f", false, "overwrite existing output file (non-append mode)") flag.Int64Var(&maxSize, "max-size", DefaultMaxSize, "max file size in bytes") flag.IntVar(&maxDepth, "max-depth", DefaultMaxDepth, "max directory recursion depth") flag.Var(&ignores, "ignore", "additional ignore patterns (glob, repeatable)") flag.Var(&exts, "extensions", "additional file extensions to skip (repeatable)") flag.BoolVar(&showVer, "version", false, "print version and exit") flag.BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally") flag.StringVar(&drivePath, "drive-path", "", "full remote path on Proton Drive (requires --proton-drive") flag.BoolVar(&verticalSlices, "vertical-slices", false, "split output into root file + one markdown per top-level directory") flag.BoolVar(&skipFrontend, "skip-frontend", false, "omit frontend file types (.html, .css, .js, .ts, .vue, .svelte, etc.)") flag.BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md") flag.BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust") flag.BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md") flag.Usage = PrintUsage flag.Parse() if showVer { fmt.Printf("dirmd v%s\n", Version) os.Exit(0) } if output == "" { return nil, fmt.Errorf("-o is required") } if drivePath != "" && !protonDrive { return nil, fmt.Errorf("--drive-path requires --proton-drive") } if verticalSlices && appendMd { return nil, fmt.Errorf("--vertical-slices cannot be used with --append") } if verticalSlices && input != "" { return nil, fmt.Errorf("--vertical-slices cannot be used with --input-file") } if skipFrontend && frontendOnly { return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") } excludedExts := append(DefaultExts, exts...) if skipFrontend { excludedExts = append(excludedExts, FrontendExts...) } cfg := &Config{ OutputPath: output, AppendMode: appendMd, Force: force, MaxSize: maxSize, MaxDepth: maxDepth, Ignores: append(DefaultIgnores, ignores...), Extensions: excludedExts, ProtonDrive: protonDrive, DrivePath: drivePath, VerticalSlices: verticalSlices, SkipFrontend: skipFrontend, FrontendOnly: frontendOnly, OmitTest: omitTest, OmitMd: omitMd, } if input != "" { cfg.SingleFile = true cfg.InputPath = input if !appendMd { return nil, fmt.Errorf("single file input (-i) requires append mode (-a)") } } else { args := flag.Args() if len(args) < 1 { return nil, fmt.Errorf("directory argument is required when -i is not used") } cfg.InputPath = args[0] } abs, err := filepath.Abs(cfg.InputPath) if err != nil { return nil, fmt.Errorf("failed to resolve absolute path: %w", err) } cfg.AbsRoot = abs if !appendMd { if _, err := os.Stat(output); err == nil && !force { return nil, fmt.Errorf("output file %s already exists (use -f to overwrite or -a to append)", output) } } return cfg, nil }