finished vertical-.slices
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
# dirmd
|
||||
|
||||
Generate markdown documentation from directory structure.
|
||||
Generate markdown documentation from directory structure. Optimized for LLM ingestion.
|
||||
|
||||
## Features
|
||||
|
||||
- Recursive directory traversal
|
||||
- Tree visualization (standard tree command format)
|
||||
- Full file contents with syntax-highlighted code blocks
|
||||
- ASCII tree visualization (LLM-friendly, minimal token overhead)
|
||||
- Full file contents with explicit delimiters and metadata
|
||||
- Filters dotfiles, binaries, and blacklisted extensions
|
||||
- Vertical slices mode: root file + one markdown per top-level directory
|
||||
- Append mode for single files or directories
|
||||
- Force overwrite of existing output files
|
||||
- Configurable depth, file size limits, ignore patterns
|
||||
@@ -31,12 +32,42 @@ Generate markdown documentation from directory structure.
|
||||
# Append another directory
|
||||
dirmd -a ~/repos/anotherproject -o existing.md
|
||||
|
||||
# Generate with vertical slices
|
||||
dirmd -o output.md ~/repos/myproject --vertical-slices
|
||||
|
||||
# Generate and upload to Proton Drive (default: /my-files/md/<filename>)
|
||||
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
|
||||
|
||||
## Vertical Slices Mode
|
||||
|
||||
When `--vertical-slices` is passed, dirmd splits output into multiple files instead of one monolithic document:
|
||||
|
||||
- **Root file** (`-o output.md`): Full directory tree with cross-references to slice files, a vertical slices index table (file counts, total bytes per directory), and full contents of all root-level files.
|
||||
- **Slice files** (`cmd.md`, `internal.md`, etc.): One per top-level directory, placed alongside the root file. Each contains its own tree, a back-reference to the root document, and full recursive file contents for that directory.
|
||||
|
||||
This mode cannot be combined with `--append` or `--input-file`.
|
||||
|
||||
Example output with `dirmd -o docs.md ~/repos/myproject --vertical-slices`:
|
||||
|
||||
docs/
|
||||
├── docs.md ← root: tree + slices index + root file contents
|
||||
├── cmd.md ← slice: tree + contents of cmd/
|
||||
└── internal.md ← slice: tree + contents of internal/
|
||||
|
||||
## LLM-Optimized Output
|
||||
|
||||
Output is designed for RAG ingestion and LLM context windows, not human presentation:
|
||||
|
||||
- **ASCII tree format** (`+-` instead of Unicode box-drawing) to reduce token overhead
|
||||
- **Explicit file delimiters**: `--- FILE: ./path (bytes, lines) ---` and `--- END FILE ---`
|
||||
- **Document type markers**: `[ROOT]` and `[SLICE]` in H1 headers
|
||||
- **Cross-references**: Directory entries in root tree show `-> see cmd.md`; slice files reference their root document
|
||||
- **Slices index table**: File counts and total bytes per slice for informed retrieval decisions
|
||||
- **Tilde fences** (`~~~`) to avoid conflicts with backticks in source files
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Description | Default |
|
||||
@@ -45,6 +76,7 @@ Generate markdown documentation from directory structure.
|
||||
| `-a`, `--append` | Append to existing output file | false |
|
||||
| `-f`, `--force` | Overwrite existing output file (non-append mode) | false |
|
||||
| `-i`, `--input-file` | Single file input (append mode only) | |
|
||||
| `--vertical-slices` | Split output into root file + one markdown per top-level directory | false |
|
||||
| `--max-size` | Max file size in bytes | 524288 |
|
||||
| `--max-depth` | Max directory recursion depth | 20 |
|
||||
| `--ignore` | Additional ignore patterns (glob) | |
|
||||
@@ -54,7 +86,9 @@ Generate markdown documentation from directory structure.
|
||||
|
||||
## Proton Drive Integration
|
||||
|
||||
dirmd can optionally upload the generated markdown file to Proton Drive using the [Proton Drive CLI](https://proton.me/support/drive-cli). The upload is performed only when `--proton-drive` is passed. Without it, dirmd behaves exactly as before — no Proton Drive dependency is required.
|
||||
dirmd can optionally upload the generated markdown file(s) to Proton Drive using the [Proton Drive CLI](https://proton.me/support/drive-cli). The upload is performed only when `--proton-drive` is passed. Without it, dirmd behaves exactly as before — no Proton Drive dependency is required.
|
||||
|
||||
In vertical slices mode, all generated files (root + slices) are uploaded to `/my-files/md/`. The `--drive-path` flag is ignored in slices mode; each file is uploaded by its basename.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -62,14 +96,7 @@ dirmd can optionally upload the generated markdown file to Proton Drive using th
|
||||
2. Authenticate by running `proton-drive auth login`.
|
||||
3. Ensure `proton-drive` is in your `PATH`.
|
||||
|
||||
If the CLI is not installed or you are not logged in, dirmd will report the error to stderr and exit with a non-zero code. The local markdown file is still written successfully before the upload is attempted.
|
||||
|
||||
### Upload Behavior
|
||||
|
||||
- Default remote path: `/my-files/md/<filename>` where `<filename>` is the basename of the local output file.
|
||||
- Override with `--drive-path /my-files/custom/report.md` (full path including filename).
|
||||
- Existing remote files are overwritten (`--conflict-strategy replace`).
|
||||
- The local markdown file is always written before the upload is attempted.
|
||||
If the CLI is not installed or you are not logged in, dirmd will report the error to stderr and exit with a non-zero code. Local files are always written successfully before the upload is attempted.
|
||||
|
||||
For full documentation on the Proton Drive CLI, including installation, authentication, and troubleshooting, refer to the [official Proton Drive CLI support page](https://proton.me/support/drive-cli).
|
||||
|
||||
@@ -84,6 +111,9 @@ For full documentation on the Proton Drive CLI, including installation, authenti
|
||||
# Skip additional file extensions
|
||||
dirmd -o output.md ~/repos/project --extensions .log --extensions .tmp
|
||||
|
||||
# Vertical slices with overwrite
|
||||
dirmd -o docs.md ~/repos/project -f --vertical-slices
|
||||
|
||||
# Generate, then upload to default remote location
|
||||
dirmd -o docs.md ~/repos/project --proton-drive
|
||||
|
||||
@@ -97,31 +127,35 @@ For full documentation on the Proton Drive CLI, including installation, authenti
|
||||
|
||||
Produces markdown with:
|
||||
|
||||
1. Absolute path as H1 header
|
||||
2. Directory tree (indented with 4 spaces per level)
|
||||
3. All text files with content in code blocks
|
||||
4. Language detection from file extensions
|
||||
1. Absolute path as H1 header with `[ROOT]` or `[SLICE]` marker
|
||||
2. ASCII directory tree (indented with `+-` and `|` characters)
|
||||
3. Vertical slices index table (sliced mode only)
|
||||
4. All text files with content in fenced code blocks
|
||||
5. Explicit `--- FILE: ---` and `--- END FILE ---` delimiters with byte and line counts
|
||||
6. Language detection from file extensions
|
||||
|
||||
Example output structure:
|
||||
Example output structure (non-sliced mode):
|
||||
|
||||
# /home/user/repos/myproject
|
||||
# /home/user/repos/myproject [ROOT]
|
||||
|
||||
## Directory Tree
|
||||
|
||||
~~~
|
||||
myproject/
|
||||
├── cmd/
|
||||
│ └── main.go
|
||||
└── internal/
|
||||
└── config.go
|
||||
+- cmd/
|
||||
| +- main.go
|
||||
+- internal/
|
||||
| +- config.go
|
||||
~~~
|
||||
|
||||
## Contents
|
||||
|
||||
### ./cmd/main.go
|
||||
|
||||
```go
|
||||
--- FILE: ./cmd/main.go (45 bytes, 3 lines) ---
|
||||
~~~go
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
```
|
||||
~~~
|
||||
--- END FILE ---
|
||||
|
||||
Note: Uses tilde fences (~~~) internally to avoid conflicts with nested backticks in source files.
|
||||
|
||||
+51
-11
@@ -36,7 +36,10 @@ Examples:
|
||||
# Overwrite existing local file
|
||||
dirmd -o output.md ~/repos/myproject -f
|
||||
|
||||
# Generate and upload to Proton Drive (default remote path /my-files/md/<filename>)
|
||||
# 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
|
||||
@@ -54,6 +57,7 @@ Examples:
|
||||
extensions []string
|
||||
protonDrive bool
|
||||
drivePath string
|
||||
verticalSlices bool
|
||||
)
|
||||
|
||||
func Execute() {
|
||||
@@ -73,6 +77,7 @@ func init() {
|
||||
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")
|
||||
}
|
||||
@@ -82,16 +87,25 @@ func run(cmd *cobra.Command, args []string) error {
|
||||
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,
|
||||
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 != "" {
|
||||
@@ -113,7 +127,6 @@ func run(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
cfg.AbsRoot = abs
|
||||
|
||||
// Non-append mode: check for existing output file
|
||||
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)
|
||||
@@ -129,6 +142,33 @@ func run(cmd *cobra.Command, args []string) error {
|
||||
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
|
||||
}
|
||||
|
||||
+44
-32
@@ -53,18 +53,19 @@ func (s *stringSlice) Set(v string) error {
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
@@ -77,17 +78,18 @@ func PrintUsage() {
|
||||
|
||||
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
|
||||
input string
|
||||
output string
|
||||
appendMd bool
|
||||
force bool
|
||||
maxSize int64
|
||||
maxDepth int
|
||||
ignores stringSlice
|
||||
exts stringSlice
|
||||
showVer bool
|
||||
protonDrive bool
|
||||
drivePath string
|
||||
verticalSlices bool
|
||||
)
|
||||
|
||||
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
||||
@@ -101,6 +103,7 @@ func Parse() (*Config, error) {
|
||||
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.Usage = PrintUsage
|
||||
flag.Parse()
|
||||
@@ -118,16 +121,25 @@ func Parse() (*Config, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
OutputPath: output,
|
||||
AppendMode: appendMd,
|
||||
Force: force,
|
||||
MaxSize: maxSize,
|
||||
MaxDepth: maxDepth,
|
||||
Ignores: append(DefaultIgnores, ignores...),
|
||||
Extensions: append(DefaultExts, exts...),
|
||||
ProtonDrive: protonDrive,
|
||||
DrivePath: drivePath,
|
||||
OutputPath: output,
|
||||
AppendMode: appendMd,
|
||||
Force: force,
|
||||
MaxSize: maxSize,
|
||||
MaxDepth: maxDepth,
|
||||
Ignores: append(DefaultIgnores, ignores...),
|
||||
Extensions: append(DefaultExts, exts...),
|
||||
ProtonDrive: protonDrive,
|
||||
DrivePath: drivePath,
|
||||
VerticalSlices: verticalSlices,
|
||||
}
|
||||
|
||||
if input != "" {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
const fenceChars = "~~~"
|
||||
|
||||
// Write writes a single markdown file with tree and file contents.
|
||||
func Write(result *walker.Result, outputPath string, appendMode bool, singleFile bool) error {
|
||||
flags := os.O_CREATE | os.O_WRONLY
|
||||
if appendMode {
|
||||
@@ -29,20 +31,19 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Determine the correct H1 path
|
||||
h1Path := result.AbsRoot
|
||||
if singleFile && len(result.Entries) > 0 {
|
||||
h1Path = filepath.Join(result.AbsRoot, result.Entries[0].RelPath)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, "# %s\n\n", h1Path)
|
||||
fmt.Fprintf(&sb, "# %s [ROOT]\n\n", h1Path)
|
||||
|
||||
if !singleFile {
|
||||
root := tree.Build(result.Entries)
|
||||
rootName := filepath.Base(result.AbsRoot)
|
||||
sb.WriteString("## Directory Tree\n\n")
|
||||
sb.WriteString(fenceChars + "\n")
|
||||
sb.WriteString(tree.Render(root, rootName))
|
||||
sb.WriteString(renderTreeForLLM(root, rootName))
|
||||
sb.WriteString(fenceChars + "\n\n")
|
||||
}
|
||||
|
||||
@@ -61,8 +62,9 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
|
||||
}
|
||||
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
|
||||
fmt.Fprintf(&sb, "### ./%s\n\n", entry.RelPath)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
@@ -72,7 +74,7 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s\n\n", fenceChars)
|
||||
fmt.Fprintf(&sb, "%s\n--- END FILE ---\n\n", fenceChars)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +82,337 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteSlices generates a root markdown file plus one markdown per top-level directory.
|
||||
// Returns the list of generated file paths.
|
||||
func WriteSlices(result *walker.Result, outputPath string) ([]string, error) {
|
||||
entries := result.Entries
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("no entries to write")
|
||||
}
|
||||
|
||||
absRoot := result.AbsRoot
|
||||
rootName := filepath.Base(absRoot)
|
||||
|
||||
topLevelDirs := make(map[string][]walker.Entry)
|
||||
rootFiles := make([]walker.Entry, 0)
|
||||
|
||||
for _, e := range entries {
|
||||
rel := e.RelPath
|
||||
parts := strings.Split(rel, string(filepath.Separator))
|
||||
if len(parts) == 1 {
|
||||
rootFiles = append(rootFiles, e)
|
||||
} else {
|
||||
firstPart := parts[0]
|
||||
topLevelDirs[firstPart] = append(topLevelDirs[firstPart], e)
|
||||
}
|
||||
}
|
||||
|
||||
outDir := filepath.Dir(outputPath)
|
||||
|
||||
var generated []string
|
||||
|
||||
// Write root file
|
||||
rootPath := outputPath
|
||||
generated = append(generated, rootPath)
|
||||
|
||||
if err := writeRootSlice(rootPath, absRoot, rootName, rootFiles, topLevelDirs, outDir); err != nil {
|
||||
return nil, fmt.Errorf("failed to write root slice: %w", err)
|
||||
}
|
||||
|
||||
// Write each top-level directory slice into a mirroring subdirectory
|
||||
var dirNames []string
|
||||
for k := range topLevelDirs {
|
||||
dirNames = append(dirNames, k)
|
||||
}
|
||||
sort.Strings(dirNames)
|
||||
|
||||
for _, dirName := range dirNames {
|
||||
sliceName := strings.TrimSuffix(dirName, "/") + ".md"
|
||||
sliceDir := filepath.Join(outDir, dirName)
|
||||
if err := os.MkdirAll(sliceDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("cannot create slice directory %s: %w", sliceDir, err)
|
||||
}
|
||||
slicePath := filepath.Join(sliceDir, sliceName)
|
||||
generated = append(generated, slicePath)
|
||||
|
||||
absDir := filepath.Join(absRoot, dirName)
|
||||
dirEntries := topLevelDirs[dirName]
|
||||
|
||||
// Strip dirName prefix for tree building only
|
||||
treeEntries := make([]walker.Entry, len(dirEntries))
|
||||
for i, e := range dirEntries {
|
||||
treeEntries[i] = walker.Entry{
|
||||
RelPath: strings.TrimPrefix(e.RelPath, dirName+string(filepath.Separator)),
|
||||
Size: e.Size,
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, filepath.Base(outputPath)); err != nil {
|
||||
return nil, fmt.Errorf("failed to write slice %s: %w", sliceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return generated, nil
|
||||
}
|
||||
|
||||
func writeRootSlice(path, absRoot, rootName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, outDir string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create root file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "# %s [ROOT]\n\n", absRoot)
|
||||
|
||||
// Build combined tree with references
|
||||
treeLines := buildTreeWithRefs(rootName, topLevelDirs, rootFiles)
|
||||
sb.WriteString("## Directory Tree\n\n")
|
||||
sb.WriteString(fenceChars + "\n")
|
||||
sb.WriteString(treeLines)
|
||||
sb.WriteString(fenceChars + "\n\n")
|
||||
|
||||
// Vertical slices index for LLM navigation
|
||||
sb.WriteString("## Vertical Slices Index\n\n")
|
||||
sb.WriteString("These files contain the full contents of each top-level directory.\n")
|
||||
sb.WriteString("Read the relevant slice file for detailed source code.\n\n")
|
||||
sb.WriteString("| Slice File | Directory | Files | Total Bytes |\n")
|
||||
sb.WriteString("|------------|-----------|-------|-------------|\n")
|
||||
|
||||
var dirNames []string
|
||||
for k := range topLevelDirs {
|
||||
dirNames = append(dirNames, k)
|
||||
}
|
||||
sort.Strings(dirNames)
|
||||
|
||||
for _, dirName := range dirNames {
|
||||
entries := topLevelDirs[dirName]
|
||||
fileCount := len(entries)
|
||||
totalBytes := int64(0)
|
||||
for _, e := range entries {
|
||||
totalBytes += e.Size
|
||||
}
|
||||
sliceFile := filepath.Join(dirName, strings.TrimSuffix(dirName, "/")+".md")
|
||||
fmt.Fprintf(&sb, "| %s | %s/ | %d | %d |\n", sliceFile, dirName, fileCount, totalBytes)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
if len(rootFiles) > 0 {
|
||||
sb.WriteString("## Root Contents\n\n")
|
||||
|
||||
sort.Slice(rootFiles, func(i, j int) bool {
|
||||
return rootFiles[i].RelPath < rootFiles[j].RelPath
|
||||
})
|
||||
|
||||
for _, entry := range rootFiles {
|
||||
fullPath := filepath.Join(absRoot, entry.RelPath)
|
||||
content, err := readFile(fullPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
fmt.Fprintf(&sb, "%s", content)
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s\n--- END FILE ---\n\n", fenceChars)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = f.WriteString(sb.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create slice file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "# %s [SLICE]\n\n", absDir)
|
||||
fmt.Fprintf(&sb, "Root document: %s\n\n", rootFileName)
|
||||
|
||||
sb.WriteString("## Directory Tree\n\n")
|
||||
sb.WriteString(fenceChars + "\n")
|
||||
|
||||
sliceTree := buildTreeFromEntries(dirName, treeEntries)
|
||||
sb.WriteString(sliceTree)
|
||||
sb.WriteString(fenceChars + "\n\n")
|
||||
|
||||
if len(fileEntries) > 0 {
|
||||
sb.WriteString("## Contents\n\n")
|
||||
|
||||
sort.Slice(fileEntries, func(i, j int) bool {
|
||||
return fileEntries[i].RelPath < fileEntries[j].RelPath
|
||||
})
|
||||
|
||||
for _, entry := range fileEntries {
|
||||
fullPath := filepath.Join(absRoot, entry.RelPath)
|
||||
content, err := readFile(fullPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
fmt.Fprintf(&sb, "%s", content)
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s\n--- END FILE ---\n\n", fenceChars)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = f.WriteString(sb.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func buildTreeWithRefs(rootName string, topLevelDirs map[string][]walker.Entry, rootFiles []walker.Entry) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(rootName)
|
||||
sb.WriteString("/\n")
|
||||
|
||||
// Collect and sort directory names
|
||||
var dirNames []string
|
||||
for k := range topLevelDirs {
|
||||
dirNames = append(dirNames, k)
|
||||
}
|
||||
sort.Strings(dirNames)
|
||||
|
||||
// Sort root files
|
||||
sort.Slice(rootFiles, func(i, j int) bool {
|
||||
return rootFiles[i].RelPath < rootFiles[j].RelPath
|
||||
})
|
||||
|
||||
// Merge directories and files into a single sorted list for display
|
||||
type treeItem struct {
|
||||
name string
|
||||
isDir bool
|
||||
sliceRef string
|
||||
}
|
||||
|
||||
items := make([]treeItem, 0, len(dirNames)+len(rootFiles))
|
||||
for _, d := range dirNames {
|
||||
items = append(items, treeItem{
|
||||
name: d,
|
||||
isDir: true,
|
||||
sliceRef: filepath.Join(d, strings.TrimSuffix(d, "/")+".md"),
|
||||
})
|
||||
}
|
||||
for _, rf := range rootFiles {
|
||||
items = append(items, treeItem{
|
||||
name: rf.RelPath,
|
||||
isDir: false,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].name < items[j].name
|
||||
})
|
||||
|
||||
for _, item := range items {
|
||||
sb.WriteString("+- ")
|
||||
sb.WriteString(item.name)
|
||||
if item.isDir {
|
||||
sb.WriteString("/")
|
||||
sb.WriteString(" -> see ")
|
||||
sb.WriteString(item.sliceRef)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func buildTreeFromEntries(rootName string, entries []walker.Entry) string {
|
||||
root := &tree.Node{IsDir: true, Children: make(map[string]*tree.Node)}
|
||||
|
||||
for _, e := range entries {
|
||||
parts := strings.Split(e.RelPath, string(filepath.Separator))
|
||||
cur := root
|
||||
|
||||
for i, part := range parts {
|
||||
isLeaf := i == len(parts)-1
|
||||
if _, ok := cur.Children[part]; !ok {
|
||||
node := &tree.Node{Name: part, IsDir: !isLeaf}
|
||||
if !isLeaf {
|
||||
node.Children = make(map[string]*tree.Node)
|
||||
}
|
||||
cur.Children[part] = node
|
||||
}
|
||||
cur = cur.Children[part]
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(rootName)
|
||||
sb.WriteString("/\n")
|
||||
renderNodeLLM(&sb, root, "")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func renderTreeForLLM(root *tree.Node, rootName string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(rootName)
|
||||
sb.WriteString("/\n")
|
||||
renderNodeLLM(&sb, root, "")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func renderNodeLLM(sb *strings.Builder, node *tree.Node, prefix string) {
|
||||
keys := make([]string, 0, len(node.Children))
|
||||
for k := range node.Children {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i, key := range keys {
|
||||
child := node.Children[key]
|
||||
isLast := i == len(keys)-1
|
||||
|
||||
sb.WriteString(prefix)
|
||||
sb.WriteString("+- ")
|
||||
|
||||
sb.WriteString(child.Name)
|
||||
if child.IsDir {
|
||||
sb.WriteString("/")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
if child.IsDir && child.Children != nil {
|
||||
newPrefix := prefix
|
||||
if isLast {
|
||||
newPrefix += " "
|
||||
} else {
|
||||
newPrefix += "| "
|
||||
}
|
||||
renderNodeLLM(sb, child, newPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -131,3 +464,14 @@ func extToLang(ext string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func countLines(content string) int {
|
||||
if content == "" {
|
||||
return 0
|
||||
}
|
||||
count := strings.Count(content, "\n")
|
||||
if len(content) > 0 && content[len(content)-1] != '\n' {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user