Compare commits
28
Commits
v0.0.1
...
f908bffc41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f908bffc41 | ||
|
|
a1638bd944 | ||
|
|
93d830d500 | ||
|
|
89b7eb78a7 | ||
|
|
b1dd808d1e | ||
|
|
d247095e87 | ||
|
|
66579d068d | ||
|
|
4c14ef2cee | ||
|
|
0f94e50746 | ||
|
|
c9aa028b3c | ||
|
|
bca125cb92 | ||
|
|
7d73901631 | ||
|
|
6663cb25f8 | ||
|
|
8a28364839 | ||
|
|
894f97820e | ||
|
|
fb646a94be | ||
|
|
4bcae1358d | ||
|
|
2428a33046 | ||
|
|
a48fe55d4b | ||
|
|
0df346dbf9 | ||
|
|
d7f4c6826b | ||
|
|
79be211df3 | ||
|
|
ba87d6184b | ||
|
|
180c3f0eba | ||
|
|
be0c761cb3 | ||
|
|
4ebf3bd60f | ||
|
|
63547e74c2 | ||
|
|
79b630b622 |
@@ -1,3 +1,4 @@
|
|||||||
bin/
|
bin/
|
||||||
.env
|
.env
|
||||||
vendor/
|
vendor/
|
||||||
|
.dirmd
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Generate markdown documentation from directory structure. Optimized for LLM ingestion.
|
Generate markdown documentation from directory structure. Optimized for LLM ingestion.
|
||||||
|
|
||||||
|
<video src="./docs/videos/dirmd.webm" autoplay loop muted playsinline></video>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Recursive directory traversal
|
- Recursive directory traversal
|
||||||
@@ -17,6 +19,13 @@ Generate markdown documentation from directory structure. Optimized for LLM inge
|
|||||||
- Frontend only mode (include only frontend files and `README.md`)
|
- Frontend only mode (include only frontend files and `README.md`)
|
||||||
- Omit test files and inline test blocks (Go, Java, Rust)
|
- Omit test files and inline test blocks (Go, Java, Rust)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Only for linux.
|
||||||
|
Download binary from release page or install with go
|
||||||
|
```bash
|
||||||
|
go install git.flo.fo/FLO/dirmd@latest
|
||||||
|
```
|
||||||
|
|
||||||
## Use with Git Post Hook
|
## Use with Git Post Hook
|
||||||
combine with git post hook to sync with proton drive on each commit
|
combine with git post hook to sync with proton drive on each commit
|
||||||
|
|
||||||
@@ -129,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
|
||||||
@@ -209,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
|
||||||
|
|
||||||
|
|||||||
+30
-42
@@ -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 {
|
||||||
@@ -133,20 +126,21 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
OutputPath: outputPath,
|
OutputPath: outputPath,
|
||||||
AppendMode: appendMode,
|
AppendMode: appendMode,
|
||||||
Force: force,
|
Force: force,
|
||||||
MaxSize: maxSize,
|
MaxSize: maxSize,
|
||||||
MaxDepth: maxDepth,
|
MaxDepth: maxDepth,
|
||||||
Ignores: append(config.DefaultIgnores, ignorePatterns...),
|
Ignores: append(config.DefaultIgnores, ignorePatterns...),
|
||||||
Extensions: excludedExts,
|
Extensions: excludedExts,
|
||||||
ProtonDrive: protonDrive,
|
ProtonDrive: protonDrive,
|
||||||
DrivePath: drivePath,
|
DrivePath: drivePath,
|
||||||
VerticalSlices: verticalSlices,
|
VerticalSlices: verticalSlices,
|
||||||
SkipFrontend: skipFrontend,
|
SkipFrontend: skipFrontend,
|
||||||
FrontendOnly: frontendOnly,
|
FrontendOnly: frontendOnly,
|
||||||
OmitTest: omitTest,
|
OmitTest: omitTest,
|
||||||
OmitMd: omitMd,
|
OmitMd: omitMd,
|
||||||
|
IncludeInstructions: instructions,
|
||||||
}
|
}
|
||||||
|
|
||||||
if singleFile != "" {
|
if singleFile != "" {
|
||||||
@@ -173,11 +167,14 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
absOutput = filepath.Join(filepath.Dir(absOutput), filepath.Base(absOutput)+"_dirmd")
|
|
||||||
|
|
||||||
if !appendMode {
|
if !appendMode {
|
||||||
if _, err := os.Stat(absOutput); err == nil && !force {
|
if _, err := os.Stat(absOutput); err == nil {
|
||||||
return fmt.Errorf("output directory %s already exists (use -f to overwrite)", absOutput)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,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 {
|
||||||
@@ -212,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
|
||||||
}
|
}
|
||||||
@@ -244,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Automating sending files to proton drive on commits
|
|
||||||
|
|
||||||
If you add this to any repos, and have dirmd in path and logged in on proton drive cli, then after each commit the newest code is pushed to proton drive, as vertical slices. If you do not want vertical slices of the code omit --vertical-slices. Also check out the other flags of dirmd if required.
|
|
||||||
|
|
||||||
## repos-root/.git/hooks/post-commit
|
|
||||||
|
|
||||||
create this file
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if ! command -v dirmd &>/dev/null; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
||||||
REPO_NAME=$(basename "$REPO_ROOT")
|
|
||||||
|
|
||||||
dirmd -o "/tmp/${REPO_NAME}_new" "$REPO_ROOT" -f --vertical-slices --proton-drive
|
|
||||||
```
|
|
||||||
|
|
||||||
and then make it executable with something like `chmod +x .git/hooks/post-commit`
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 522 KiB |
Binary file not shown.
Binary file not shown.
+60
-78
@@ -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,33 +43,28 @@ 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
|
||||||
OutputPath string
|
OutputPath string
|
||||||
AppendMode bool
|
AppendMode bool
|
||||||
Force bool
|
Force bool
|
||||||
SingleFile bool
|
SingleFile bool
|
||||||
MaxSize int64
|
MaxSize int64
|
||||||
MaxDepth int
|
MaxDepth int
|
||||||
Ignores []string
|
Ignores []string
|
||||||
Extensions []string
|
Extensions []string
|
||||||
AbsRoot string
|
AbsRoot string
|
||||||
ProtonDrive bool
|
ProtonDrive bool
|
||||||
DrivePath string
|
DrivePath string
|
||||||
VerticalSlices bool
|
VerticalSlices bool
|
||||||
SkipFrontend bool
|
SkipFrontend bool
|
||||||
FrontendOnly bool
|
FrontendOnly bool
|
||||||
OmitTest bool
|
OmitTest bool
|
||||||
OmitMd bool
|
OmitMd bool
|
||||||
|
IncludeInstructions bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintUsage() {
|
func PrintUsage() {
|
||||||
@@ -94,22 +77,23 @@ func PrintUsage() {
|
|||||||
|
|
||||||
func Parse() (*Config, error) {
|
func Parse() (*Config, error) {
|
||||||
var (
|
var (
|
||||||
input string
|
input string
|
||||||
output string
|
output string
|
||||||
appendMd bool
|
appendMd bool
|
||||||
force bool
|
force bool
|
||||||
maxSize int64
|
maxSize int64
|
||||||
maxDepth int
|
maxDepth int
|
||||||
ignores stringSlice
|
ignores stringSlice
|
||||||
exts stringSlice
|
exts stringSlice
|
||||||
showVer bool
|
showVer bool
|
||||||
protonDrive bool
|
protonDrive bool
|
||||||
drivePath string
|
drivePath string
|
||||||
verticalSlices bool
|
verticalSlices bool
|
||||||
skipFrontend bool
|
skipFrontend bool
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -163,20 +144,21 @@ func Parse() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
OutputPath: output,
|
OutputPath: output,
|
||||||
AppendMode: appendMd,
|
AppendMode: appendMd,
|
||||||
Force: force,
|
Force: force,
|
||||||
MaxSize: maxSize,
|
MaxSize: maxSize,
|
||||||
MaxDepth: maxDepth,
|
MaxDepth: maxDepth,
|
||||||
Ignores: append(DefaultIgnores, ignores...),
|
Ignores: append(DefaultIgnores, ignores...),
|
||||||
Extensions: excludedExts,
|
Extensions: excludedExts,
|
||||||
ProtonDrive: protonDrive,
|
ProtonDrive: protonDrive,
|
||||||
DrivePath: drivePath,
|
DrivePath: drivePath,
|
||||||
VerticalSlices: verticalSlices,
|
VerticalSlices: verticalSlices,
|
||||||
SkipFrontend: skipFrontend,
|
SkipFrontend: skipFrontend,
|
||||||
FrontendOnly: frontendOnly,
|
FrontendOnly: frontendOnly,
|
||||||
OmitTest: omitTest,
|
OmitTest: omitTest,
|
||||||
OmitMd: omitMd,
|
OmitMd: omitMd,
|
||||||
|
IncludeInstructions: includeInstructions,
|
||||||
}
|
}
|
||||||
|
|
||||||
if input != "" {
|
if input != "" {
|
||||||
|
|||||||
+17
-11
@@ -30,7 +30,6 @@ func CheckAuth() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureRemoteParent creates the parent remote directory if it doesn't exist.
|
|
||||||
func EnsureRemoteParent(remoteDir string) error {
|
func EnsureRemoteParent(remoteDir string) error {
|
||||||
binary, err := Lookup()
|
binary, err := Lookup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,37 +37,45 @@ func EnsureRemoteParent(remoteDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parentDir := filepath.Dir(remoteDir)
|
parentDir := filepath.Dir(remoteDir)
|
||||||
|
|
||||||
// Try create-folder on the grandparent for the parent
|
|
||||||
grandparent := filepath.Dir(parentDir)
|
grandparent := filepath.Dir(parentDir)
|
||||||
parentName := filepath.Base(parentDir)
|
parentName := filepath.Base(parentDir)
|
||||||
|
|
||||||
cmd := exec.Command(binary, "filesystem", "create-folder", grandparent, parentName)
|
cmd := exec.Command(binary, "filesystem", "create-folder", grandparent, parentName)
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.Run() // Ignore errors — folder may already exist
|
cmd.Run()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadDir uploads an entire local directory to Proton Drive.
|
func CleanRemote(remoteDir string) {
|
||||||
// The local directory's basename becomes the remote directory name.
|
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 {
|
func UploadDir(localDir, remoteDir string) error {
|
||||||
binary, err := Lookup()
|
binary, err := Lookup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("proton-drive not found in PATH: %w", err)
|
return fmt.Errorf("proton-drive not found in PATH: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure parent of remote dir exists
|
|
||||||
if err := EnsureRemoteParent(remoteDir); err != nil {
|
if err := EnsureRemoteParent(remoteDir); err != nil {
|
||||||
return fmt.Errorf("cannot prepare remote directory: %w", err)
|
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)
|
remoteParent := filepath.Dir(remoteDir)
|
||||||
|
|
||||||
cmd := exec.Command(binary, "filesystem", "upload", localDir, remoteParent,
|
cmd := exec.Command(binary, "filesystem", "upload", localDir, remoteParent,
|
||||||
"--conflict-strategy", "replace")
|
"--folder-conflict-strategy", "replace", "--file-conflict-strategy", "replace")
|
||||||
|
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
@@ -80,7 +87,6 @@ func UploadDir(localDir, remoteDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload uploads a single local file to a remote path on Proton Drive.
|
|
||||||
func Upload(localPath, remotePath string) error {
|
func Upload(localPath, remotePath string) error {
|
||||||
binary, err := Lookup()
|
binary, err := Lookup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -111,7 +117,7 @@ func Upload(localPath, remotePath string) error {
|
|||||||
|
|
||||||
func runUpload(binary, localPath, remoteDir string) error {
|
func runUpload(binary, localPath, remoteDir string) error {
|
||||||
cmd := exec.Command(binary, "filesystem", "upload", localPath, remoteDir,
|
cmd := exec.Command(binary, "filesystem", "upload", localPath, remoteDir,
|
||||||
"--conflict-strategy", "replace")
|
"--folder-conflict-strategy", "replace", "--file-conflict-strategy", "replace")
|
||||||
|
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,17 +89,9 @@ func Generate(cfg *config.Config) GenerateResult {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.VerticalSlices {
|
if err := drive.UploadDir(cfg.OutputPath, remoteDir); err != nil {
|
||||||
if err := drive.UploadDir(cfg.OutputPath, remoteDir); err != nil {
|
result.Error = fmt.Errorf("drive upload: %w", err)
|
||||||
result.Error = fmt.Errorf("drive upload: %w", err)
|
return result
|
||||||
return result
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
indexPath := filepath.Join(cfg.OutputPath, "index_"+repoName+".md")
|
|
||||||
if err := drive.Upload(indexPath, remoteDir); err != nil {
|
|
||||||
result.Error = fmt.Errorf("drive upload: %w", err)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result.Uploaded = true
|
result.Uploaded = true
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -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")
|
||||||
@@ -155,20 +157,21 @@ func Run() error {
|
|||||||
allIgnores = append(allIgnores, ignores...)
|
allIgnores = append(allIgnores, ignores...)
|
||||||
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
InputPath: inputDir,
|
InputPath: inputDir,
|
||||||
OutputPath: outputDir,
|
OutputPath: outputDir,
|
||||||
Force: force,
|
Force: force,
|
||||||
MaxSize: int64(maxSizeKB) * 1024,
|
MaxSize: int64(maxSizeKB) * 1024,
|
||||||
MaxDepth: maxDepth,
|
MaxDepth: maxDepth,
|
||||||
Ignores: allIgnores,
|
Ignores: allIgnores,
|
||||||
Extensions: excludedExts,
|
Extensions: excludedExts,
|
||||||
ProtonDrive: protonDrive,
|
ProtonDrive: protonDrive,
|
||||||
DrivePath: drivePath,
|
DrivePath: drivePath,
|
||||||
VerticalSlices: verticalSlices,
|
VerticalSlices: verticalSlices,
|
||||||
SkipFrontend: skipFrontend,
|
SkipFrontend: skipFrontend,
|
||||||
FrontendOnly: frontendOnly,
|
FrontendOnly: frontendOnly,
|
||||||
OmitTest: omitTest,
|
OmitTest: omitTest,
|
||||||
OmitMd: omitMd,
|
OmitMd: omitMd,
|
||||||
|
IncludeInstructions: includeInstructions,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := buildCommand(cfg)
|
cmd := buildCommand(cfg)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -17,10 +19,12 @@ type Entry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Entries []Entry
|
Entries []Entry
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user