package cmd import ( "fmt" "os" "path/filepath" "github.com/spf13/cobra" "dirmd/internal/config" "dirmd/internal/drive" "dirmd/internal/renderer" "dirmd/internal/walker" ) var ( rootCmd = &cobra.Command{ Use: "dirmd [flags] ", Short: "Generate markdown documentation from directory structure", Long: `dirmd walks a directory and generates a markdown file with: - Directory tree structure - All readable text files with their contents Examples: # Create new documentation dirmd -o output.md ~/repos/myproject # Append a single file dirmd -a -i README.md -o existing.md # Append another directory dirmd -a -o existing.md ~/repos/anotherproject # Overwrite existing local file dirmd -o output.md ~/repos/myproject -f # Generate with vertical slices (root + one file per top-level dir) dirmd -o output.md ~/repos/myproject --vertical-slices # Generate and upload to Proton Drive dirmd -o output.md ~/repos/myproject --proton-drive # Generate and upload to a specific remote path dirmd -o output.md ~/repos/myproject --proton-drive --drive-path /my-files/docs/api.md`, RunE: run, } outputPath string appendMode bool force bool singleFile string maxSize int64 maxDepth int ignorePatterns []string extensions []string protonDrive bool drivePath string verticalSlices bool ) func Execute() { if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func init() { rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output markdown file path (required)") rootCmd.Flags().BoolVarP(&appendMode, "append", "a", false, "append to existing output file") rootCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing output file (non-append mode)") rootCmd.Flags().StringVarP(&singleFile, "input-file", "i", "", "single file input (append mode only)") rootCmd.Flags().Int64Var(&maxSize, "max-size", config.DefaultMaxSize, "max file size in bytes") rootCmd.Flags().IntVar(&maxDepth, "max-depth", config.DefaultMaxDepth, "max directory recursion depth") rootCmd.Flags().StringArrayVar(&ignorePatterns, "ignore", nil, "additional ignore patterns (glob)") rootCmd.Flags().StringArrayVar(&extensions, "extensions", nil, "additional file extensions to skip (repeatable)") rootCmd.Flags().BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally") rootCmd.Flags().StringVar(&drivePath, "drive-path", "", "full remote path on Proton Drive (e.g. /my-files/md/report.md). Requires --proton-drive") rootCmd.Flags().BoolVar(&verticalSlices, "vertical-slices", false, "split output into root file + one markdown per top-level directory") rootCmd.MarkPersistentFlagRequired("output") } func run(cmd *cobra.Command, args []string) error { if drivePath != "" && !protonDrive { return fmt.Errorf("--drive-path requires --proton-drive") } if verticalSlices && appendMode { return fmt.Errorf("--vertical-slices cannot be used with --append") } if verticalSlices && singleFile != "" { return fmt.Errorf("--vertical-slices cannot be used with --input-file") } cfg := &config.Config{ OutputPath: outputPath, AppendMode: appendMode, Force: force, MaxSize: maxSize, MaxDepth: maxDepth, Ignores: append(config.DefaultIgnores, ignorePatterns...), Extensions: append(config.DefaultExts, extensions...), ProtonDrive: protonDrive, DrivePath: drivePath, VerticalSlices: verticalSlices, } if singleFile != "" { cfg.InputPath = singleFile cfg.SingleFile = true if !appendMode { return fmt.Errorf("single file input (-i) requires append mode (-a)") } } else { if len(args) < 1 { return 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 fmt.Errorf("failed to resolve absolute path: %w", err) } cfg.AbsRoot = abs if !appendMode { if _, err := os.Stat(outputPath); err == nil && !force { return fmt.Errorf("output file %s already exists (use -f to overwrite or -a to append)", outputPath) } } result, err := walker.Run(cfg) if err != nil { return err } if len(result.Entries) == 0 { return fmt.Errorf("no readable text files found in %s", cfg.AbsRoot) } if verticalSlices { generated, err := renderer.WriteSlices(result, outputPath) if err != nil { return err } fmt.Fprintf(os.Stderr, "generated %d files\n", len(generated)) if protonDrive { if err := drive.CheckAuth(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintln(os.Stderr, "local files were written successfully, but upload was skipped") return err } for _, path := range generated { if err := drive.Upload(path, "/my-files/md/"+filepath.Base(path)); err != nil { fmt.Fprintf(os.Stderr, "error uploading %s: %v\n", path, err) } else { fmt.Fprintf(os.Stderr, "uploaded %s to Proton Drive\n", path) } } } return nil } if err := renderer.Write(result, outputPath, appendMode, cfg.SingleFile); err != nil { return err } fmt.Fprintf(os.Stderr, "wrote %d entries to %s\n", len(result.Entries), outputPath) if protonDrive { remotePath := drivePath if remotePath == "" { remotePath = "/my-files/md/" + filepath.Base(outputPath) } if err := drive.CheckAuth(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintln(os.Stderr, "local file was written successfully, but upload was skipped") return err } if err := drive.Upload(outputPath, remotePath); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintln(os.Stderr, "local file was written successfully, but upload failed") return err } fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", outputPath, remotePath) } return nil }