package config import ( "flag" "fmt" "os" "path/filepath" "strings" ) const ( DefaultMaxSize = 512 * 1024 DefaultMaxDepth = 20 Version = "0.1.4" ) 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", "uv.lock", } 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 IncludeInstructions bool PickPaths []string SmallTree 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 includeInstructions bool pickPaths stringSlice smallTree 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") flag.BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md") flag.BoolVar(&omitTest, "omit-test", false, "exclude test files and strip #[cfg(test)] blocks") flag.BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md") flag.BoolVar(&includeInstructions, "instructions", false, "include .dirmd instructions file if present in root") flag.Var(&pickPaths, "pick", "repeatable: relative path to file to include as pick (requires --vertical-slices)") flag.BoolVar(&smallTree, "small-tree", false, "use breadcrumb-only tree for picked files (default: full tree)") 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") } if len(pickPaths) > 0 && !verticalSlices { return nil, fmt.Errorf("--pick requires --vertical-slices") } 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, IncludeInstructions: includeInstructions, PickPaths: pickPaths, SmallTree: smallTree, } 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 }