add .dirmd and other metadata

This commit is contained in:
2026-09-02 12:01:42 +01:00
parent 93d830d500
commit a1638bd944
8 changed files with 213 additions and 180 deletions
+1
View File
@@ -1,3 +1,4 @@
bin/ bin/
.env .env
vendor/ vendor/
.dirmd
+20
View File
@@ -138,6 +138,26 @@ Example output with `dirmd -o docs.md ~/repos/myproject --vertical-slices`:
├── cmd.md ← slice: tree + contents of cmd/ ├── cmd.md ← slice: tree + contents of cmd/
└── internal.md ← slice: tree + contents of internal/ └── internal.md ← slice: tree + contents of internal/
## Instructions File (`.dirmd`)
If a file named `.dirmd` exists in the root of the input directory, you can include its contents as metadata in the generated output by passing the `--instructions` flag.
This is useful for injecting specific context, constraints, or instructions for LLMs processing the snapshot.
- **File Name**: `.dirmd`
- **Location**: Root of the input directory.
- **Format**: Plain text or Markdown.
- **Output**: Rendered as a quoted block (`> Instructions: ...`) immediately after the timestamp and commit info.
- **Opt-in**: The file is **ignored** unless `--instructions` is explicitly passed.
Example `.dirmd`:
```text
Focus on the `internal/` package for business logic.
Ignore `cmd/` unless explicitly asked.
Prioritize Go files over Python.
```
## LLM-Optimized Output ## LLM-Optimized Output
Output is designed for RAG ingestion and LLM context windows, not human presentation: Output is designed for RAG ingestion and LLM context windows, not human presentation:
+8 -25
View File
@@ -27,33 +27,24 @@ Output is always written to a directory specified by -o.
Examples: Examples:
# Create new documentation (writes index.md inside ./docs/) # Create new documentation
dirmd -o ./docs ~/repos/myproject dirmd -o ./docs ~/repos/myproject
# Overwrite existing output directory (DELETES everything in it first) # Overwrite existing output directory (deletes all contents)
dirmd -o ./docs ~/repos/myproject -f dirmd -o ./docs ~/repos/myproject -f
# Append a single file # Append a single file
dirmd -a -i README.md -o ./docs dirmd -a -i README.md -o ./docs
# Vertical slices (root index.md + one markdown per top-level dir) # Vertical slices
dirmd -o ./docs ~/repos/myproject --vertical-slices dirmd -o ./docs ~/repos/myproject --vertical-slices
# Skip frontend files # Include instructions from .dirmd
dirmd -o ./docs ~/repos/myproject --skip-frontend dirmd -o ./docs ~/repos/myproject --instructions
# Frontend only
dirmd -o ./docs ~/repos/myproject --frontend-only
# Omit test files and test blocks
dirmd -o ./docs ~/repos/myproject --omit-test
# Generate and upload to Proton Drive # Generate and upload to Proton Drive
dirmd -o ./docs ~/repos/myproject --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
# Interactive TUI mode # Interactive TUI mode
dirmd --tui`, dirmd --tui`,
RunE: run, RunE: run,
@@ -75,6 +66,7 @@ Examples:
omitTest bool omitTest bool
omitMd bool omitMd bool
tuiMode bool tuiMode bool
instructions bool
) )
func Execute() { func Execute() {
@@ -100,6 +92,7 @@ func init() {
rootCmd.Flags().BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md") rootCmd.Flags().BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
rootCmd.Flags().BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust") rootCmd.Flags().BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust")
rootCmd.Flags().BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md") rootCmd.Flags().BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md")
rootCmd.Flags().BoolVar(&instructions, "instructions", false, "include .dirmd instructions file if present in root")
} }
func run(cmd *cobra.Command, args []string) error { func run(cmd *cobra.Command, args []string) error {
@@ -147,6 +140,7 @@ func run(cmd *cobra.Command, args []string) error {
FrontendOnly: frontendOnly, FrontendOnly: frontendOnly,
OmitTest: omitTest, OmitTest: omitTest,
OmitMd: omitMd, OmitMd: omitMd,
IncludeInstructions: instructions,
} }
if singleFile != "" { if singleFile != "" {
@@ -173,13 +167,11 @@ func run(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to resolve output path: %w", err) return fmt.Errorf("failed to resolve output path: %w", err)
} }
// Handle output directory existence
if !appendMode { if !appendMode {
if _, err := os.Stat(absOutput); err == nil { if _, err := os.Stat(absOutput); err == nil {
if !force { if !force {
return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput) return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput)
} }
// FORCE MODE: Always delete the entire directory and recreate it fresh
if err := os.RemoveAll(absOutput); err != nil { if err := os.RemoveAll(absOutput); err != nil {
return fmt.Errorf("failed to remove existing output directory: %w", err) return fmt.Errorf("failed to remove existing output directory: %w", err)
} }
@@ -209,7 +201,6 @@ func run(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintf(os.Stderr, "generated %d files in %s\n", len(generated), absOutput) fmt.Fprintf(os.Stderr, "generated %d files in %s\n", len(generated), absOutput)
if protonDrive { if protonDrive {
@@ -217,27 +208,22 @@ func run(cmd *cobra.Command, args []string) error {
if remoteDir == "" { if remoteDir == "" {
remoteDir = "/my-files/md/" + filepath.Base(absOutput) remoteDir = "/my-files/md/" + filepath.Base(absOutput)
} }
if err := drive.CheckAuth(); err != nil { if err := drive.CheckAuth(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
fmt.Fprintln(os.Stderr, "local files were written successfully, but upload was skipped") fmt.Fprintln(os.Stderr, "local files were written successfully, but upload was skipped")
return err return err
} }
if err := drive.UploadDir(absOutput, remoteDir); err != nil { if err := drive.UploadDir(absOutput, remoteDir); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
fmt.Fprintln(os.Stderr, "local files were written successfully, but upload failed") fmt.Fprintln(os.Stderr, "local files were written successfully, but upload failed")
return err return err
} }
fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", absOutput, remoteDir) fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", absOutput, remoteDir)
} }
return nil return nil
} }
indexPath := filepath.Join(absOutput, indexBasename) indexPath := filepath.Join(absOutput, indexBasename)
if err := renderer.Write(result, indexPath, appendMode, cfg.SingleFile); err != nil { if err := renderer.Write(result, indexPath, appendMode, cfg.SingleFile); err != nil {
return err return err
} }
@@ -249,19 +235,16 @@ func run(cmd *cobra.Command, args []string) error {
if remoteDir == "" { if remoteDir == "" {
remoteDir = "/my-files/md/" + filepath.Base(absOutput) remoteDir = "/my-files/md/" + filepath.Base(absOutput)
} }
if err := drive.CheckAuth(); err != nil { if err := drive.CheckAuth(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
fmt.Fprintln(os.Stderr, "local file was written successfully, but upload was skipped") fmt.Fprintln(os.Stderr, "local file was written successfully, but upload was skipped")
return err return err
} }
if err := drive.UploadDir(absOutput, remoteDir); err != nil { if err := drive.UploadDir(absOutput, remoteDir); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
fmt.Fprintln(os.Stderr, "local file was written successfully, but upload failed") fmt.Fprintln(os.Stderr, "local file was written successfully, but upload failed")
return err return err
} }
fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", absOutput, remoteDir) fmt.Fprintf(os.Stderr, "uploaded %s to %s on Proton Drive\n", absOutput, remoteDir)
} }
+12 -34
View File
@@ -15,22 +15,9 @@ const (
) )
var DefaultIgnores = []string{ var DefaultIgnores = []string{
".git", ".git", "node_modules", "vendor", "bin", "go.mod", "go.sum",
"node_modules", "target", "build", "dist", "Cargo.lock", ".idea", ".vscode",
"vendor", "dependency-reduced-pom.xml", "__pycache__", "venv", "env",
"bin",
"go.mod",
"go.sum",
"target",
"build",
"dist",
"Cargo.lock",
".idea",
".vscode",
"dependency-reduced-pom.xml",
"__pycache__",
"venv",
"env",
} }
var DefaultExts = []string{ var DefaultExts = []string{
@@ -42,10 +29,7 @@ var DefaultExts = []string{
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".pdf", ".doc", ".docx", ".xls", ".xlsx",
".pem", ".key", ".cert", ".pem", ".key", ".cert",
".db", ".sqlite", ".sqlite3", ".db", ".sqlite", ".sqlite3",
".class", ".class", ".jar", ".war", ".ear",
".jar",
".war",
".ear",
".pyc", ".pyo", ".pyc", ".pyo",
} }
@@ -59,14 +43,8 @@ var FrontendExts = []string{
type stringSlice []string type stringSlice []string
func (s *stringSlice) String() string { func (s *stringSlice) String() string { return strings.Join(*s, ", ") }
return strings.Join(*s, ", ") func (s *stringSlice) Set(v string) error { *s = append(*s, v); return nil }
}
func (s *stringSlice) Set(v string) error {
*s = append(*s, v)
return nil
}
type Config struct { type Config struct {
InputPath string InputPath string
@@ -86,6 +64,7 @@ type Config struct {
FrontendOnly bool FrontendOnly bool
OmitTest bool OmitTest bool
OmitMd bool OmitMd bool
IncludeInstructions bool
} }
func PrintUsage() { func PrintUsage() {
@@ -114,6 +93,7 @@ func Parse() (*Config, error) {
frontendOnly bool frontendOnly bool
omitTest bool omitTest bool
omitMd bool omitMd bool
includeInstructions bool
) )
flag.StringVar(&input, "i", "", "single file input (append mode only)") flag.StringVar(&input, "i", "", "single file input (append mode only)")
@@ -128,10 +108,11 @@ func Parse() (*Config, error) {
flag.BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally") 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.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(&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 (.html, .css, .js, .ts, .vue, .svelte, etc.)") 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(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
flag.BoolVar(&omitTest, "omit-test", false, "exclude test files (Go *_test.go, Java *Test.java) and strip #[cfg(test)] blocks from Rust") 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(&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.Usage = PrintUsage flag.Usage = PrintUsage
flag.Parse() flag.Parse()
@@ -144,19 +125,15 @@ func Parse() (*Config, error) {
if output == "" { if output == "" {
return nil, fmt.Errorf("-o is required") return nil, fmt.Errorf("-o is required")
} }
if drivePath != "" && !protonDrive { if drivePath != "" && !protonDrive {
return nil, fmt.Errorf("--drive-path requires --proton-drive") return nil, fmt.Errorf("--drive-path requires --proton-drive")
} }
if verticalSlices && appendMd { if verticalSlices && appendMd {
return nil, fmt.Errorf("--vertical-slices cannot be used with --append") return nil, fmt.Errorf("--vertical-slices cannot be used with --append")
} }
if verticalSlices && input != "" { if verticalSlices && input != "" {
return nil, fmt.Errorf("--vertical-slices cannot be used with --input-file") return nil, fmt.Errorf("--vertical-slices cannot be used with --input-file")
} }
if skipFrontend && frontendOnly { if skipFrontend && frontendOnly {
return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
} }
@@ -181,6 +158,7 @@ func Parse() (*Config, error) {
FrontendOnly: frontendOnly, FrontendOnly: frontendOnly,
OmitTest: omitTest, OmitTest: omitTest,
OmitMd: omitMd, OmitMd: omitMd,
IncludeInstructions: includeInstructions,
} }
if input != "" { if input != "" {
+41
View File
@@ -0,0 +1,41 @@
package instructions
import (
"os"
"path/filepath"
"strings"
)
const FileName = ".dirmd"
func Read(rootDir string) (string, error) {
path := filepath.Join(rootDir, FileName)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(data), nil
}
func Format(content string) string {
if content == "" {
return ""
}
content = strings.TrimSpace(content)
if content == "" {
return ""
}
lines := strings.Split(content, "\n")
var sb strings.Builder
sb.WriteString("> Instructions:\n")
for _, line := range lines {
sb.WriteString("> ")
sb.WriteString(line)
sb.WriteString("\n")
}
sb.WriteString("\n")
return sb.String()
}
+24 -31
View File
@@ -10,6 +10,7 @@ import (
"unicode/utf8" "unicode/utf8"
"git.flo.fo/FLO/dirmd/internal/git" "git.flo.fo/FLO/dirmd/internal/git"
"git.flo.fo/FLO/dirmd/internal/instructions"
"git.flo.fo/FLO/dirmd/internal/tree" "git.flo.fo/FLO/dirmd/internal/tree"
"git.flo.fo/FLO/dirmd/internal/walker" "git.flo.fo/FLO/dirmd/internal/walker"
) )
@@ -42,8 +43,13 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
if !appendMode { if !appendMode {
fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
sb.WriteString(git.FormatCommitLine(result.Commit)) sb.WriteString(git.FormatCommitLine(result.Commit))
if result.Instructions != "" {
sb.WriteString(instructions.Format(result.Instructions))
} else {
sb.WriteString("\n") sb.WriteString("\n")
} }
}
if !singleFile { if !singleFile {
root := tree.Build(result.Entries) root := tree.Build(result.Entries)
@@ -61,7 +67,6 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
for _, entry := range result.Entries { for _, entry := range result.Entries {
fullPath := filepath.Join(result.AbsRoot, entry.RelPath) fullPath := filepath.Join(result.AbsRoot, entry.RelPath)
content, err := readFileForOutput(fullPath, result.OmitTest) content, err := readFileForOutput(fullPath, result.OmitTest)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err) fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err)
@@ -113,12 +118,11 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
} }
var generated []string var generated []string
indexBasename := "index_" + repoName + ".md" indexBasename := "index_" + repoName + ".md"
rootPath := filepath.Join(outputDir, indexBasename) rootPath := filepath.Join(outputDir, indexBasename)
generated = append(generated, rootPath) generated = append(generated, rootPath)
if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest, result.Commit); err != nil { if err := writeRootSlice(rootPath, absRoot, rootName, repoName, rootFiles, topLevelDirs, result.OmitTest, result.Commit, result.Instructions); err != nil {
return nil, fmt.Errorf("failed to write root slice: %w", err) return nil, fmt.Errorf("failed to write root slice: %w", err)
} }
@@ -144,16 +148,13 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
for i, e := range dirEntries { for i, e := range dirEntries {
sepIdx := strings.Index(e.RelPath, string(filepath.Separator)) sepIdx := strings.Index(e.RelPath, string(filepath.Separator))
if sepIdx >= 0 { if sepIdx >= 0 {
treeEntries[i] = walker.Entry{ treeEntries[i] = walker.Entry{RelPath: e.RelPath[sepIdx+1:], Size: e.Size}
RelPath: e.RelPath[sepIdx+1:],
Size: e.Size,
}
} else { } else {
treeEntries[i] = e treeEntries[i] = e
} }
} }
if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, indexBasename, result.OmitTest, result.Commit); err != nil { if err := writeDirSlice(slicePath, absDir, absRoot, dirName, treeEntries, dirEntries, indexBasename, result.OmitTest, result.Commit, result.Instructions); err != nil {
return nil, fmt.Errorf("failed to write slice %s: %w", sliceName, err) return nil, fmt.Errorf("failed to write slice %s: %w", sliceName, err)
} }
} }
@@ -161,7 +162,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
return generated, nil return generated, nil
} }
func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool, commit *git.CommitInfo) error { func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker.Entry, topLevelDirs map[string][]walker.Entry, omitTest bool, commit *git.CommitInfo, instContent string) error {
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
return fmt.Errorf("cannot create root file: %w", err) return fmt.Errorf("cannot create root file: %w", err)
@@ -169,11 +170,14 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
defer f.Close() defer f.Close()
var sb strings.Builder var sb strings.Builder
fmt.Fprintf(&sb, "# %s [ROOT]\n", absRoot) fmt.Fprintf(&sb, "# %s [ROOT]\n", absRoot)
fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
sb.WriteString(git.FormatCommitLine(commit)) sb.WriteString(git.FormatCommitLine(commit))
if instContent != "" {
sb.WriteString(instructions.Format(instContent))
} else {
sb.WriteString("\n") sb.WriteString("\n")
}
treeLines := buildTreeWithRefs(rootName, repoName, topLevelDirs, rootFiles) treeLines := buildTreeWithRefs(rootName, repoName, topLevelDirs, rootFiles)
sb.WriteString("## Directory Tree\n\n") sb.WriteString("## Directory Tree\n\n")
@@ -207,11 +211,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
if len(rootFiles) > 0 { if len(rootFiles) > 0 {
sb.WriteString("## Root Contents\n\n") sb.WriteString("## Root Contents\n\n")
sort.Slice(rootFiles, func(i, j int) bool { return rootFiles[i].RelPath < rootFiles[j].RelPath })
sort.Slice(rootFiles, func(i, j int) bool {
return rootFiles[i].RelPath < rootFiles[j].RelPath
})
for _, entry := range rootFiles { for _, entry := range rootFiles {
fullPath := filepath.Join(absRoot, entry.RelPath) fullPath := filepath.Join(absRoot, entry.RelPath)
content, err := readFileForOutput(fullPath, omitTest) content, err := readFileForOutput(fullPath, omitTest)
@@ -219,17 +219,14 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err) fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err)
continue continue
} }
language := extToLang(filepath.Ext(entry.RelPath)) language := extToLang(filepath.Ext(entry.RelPath))
lines := countLines(content) lines := countLines(content)
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines) fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
fmt.Fprintf(&sb, "%s", fenceChars) fmt.Fprintf(&sb, "%s", fenceChars)
if language != "" { if language != "" {
fmt.Fprintf(&sb, "%s", language) fmt.Fprintf(&sb, "%s", language)
} }
fmt.Fprintf(&sb, "\n") fmt.Fprintf(&sb, "\n%s", content)
fmt.Fprintf(&sb, "%s", content)
if !strings.HasSuffix(content, "\n") { if !strings.HasSuffix(content, "\n") {
fmt.Fprintf(&sb, "\n") fmt.Fprintf(&sb, "\n")
} }
@@ -241,7 +238,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
return err return err
} }
func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string, omitTest bool, commit *git.CommitInfo) error { func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntries []walker.Entry, rootFileName string, omitTest bool, commit *git.CommitInfo, instContent string) error {
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
return fmt.Errorf("cannot create slice file: %w", err) return fmt.Errorf("cannot create slice file: %w", err)
@@ -249,26 +246,25 @@ func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntri
defer f.Close() defer f.Close()
var sb strings.Builder var sb strings.Builder
fmt.Fprintf(&sb, "# %s [SLICE]\n", absDir) fmt.Fprintf(&sb, "# %s [SLICE]\n", absDir)
fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
sb.WriteString(git.FormatCommitLine(commit)) sb.WriteString(git.FormatCommitLine(commit))
if instContent != "" {
sb.WriteString(instructions.Format(instContent))
} else {
sb.WriteString("\n")
}
fmt.Fprintf(&sb, "Root document: %s\n\n", rootFileName) fmt.Fprintf(&sb, "Root document: %s\n\n", rootFileName)
sb.WriteString("## Directory Tree\n\n") sb.WriteString("## Directory Tree\n\n")
sb.WriteString(fenceChars + "\n") sb.WriteString(fenceChars + "\n")
sliceTree := buildTreeFromEntries(dirName, treeEntries) sliceTree := buildTreeFromEntries(dirName, treeEntries)
sb.WriteString(sliceTree) sb.WriteString(sliceTree)
sb.WriteString(fenceChars + "\n\n") sb.WriteString(fenceChars + "\n\n")
if len(fileEntries) > 0 { if len(fileEntries) > 0 {
sb.WriteString("## Contents\n\n") sb.WriteString("## Contents\n\n")
sort.Slice(fileEntries, func(i, j int) bool { return fileEntries[i].RelPath < fileEntries[j].RelPath })
sort.Slice(fileEntries, func(i, j int) bool {
return fileEntries[i].RelPath < fileEntries[j].RelPath
})
for _, entry := range fileEntries { for _, entry := range fileEntries {
fullPath := filepath.Join(absRoot, entry.RelPath) fullPath := filepath.Join(absRoot, entry.RelPath)
content, err := readFileForOutput(fullPath, omitTest) content, err := readFileForOutput(fullPath, omitTest)
@@ -276,17 +272,14 @@ func writeDirSlice(path, absDir, absRoot, dirName string, treeEntries, fileEntri
fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err) fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err)
continue continue
} }
language := extToLang(filepath.Ext(entry.RelPath)) language := extToLang(filepath.Ext(entry.RelPath))
lines := countLines(content) lines := countLines(content)
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines) fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
fmt.Fprintf(&sb, "%s", fenceChars) fmt.Fprintf(&sb, "%s", fenceChars)
if language != "" { if language != "" {
fmt.Fprintf(&sb, "%s", language) fmt.Fprintf(&sb, "%s", language)
} }
fmt.Fprintf(&sb, "\n") fmt.Fprintf(&sb, "\n%s", content)
fmt.Fprintf(&sb, "%s", content)
if !strings.HasSuffix(content, "\n") { if !strings.HasSuffix(content, "\n") {
fmt.Fprintf(&sb, "\n") fmt.Fprintf(&sb, "\n")
} }
+3
View File
@@ -79,6 +79,7 @@ func Run() error {
huh.NewOption("Omit test files", "omit-test"), huh.NewOption("Omit test files", "omit-test"),
huh.NewOption("Omit non-README markdown", "omit-md"), huh.NewOption("Omit non-README markdown", "omit-md"),
huh.NewOption("Upload to Proton Drive", "proton-drive"), huh.NewOption("Upload to Proton Drive", "proton-drive"),
huh.NewOption("Include .dirmd instructions", "instructions"),
). ).
Value(&options). Value(&options).
Height(10), Height(10),
@@ -115,6 +116,7 @@ func Run() error {
omitTest := containsOption(options, "omit-test") omitTest := containsOption(options, "omit-test")
omitMd := containsOption(options, "omit-md") omitMd := containsOption(options, "omit-md")
protonDrive := containsOption(options, "proton-drive") protonDrive := containsOption(options, "proton-drive")
includeInstructions := containsOption(options, "instructions")
if skipFrontend && frontendOnly { if skipFrontend && frontendOnly {
return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive") return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
@@ -169,6 +171,7 @@ func Run() error {
FrontendOnly: frontendOnly, FrontendOnly: frontendOnly,
OmitTest: omitTest, OmitTest: omitTest,
OmitMd: omitMd, OmitMd: omitMd,
IncludeInstructions: includeInstructions,
} }
cmd := buildCommand(cfg) cmd := buildCommand(cfg)
+16 -2
View File
@@ -10,6 +10,7 @@ import (
"git.flo.fo/FLO/dirmd/internal/config" "git.flo.fo/FLO/dirmd/internal/config"
"git.flo.fo/FLO/dirmd/internal/filter" "git.flo.fo/FLO/dirmd/internal/filter"
"git.flo.fo/FLO/dirmd/internal/git" "git.flo.fo/FLO/dirmd/internal/git"
"git.flo.fo/FLO/dirmd/internal/instructions"
) )
type Entry struct { type Entry struct {
@@ -23,6 +24,7 @@ type Result struct {
SingleFile bool SingleFile bool
OmitTest bool OmitTest bool
Commit *git.CommitInfo Commit *git.CommitInfo
Instructions string
} }
func Run(cfg *config.Config) (*Result, error) { func Run(cfg *config.Config) (*Result, error) {
@@ -55,8 +57,11 @@ func processSingleFile(cfg *config.Config) (*Result, error) {
return nil, fmt.Errorf("file %s filtered out", cfg.AbsRoot) return nil, fmt.Errorf("file %s filtered out", cfg.AbsRoot)
} }
// For single files, try to get git info from the parent directory
commitInfo := git.GetCommitInfo(filepath.Dir(cfg.AbsRoot)) commitInfo := git.GetCommitInfo(filepath.Dir(cfg.AbsRoot))
var instContent string
if cfg.IncludeInstructions {
instContent, _ = instructions.Read(filepath.Dir(cfg.AbsRoot))
}
return &Result{ return &Result{
Entries: []Entry{{RelPath: filepath.Base(cfg.AbsRoot), Size: info.Size()}}, Entries: []Entry{{RelPath: filepath.Base(cfg.AbsRoot), Size: info.Size()}},
@@ -64,6 +69,7 @@ func processSingleFile(cfg *config.Config) (*Result, error) {
SingleFile: true, SingleFile: true,
OmitTest: cfg.OmitTest, OmitTest: cfg.OmitTest,
Commit: commitInfo, Commit: commitInfo,
Instructions: instContent,
}, nil }, nil
} }
@@ -79,9 +85,17 @@ func walkDirectory(cfg *config.Config) (*Result, error) {
result := &Result{AbsRoot: cfg.AbsRoot, OmitTest: cfg.OmitTest} result := &Result{AbsRoot: cfg.AbsRoot, OmitTest: cfg.OmitTest}
// Fetch git info for the root directory
result.Commit = git.GetCommitInfo(cfg.AbsRoot) result.Commit = git.GetCommitInfo(cfg.AbsRoot)
if cfg.IncludeInstructions {
instContent, err := instructions.Read(cfg.AbsRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "warning: could not read .dirmd: %v\n", err)
} else {
result.Instructions = instContent
}
}
err := filepath.WalkDir(cfg.AbsRoot, func(path string, d fs.DirEntry, err error) error { err := filepath.WalkDir(cfg.AbsRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err) fmt.Fprintf(os.Stderr, "warning: %v\n", err)