24 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
bartal-lsn d247095e87 fix(drive cli)!: delete remote flag changed 2026-08-28 14:08:58 +01:00
bartal-lsn 66579d068d webm 2026-08-28 01:14:26 +01:00
bartal-lsn 4c14ef2cee webm 2026-08-28 01:10:37 +01:00
bartal-lsn 0f94e50746 webm 2026-08-28 01:07:41 +01:00
bartal-lsn c9aa028b3c webm 2026-08-28 01:01:49 +01:00
bartal-lsn bca125cb92 Update README.md 2026-08-28 00:50:13 +01:00
bartal-lsn 7d73901631 Update README.md 2026-08-28 00:49:26 +01:00
bartal-lsn 6663cb25f8 webm 2026-08-28 00:42:23 +01:00
bartal-lsn 8a28364839 Update README.md 2026-08-28 00:38:34 +01:00
bartal-lsn 894f97820e webm 2026-08-28 00:37:43 +01:00
bartal-lsn fb646a94be Update README.md 2026-08-28 00:32:49 +01:00
bartal-lsn 4bcae1358d Update README.md 2026-08-28 00:32:33 +01:00
bartal-lsn 2428a33046 Update README.md 2026-08-28 00:31:40 +01:00
bartal-lsn a48fe55d4b Update README.md 2026-08-28 00:29:47 +01:00
bartal-lsn 0df346dbf9 Update README.md 2026-08-28 00:29:18 +01:00
bartal-lsn d7f4c6826b Update README.md 2026-08-28 00:27:34 +01:00
bartal-lsn 79be211df3 test 2026-08-28 00:26:00 +01:00
bartal-lsn ba87d6184b test webm 2026-08-28 00:24:20 +01:00
bartal-lsn 180c3f0eba test webm 2026-08-28 00:19:24 +01:00
13 changed files with 316 additions and 190 deletions
+1
View File
@@ -1,3 +1,4 @@
bin/
.env
vendor/
.dirmd
+32 -5
View File
@@ -2,6 +2,8 @@
Generate markdown documentation from directory structure. Optimized for LLM ingestion.
<video src="./docs/videos/dirmd.webm" autoplay loop muted playsinline></video>
## Features
- Recursive directory traversal
@@ -136,10 +138,32 @@ Example output with `dirmd -o docs.md ~/repos/myproject --vertical-slices`:
├── cmd.md ← slice: tree + contents of cmd/
└── 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
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
- **Explicit file delimiters**: `--- FILE: ./path (bytes, lines) ---` and `--- END FILE ---`
- **Document type markers**: `[ROOT]` and `[SLICE]` in H1 headers
@@ -216,15 +240,18 @@ For full documentation on the Proton Drive CLI, including installation, authenti
Produces markdown with:
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
2. **Timestamp metadata line** (ISO 8601) immediately after the H1 header (only for new files)
3. ASCII directory tree (indented with `+-` and `|` characters)
4. Vertical slices index table (sliced mode only)
5. All text files with content in fenced code blocks
6. Explicit `--- FILE: ---` and `--- END FILE ---` delimiters with byte and line counts
7. Language detection from file extensions
Example output structure (non-sliced mode):
# /home/user/repos/myproject [ROOT]
> Generated: 2026-09-02T14:30:00Z
> Commit: a1b2c3d (Fix login bug)
## Directory Tree
+15 -25
View File
@@ -27,33 +27,24 @@ Output is always written to a directory specified by -o.
Examples:
# Create new documentation (writes index.md inside ./docs/)
# Create new documentation
dirmd -o ./docs ~/repos/myproject
# Overwrite existing output directory
# Overwrite existing output directory (deletes all contents)
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)
# Vertical slices
dirmd -o ./docs ~/repos/myproject --vertical-slices
# Skip frontend files
dirmd -o ./docs ~/repos/myproject --skip-frontend
# Frontend only
dirmd -o ./docs ~/repos/myproject --frontend-only
# Omit test files and test blocks
dirmd -o ./docs ~/repos/myproject --omit-test
# Include instructions from .dirmd
dirmd -o ./docs ~/repos/myproject --instructions
# 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
# Interactive TUI mode
dirmd --tui`,
RunE: run,
@@ -75,6 +66,7 @@ Examples:
omitTest bool
omitMd bool
tuiMode bool
instructions bool
)
func Execute() {
@@ -86,7 +78,7 @@ func Execute() {
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().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().BoolVarP(&tuiMode, "interactive", "i", false, "launch interactive TUI configuration wizard")
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(&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(&instructions, "instructions", false, "include .dirmd instructions file if present in root")
}
func run(cmd *cobra.Command, args []string) error {
@@ -147,6 +140,7 @@ func run(cmd *cobra.Command, args []string) error {
FrontendOnly: frontendOnly,
OmitTest: omitTest,
OmitMd: omitMd,
IncludeInstructions: instructions,
}
if singleFile != "" {
@@ -174,9 +168,14 @@ func run(cmd *cobra.Command, args []string) error {
}
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)
}
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 {
@@ -202,7 +201,6 @@ func run(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "generated %d files in %s\n", len(generated), absOutput)
if protonDrive {
@@ -210,27 +208,22 @@ func run(cmd *cobra.Command, args []string) error {
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
}
@@ -242,19 +235,16 @@ func run(cmd *cobra.Command, args []string) error {
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)
}
Binary file not shown.
Binary file not shown.
+13 -31
View File
@@ -15,19 +15,9 @@ const (
)
var DefaultIgnores = []string{
".git",
"node_modules",
"vendor",
"bin",
"go.mod",
"go.sum",
"target",
"build",
"dist",
"Cargo.lock",
".idea",
".vscode",
"dependency-reduced-pom.xml",
".git", "node_modules", "vendor", "bin", "go.mod", "go.sum",
"target", "build", "dist", "Cargo.lock", ".idea", ".vscode",
"dependency-reduced-pom.xml", "__pycache__", "venv", "env", "uv.lock",
}
var DefaultExts = []string{
@@ -39,10 +29,8 @@ var DefaultExts = []string{
".pdf", ".doc", ".docx", ".xls", ".xlsx",
".pem", ".key", ".cert",
".db", ".sqlite", ".sqlite3",
".class",
".jar",
".war",
".ear",
".class", ".jar", ".war", ".ear",
".pyc", ".pyo",
}
var FrontendExts = []string{
@@ -55,14 +43,8 @@ var FrontendExts = []string{
type stringSlice []string
func (s *stringSlice) String() string {
return strings.Join(*s, ", ")
}
func (s *stringSlice) Set(v string) error {
*s = append(*s, v)
return nil
}
func (s *stringSlice) String() string { return strings.Join(*s, ", ") }
func (s *stringSlice) Set(v string) error { *s = append(*s, v); return nil }
type Config struct {
InputPath string
@@ -82,6 +64,7 @@ type Config struct {
FrontendOnly bool
OmitTest bool
OmitMd bool
IncludeInstructions bool
}
func PrintUsage() {
@@ -110,6 +93,7 @@ func Parse() (*Config, error) {
frontendOnly bool
omitTest bool
omitMd bool
includeInstructions bool
)
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.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(&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(&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(&includeInstructions, "instructions", false, "include .dirmd instructions file if present in root")
flag.Usage = PrintUsage
flag.Parse()
@@ -140,19 +125,15 @@ func Parse() (*Config, error) {
if output == "" {
return nil, fmt.Errorf("-o is required")
}
if drivePath != "" && !protonDrive {
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")
}
if skipFrontend && frontendOnly {
return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
}
@@ -177,6 +158,7 @@ func Parse() (*Config, error) {
FrontendOnly: frontendOnly,
OmitTest: omitTest,
OmitMd: omitMd,
IncludeInstructions: includeInstructions,
}
if input != "" {
+17 -11
View File
@@ -30,7 +30,6 @@ func CheckAuth() error {
return nil
}
// EnsureRemoteParent creates the parent remote directory if it doesn't exist.
func EnsureRemoteParent(remoteDir string) error {
binary, err := Lookup()
if err != nil {
@@ -38,37 +37,45 @@ func EnsureRemoteParent(remoteDir string) error {
}
parentDir := filepath.Dir(remoteDir)
// Try create-folder on the grandparent for the parent
grandparent := filepath.Dir(parentDir)
parentName := filepath.Base(parentDir)
cmd := exec.Command(binary, "filesystem", "create-folder", grandparent, parentName)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Run() // Ignore errors — folder may already exist
cmd.Run()
return nil
}
// UploadDir uploads an entire local directory to Proton Drive.
// The local directory's basename becomes the remote directory name.
func CleanRemote(remoteDir string) {
binary, err := Lookup()
if err != nil {
return
}
cmd := exec.Command(binary, "filesystem", "delete", remoteDir)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Run()
}
func UploadDir(localDir, remoteDir string) error {
binary, err := Lookup()
if err != nil {
return fmt.Errorf("proton-drive not found in PATH: %w", err)
}
// Ensure parent of remote dir exists
if err := EnsureRemoteParent(remoteDir); err != nil {
return fmt.Errorf("cannot prepare remote directory: %w", err)
}
// Upload to parent — the local dir's basename creates the remote dir
CleanRemote(remoteDir)
remoteParent := filepath.Dir(remoteDir)
cmd := exec.Command(binary, "filesystem", "upload", localDir, remoteParent,
"--conflict-strategy", "replace")
"--folder-conflict-strategy", "replace", "--file-conflict-strategy", "replace")
var stderr strings.Builder
cmd.Stderr = &stderr
@@ -80,7 +87,6 @@ func UploadDir(localDir, remoteDir string) error {
return nil
}
// Upload uploads a single local file to a remote path on Proton Drive.
func Upload(localPath, remotePath string) error {
binary, err := Lookup()
if err != nil {
@@ -111,7 +117,7 @@ func Upload(localPath, remotePath string) error {
func runUpload(binary, localPath, remoteDir string) error {
cmd := exec.Command(binary, "filesystem", "upload", localPath, remoteDir,
"--conflict-strategy", "replace")
"--folder-conflict-strategy", "replace", "--file-conflict-strategy", "replace")
var stderr strings.Builder
cmd.Stderr = &stderr
+10
View File
@@ -99,6 +99,16 @@ func isTestFile(path, name string) bool {
}
slashPath := filepath.ToSlash(path)
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
+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"
"sort"
"strings"
"time"
"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/walker"
)
@@ -35,7 +38,18 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
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 {
root := tree.Build(result.Entries)
@@ -53,7 +67,6 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
for _, entry := range result.Entries {
fullPath := filepath.Join(result.AbsRoot, entry.RelPath)
content, err := readFileForOutput(fullPath, result.OmitTest)
if err != nil {
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
indexBasename := "index_" + repoName + ".md"
rootPath := filepath.Join(outputDir, indexBasename)
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)
}
@@ -136,16 +148,13 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
for i, e := range dirEntries {
sepIdx := strings.Index(e.RelPath, string(filepath.Separator))
if sepIdx >= 0 {
treeEntries[i] = walker.Entry{
RelPath: e.RelPath[sepIdx+1:],
Size: e.Size,
}
treeEntries[i] = walker.Entry{RelPath: e.RelPath[sepIdx+1:], Size: e.Size}
} else {
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)
}
}
@@ -153,7 +162,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
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)
if err != nil {
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()
var sb strings.Builder
fmt.Fprintf(&sb, "# %s [ROOT]\n\n", absRoot)
fmt.Fprintf(&sb, "# %s [ROOT]\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)
sb.WriteString("## Directory Tree\n\n")
@@ -196,11 +211,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
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
})
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 := 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)
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)
fmt.Fprintf(&sb, "\n%s", content)
if !strings.HasSuffix(content, "\n") {
fmt.Fprintf(&sb, "\n")
}
@@ -230,7 +238,7 @@ func writeRootSlice(path, absRoot, rootName, repoName string, rootFiles []walker
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)
if err != nil {
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()
var sb strings.Builder
fmt.Fprintf(&sb, "# %s [SLICE]\n\n", absDir)
fmt.Fprintf(&sb, "# %s [SLICE]\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)
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
})
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 := 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)
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)
fmt.Fprintf(&sb, "\n%s", content)
if !strings.HasSuffix(content, "\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 non-README markdown", "omit-md"),
huh.NewOption("Upload to Proton Drive", "proton-drive"),
huh.NewOption("Include .dirmd instructions", "instructions"),
).
Value(&options).
Height(10),
@@ -115,6 +116,7 @@ func Run() error {
omitTest := containsOption(options, "omit-test")
omitMd := containsOption(options, "omit-md")
protonDrive := containsOption(options, "proton-drive")
includeInstructions := containsOption(options, "instructions")
if skipFrontend && frontendOnly {
return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
@@ -169,6 +171,7 @@ func Run() error {
FrontendOnly: frontendOnly,
OmitTest: omitTest,
OmitMd: omitMd,
IncludeInstructions: includeInstructions,
}
cmd := buildCommand(cfg)
+23
View File
@@ -9,6 +9,8 @@ import (
"git.flo.fo/FLO/dirmd/internal/config"
"git.flo.fo/FLO/dirmd/internal/filter"
"git.flo.fo/FLO/dirmd/internal/git"
"git.flo.fo/FLO/dirmd/internal/instructions"
)
type Entry struct {
@@ -21,6 +23,8 @@ type Result struct {
AbsRoot string
SingleFile bool
OmitTest bool
Commit *git.CommitInfo
Instructions string
}
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)
}
commitInfo := git.GetCommitInfo(filepath.Dir(cfg.AbsRoot))
var instContent string
if cfg.IncludeInstructions {
instContent, _ = instructions.Read(filepath.Dir(cfg.AbsRoot))
}
return &Result{
Entries: []Entry{{RelPath: filepath.Base(cfg.AbsRoot), Size: info.Size()}},
AbsRoot: filepath.Dir(cfg.AbsRoot),
SingleFile: true,
OmitTest: cfg.OmitTest,
Commit: commitInfo,
Instructions: instContent,
}, nil
}
@@ -73,6 +85,17 @@ func walkDirectory(cfg *config.Config) (*Result, error) {
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 {
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err)