5 Commits
Author SHA1 Message Date
bartal-lsn f908bffc41 ignore uv.lock 2026-09-03 17:27:00 +01:00
bartal-lsn a1638bd944 add .dirmd and other metadata 2026-09-02 12:01:42 +01:00
bartal-lsn 93d830d500 fix pre delete dirmd set dir bug and implement last git sha and commit# 2026-09-02 11:43:45 +01:00
bartal-lsn 89b7eb78a7 add timestamp to dirmd files 2026-09-02 10:39:55 +01:00
bartal-lsn b1dd808d1e add python support 2026-08-29 21:58:16 +01:00
10 changed files with 297 additions and 179 deletions
+1
View File
@@ -1,3 +1,4 @@
bin/ bin/
.env .env
vendor/ vendor/
.dirmd
+30 -5
View File
@@ -138,10 +138,32 @@ 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:
- **ISO 8601 Timestamp**: Added as a metadata line (`> Generated: ...`) immediately after the H1 header for every new file created.
- **Git Commit Info**: If the source directory is a Git repository, a second metadata line (`> Commit: <sha> (<message>)`) is added below the timestamp.
- **ASCII tree format** (`+-` instead of Unicode box-drawing) to reduce token overhead - **ASCII tree format** (`+-` instead of Unicode box-drawing) to reduce token overhead
- **Explicit file delimiters**: `--- FILE: ./path (bytes, lines) ---` and `--- END FILE ---` - **Explicit file delimiters**: `--- FILE: ./path (bytes, lines) ---` and `--- END FILE ---`
- **Document type markers**: `[ROOT]` and `[SLICE]` in H1 headers - **Document type markers**: `[ROOT]` and `[SLICE]` in H1 headers
@@ -218,15 +240,18 @@ For full documentation on the Proton Drive CLI, including installation, authenti
Produces markdown with: Produces markdown with:
1. Absolute path as H1 header with `[ROOT]` or `[SLICE]` marker 1. Absolute path as H1 header with `[ROOT]` or `[SLICE]` marker
2. ASCII directory tree (indented with `+-` and `|` characters) 2. **Timestamp metadata line** (ISO 8601) immediately after the H1 header (only for new files)
3. Vertical slices index table (sliced mode only) 3. ASCII directory tree (indented with `+-` and `|` characters)
4. All text files with content in fenced code blocks 4. Vertical slices index table (sliced mode only)
5. Explicit `--- FILE: ---` and `--- END FILE ---` delimiters with byte and line counts 5. All text files with content in fenced code blocks
6. Language detection from file extensions 6. Explicit `--- FILE: ---` and `--- END FILE ---` delimiters with byte and line counts
7. Language detection from file extensions
Example output structure (non-sliced mode): Example output structure (non-sliced mode):
# /home/user/repos/myproject [ROOT] # /home/user/repos/myproject [ROOT]
> Generated: 2026-09-02T14:30:00Z
> Commit: a1b2c3d (Fix login bug)
## Directory Tree ## Directory Tree
+15 -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 # 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() {
@@ -86,7 +78,7 @@ func Execute() {
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output directory (required)") rootCmd.PersistentFlags().StringVarP(&outputPath, "output", "o", "", "output directory (required)")
rootCmd.Flags().BoolVarP(&appendMode, "append", "a", false, "append to existing index.md") 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().BoolVarP(&force, "force", "f", false, "overwrite existing output directory (deletes all contents)")
rootCmd.Flags().StringVar(&singleFile, "input-file", "", "single file input (append mode only)") rootCmd.Flags().StringVar(&singleFile, "input-file", "", "single file input (append mode only)")
rootCmd.Flags().BoolVarP(&tuiMode, "interactive", "i", false, "launch interactive TUI configuration wizard") rootCmd.Flags().BoolVarP(&tuiMode, "interactive", "i", false, "launch interactive TUI configuration wizard")
rootCmd.Flags().Int64Var(&maxSize, "max-size", config.DefaultMaxSize, "max file size in bytes") rootCmd.Flags().Int64Var(&maxSize, "max-size", config.DefaultMaxSize, "max file size in bytes")
@@ -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 != "" {
@@ -174,9 +168,14 @@ func run(cmd *cobra.Command, args []string) error {
} }
if !appendMode { if !appendMode {
if _, err := os.Stat(absOutput); err == nil && !force { if _, err := os.Stat(absOutput); err == nil {
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)
} }
if err := os.RemoveAll(absOutput); err != nil {
return fmt.Errorf("failed to remove existing output directory: %w", err)
}
}
} }
if err := os.MkdirAll(absOutput, 0755); err != nil { if err := os.MkdirAll(absOutput, 0755); err != nil {
@@ -202,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 {
@@ -210,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
} }
@@ -242,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)
} }
+13 -31
View File
@@ -15,19 +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", "uv.lock",
"bin",
"go.mod",
"go.sum",
"target",
"build",
"dist",
"Cargo.lock",
".idea",
".vscode",
"dependency-reduced-pom.xml",
} }
var DefaultExts = []string{ var DefaultExts = []string{
@@ -39,10 +29,8 @@ 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", ".pyc", ".pyo",
".war",
".ear",
} }
var FrontendExts = []string{ var FrontendExts = []string{
@@ -55,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
@@ -82,6 +64,7 @@ type Config struct {
FrontendOnly bool FrontendOnly bool
OmitTest bool OmitTest bool
OmitMd bool OmitMd bool
IncludeInstructions bool
} }
func PrintUsage() { func PrintUsage() {
@@ -110,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)")
@@ -124,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()
@@ -140,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")
} }
@@ -177,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 != "" {
+10
View File
@@ -99,6 +99,16 @@ func isTestFile(path, name string) bool {
} }
slashPath := filepath.ToSlash(path) slashPath := filepath.ToSlash(path)
return strings.Contains(slashPath, "/src/test/") return strings.Contains(slashPath, "/src/test/")
case ".py":
lower := strings.ToLower(name)
if strings.HasPrefix(lower, "test_") || strings.HasSuffix(lower, "_test.py") {
return true
}
if strings.EqualFold(name, "conftest.py") {
return true
}
slashPath := filepath.ToSlash(path)
return strings.Contains(slashPath, "/tests/")
} }
return false return false
+37
View File
@@ -0,0 +1,37 @@
package git
import (
"fmt"
"os/exec"
"strings"
)
type CommitInfo struct {
ShortSHA string
Message string
}
func GetCommitInfo(rootDir string) *CommitInfo {
cmd := exec.Command("git", "-C", rootDir, "log", "-1", "--format=%h %s")
output, err := cmd.Output()
if err != nil {
return nil
}
parts := strings.SplitN(strings.TrimSpace(string(output)), " ", 2)
if len(parts) < 2 {
return nil
}
return &CommitInfo{
ShortSHA: parts[0],
Message: parts[1],
}
}
func FormatCommitLine(info *CommitInfo) string {
if info == nil {
return ""
}
return fmt.Sprintf("> Commit: %s (%s)\n", info.ShortSHA, info.Message)
}
+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()
}
+40 -34
View File
@@ -6,8 +6,11 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"time"
"unicode/utf8" "unicode/utf8"
"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"
) )
@@ -35,7 +38,18 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
h1Path = filepath.Join(result.AbsRoot, result.Entries[0].RelPath) h1Path = filepath.Join(result.AbsRoot, result.Entries[0].RelPath)
} }
fmt.Fprintf(&sb, "# %s [ROOT]\n\n", h1Path) fmt.Fprintf(&sb, "# %s [ROOT]\n", h1Path)
if !appendMode {
fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
sb.WriteString(git.FormatCommitLine(result.Commit))
if result.Instructions != "" {
sb.WriteString(instructions.Format(result.Instructions))
} else {
sb.WriteString("\n")
}
}
if !singleFile { if !singleFile {
root := tree.Build(result.Entries) root := tree.Build(result.Entries)
@@ -53,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)
@@ -105,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); 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)
} }
@@ -136,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); 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)
} }
} }
@@ -153,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) 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)
@@ -161,8 +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\n", absRoot) fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
sb.WriteString(git.FormatCommitLine(commit))
if instContent != "" {
sb.WriteString(instructions.Format(instContent))
} else {
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")
@@ -196,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)
@@ -208,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")
} }
@@ -230,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) 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)
@@ -238,24 +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\n", absDir) fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
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)
@@ -263,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)
+23
View File
@@ -9,6 +9,8 @@ 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/instructions"
) )
type Entry struct { type Entry struct {
@@ -21,6 +23,8 @@ type Result struct {
AbsRoot string AbsRoot string
SingleFile bool SingleFile bool
OmitTest bool OmitTest bool
Commit *git.CommitInfo
Instructions string
} }
func Run(cfg *config.Config) (*Result, error) { func Run(cfg *config.Config) (*Result, error) {
@@ -53,11 +57,19 @@ 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)
} }
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()}},
AbsRoot: filepath.Dir(cfg.AbsRoot), AbsRoot: filepath.Dir(cfg.AbsRoot),
SingleFile: true, SingleFile: true,
OmitTest: cfg.OmitTest, OmitTest: cfg.OmitTest,
Commit: commitInfo,
Instructions: instContent,
}, nil }, nil
} }
@@ -73,6 +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}
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)