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 markdown files with: - Directory tree structure - All readable text files with their contents Output is always written to a directory specified by -o. Examples: # Create new documentation (writes index.md inside ./docs/) dirmd -o ./docs ~/repos/myproject # Overwrite existing output directory dirmd -o ./docs ~/repos/myproject -f # Append a single file dirmd -a -i README.md -o ./docs # Vertical slices (root index.md + one markdown per top-level dir) dirmd -o ./docs ~/repos/myproject --vertical-slices # Skip frontend files dirmd -o ./docs ~/repos/myproject --skip-frontend # Generate and upload to Proton Drive dirmd -o ./docs ~/repos/myproject --proton-drive # Generate and upload to a specific remote path dirmd -o ./docs ~/repos/myproject --proton-drive --drive-path /my-files/docs/api`, RunE: run, } outputPath string appendMode bool force bool singleFile string maxSize int64 maxDepth int ignorePatterns []string extensions []string protonDrive bool drivePath string verticalSlices bool skipFrontend bool ) func Execute() { if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func init() { rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output directory (required)") rootCmd.Flags().BoolVarP(&appendMode, "append", "a", false, "append to existing index.md") rootCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing output directory") 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 directory to Proton Drive after writing locally") rootCmd.Flags().StringVar(&drivePath, "drive-path", "", "full remote directory on Proton Drive (e.g. /my-files/docs/api). Requires --proton-drive") rootCmd.Flags().BoolVar(&verticalSlices, "vertical-slices", false, "split output into root index.md + one markdown per top-level directory") rootCmd.Flags().BoolVar(&skipFrontend, "skip-frontend", false, "omit frontend file types (.html, .css, .js, .ts, .vue, .svelte, etc.)") 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") } excludedExts := append(config.DefaultExts, extensions...) if skipFrontend { excludedExts = append(excludedExts, config.FrontendExts...) } cfg := &config.Config{ OutputPath: outputPath, AppendMode: appendMode, Force: force, MaxSize: maxSize, MaxDepth: maxDepth, Ignores: append(config.DefaultIgnores, ignorePatterns...), Extensions: excludedExts, ProtonDrive: protonDrive, DrivePath: drivePath, VerticalSlices: verticalSlices, SkipFrontend: skipFrontend, } 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 absOutput, err := filepath.Abs(outputPath) if err != nil { return fmt.Errorf("failed to resolve output path: %w", err) } absOutput = filepath.Join(filepath.Dir(absOutput), filepath.Base(absOutput)+"_dirmd") if !appendMode { if _, err := os.Stat(absOutput); err == nil && !force { return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput) } } if err := os.MkdirAll(absOutput, 0755); err != nil { return fmt.Errorf("cannot create output directory: %w", err) } repoName := filepath.Base(cfg.AbsRoot) fmt.Printf("%s\n", absOutput) 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) } indexBasename := "index_" + repoName + ".md" if verticalSlices { generated, err := renderer.WriteSlices(result, absOutput, repoName) if err != nil { return err } fmt.Fprintf(os.Stderr, "generated %d files in %s\n", len(generated), absOutput) if protonDrive { remoteDir := drivePath if remoteDir == "" { remoteDir = "/my-files/md/" + filepath.Base(absOutput) } 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 } if err := drive.UploadDir(absOutput, remoteDir); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintln(os.Stderr, "local files were written successfully, but upload failed") return err } fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", absOutput, remoteDir) } return nil } indexPath := filepath.Join(absOutput, indexBasename) if err := renderer.Write(result, indexPath, appendMode, cfg.SingleFile); err != nil { return err } fmt.Fprintf(os.Stderr, "wrote %d entries to %s\n", len(result.Entries), indexPath) if protonDrive { remoteDir := drivePath if remoteDir == "" { remoteDir = "/my-files/md/" + filepath.Base(absOutput) } 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.UploadDir(absOutput, remoteDir); 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", absOutput, remoteDir) } return nil }