package main import ( "flag" "fmt" "io/fs" "os" "path/filepath" "strings" ) const ( defaultMaxSize = 512 * 1024 // 512KB defaultMaxDepth = 20 version = "0.1.0" ) var defaultIgnores = []string{ ".git", "node_modules", "vendor", "bin", } 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", } 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 maxSize int64 maxDepth int ignores []string exts []string absRoot string } func parseFlags() (*config, error) { var ( input string output string appendMd bool maxSize int64 maxDepth int ignores stringSlice exts stringSlice showVer 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.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, e.g. .log)") flag.BoolVar(&showVer, "version", false, "print version and exit") flag.Usage = func() { 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() } flag.Parse() if showVer { fmt.Printf("dirmd v%s\n", version) os.Exit(0) } // Validation if output == "" { return nil, fmt.Errorf("-o is required") } cfg := &config{ outputPath: output, appendMode: appendMd, maxSize: maxSize, maxDepth: maxDepth, ignores: append(defaultIgnores, ignores...), exts: append(defaultExts, exts...), } if input != "" { // Single file mode cfg.inputPath = input if !appendMd { return nil, fmt.Errorf("single file input (-i) requires append mode (-a)") } } else { // Directory mode args := flag.Args() if len(args) < 1 { return nil, fmt.Errorf("directory argument is required when -i is not used") } cfg.inputPath = args[0] } // Resolve absolute path abs, err := filepath.Abs(cfg.inputPath) if err != nil { return nil, fmt.Errorf("failed to resolve absolute path: %w", err) } cfg.absRoot = abs // Non-append mode: output must not exist if !appendMd { if _, err := os.Stat(output); err == nil { return nil, fmt.Errorf("output file %s already exists (use -a to append)", output) } } return cfg, nil } func isSymlink(path string) bool { info, err := os.Lstat(path) if err != nil { return false } return info.Mode()&os.ModeSymlink != 0 } func walkDirectory(cfg *config) error { return filepath.WalkDir(cfg.absRoot, func(path string, d fs.DirEntry, err error) error { if err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) return nil } rel, err := filepath.Rel(cfg.absRoot, path) if err != nil { return nil } // Calculate depth depth := strings.Count(rel, string(filepath.Separator)) if rel == "." { depth = 0 } // Check max depth if depth > cfg.maxDepth { if d.IsDir() { return filepath.SkipDir } return nil } // Skip symlinks if isSymlink(path) { if d.IsDir() { return filepath.SkipDir } return nil } // Skip dotfiles base := filepath.Base(path) if strings.HasPrefix(base, ".") && base != "." { if d.IsDir() { return filepath.SkipDir } return nil } // Check ignore patterns against full relative path for _, pattern := range cfg.ignores { matched, _ := filepath.Match(pattern, base) if matched { if d.IsDir() { return filepath.SkipDir } return nil } } if d.IsDir() { fmt.Fprintf(os.Stderr, "[dir] %s (depth %d)\n", rel, depth) return nil } // Check extension blacklist ext := filepath.Ext(path) for _, badExt := range cfg.exts { if strings.EqualFold(ext, badExt) { fmt.Fprintf(os.Stderr, "[skip] %s (blacklisted extension)\n", rel) return nil } } // Check file size info, err := d.Info() if err != nil { fmt.Fprintf(os.Stderr, "warning: cannot stat %s: %v\n", rel, err) return nil } if info.Size() > cfg.maxSize { fmt.Fprintf(os.Stderr, "[skip] %s (size %d > %d)\n", rel, info.Size(), cfg.maxSize) return nil } fmt.Fprintf(os.Stderr, "[file] %s (size %d, depth %d)\n", rel, info.Size(), depth) return nil }) } func processSingleFile(cfg *config) error { info, err := os.Stat(cfg.absRoot) if err != nil { return fmt.Errorf("cannot access %s: %w", cfg.absRoot, err) } if info.IsDir() { // User passed a directory with -i, treat as directory walk return walkDirectory(cfg) } if info.Size() > cfg.maxSize { return fmt.Errorf("file %s exceeds max size (%d > %d)", cfg.absRoot, info.Size(), cfg.maxSize) } fmt.Fprintf(os.Stderr, "[file] %s (size %d)\n", cfg.absRoot, info.Size()) return nil } func main() { cfg, err := parseFlags() if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n\n", err) flag.Usage() os.Exit(1) } if cfg.inputPath == "" { fmt.Fprintln(os.Stderr, "error: no input specified") flag.Usage() os.Exit(1) } if cfg.appendMode && cfg.inputPath != "" { // Could be single file or directory with -i in append mode if err := processSingleFile(cfg); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } else { // Directory mode info, err := os.Stat(cfg.absRoot) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if !info.IsDir() { fmt.Fprintf(os.Stderr, "error: %s is not a directory\n", cfg.absRoot) os.Exit(1) } if err := walkDirectory(cfg); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } fmt.Fprintf(os.Stderr, "done\n") }