v0.1.4 #15
+14
-1
@@ -46,7 +46,10 @@ Examples:
|
|||||||
dirmd -o ./docs ~/repos/myproject --proton-drive
|
dirmd -o ./docs ~/repos/myproject --proton-drive
|
||||||
|
|
||||||
# Interactive TUI mode
|
# Interactive TUI mode
|
||||||
dirmd --tui`,
|
dirmd --tui
|
||||||
|
|
||||||
|
# Pick specific files in vertical slices mode
|
||||||
|
dirmd -o ./docs ~/repos/myproject --vertical-slices --pick common/src/lib.rs --pick mssql-extractor/src/processor.rs`,
|
||||||
RunE: run,
|
RunE: run,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,8 @@ Examples:
|
|||||||
omitMd bool
|
omitMd bool
|
||||||
tuiMode bool
|
tuiMode bool
|
||||||
instructions bool
|
instructions bool
|
||||||
|
pickPaths []string
|
||||||
|
smallTree bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
@@ -93,6 +98,8 @@ func init() {
|
|||||||
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")
|
rootCmd.Flags().BoolVar(&instructions, "instructions", false, "include .dirmd instructions file if present in root")
|
||||||
|
rootCmd.Flags().StringArrayVar(&pickPaths, "pick", nil, "repeatable: relative path to file to include as pick (requires --vertical-slices)")
|
||||||
|
rootCmd.Flags().BoolVar(&smallTree, "small-tree", false, "use breadcrumb-only tree for picked files (default: full tree)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cmd *cobra.Command, args []string) error {
|
func run(cmd *cobra.Command, args []string) error {
|
||||||
@@ -120,6 +127,10 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
|
return fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(pickPaths) > 0 && !verticalSlices {
|
||||||
|
return fmt.Errorf("--pick requires --vertical-slices")
|
||||||
|
}
|
||||||
|
|
||||||
excludedExts := append(config.DefaultExts, extensions...)
|
excludedExts := append(config.DefaultExts, extensions...)
|
||||||
if skipFrontend {
|
if skipFrontend {
|
||||||
excludedExts = append(excludedExts, config.FrontendExts...)
|
excludedExts = append(excludedExts, config.FrontendExts...)
|
||||||
@@ -141,6 +152,8 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
OmitTest: omitTest,
|
OmitTest: omitTest,
|
||||||
OmitMd: omitMd,
|
OmitMd: omitMd,
|
||||||
IncludeInstructions: instructions,
|
IncludeInstructions: instructions,
|
||||||
|
PickPaths: pickPaths,
|
||||||
|
SmallTree: smallTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
if singleFile != "" {
|
if singleFile != "" {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
DefaultMaxSize = 512 * 1024
|
DefaultMaxSize = 512 * 1024
|
||||||
DefaultMaxDepth = 20
|
DefaultMaxDepth = 20
|
||||||
Version = "0.1.0"
|
Version = "0.1.4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultIgnores = []string{
|
var DefaultIgnores = []string{
|
||||||
@@ -65,6 +65,8 @@ type Config struct {
|
|||||||
OmitTest bool
|
OmitTest bool
|
||||||
OmitMd bool
|
OmitMd bool
|
||||||
IncludeInstructions bool
|
IncludeInstructions bool
|
||||||
|
PickPaths []string
|
||||||
|
SmallTree bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintUsage() {
|
func PrintUsage() {
|
||||||
@@ -94,6 +96,8 @@ func Parse() (*Config, error) {
|
|||||||
omitTest bool
|
omitTest bool
|
||||||
omitMd bool
|
omitMd bool
|
||||||
includeInstructions bool
|
includeInstructions bool
|
||||||
|
pickPaths stringSlice
|
||||||
|
smallTree bool
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
||||||
@@ -113,6 +117,8 @@ func Parse() (*Config, error) {
|
|||||||
flag.BoolVar(&omitTest, "omit-test", false, "exclude test files and strip #[cfg(test)] blocks")
|
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.BoolVar(&includeInstructions, "instructions", false, "include .dirmd instructions file if present in root")
|
||||||
|
flag.Var(&pickPaths, "pick", "repeatable: relative path to file to include as pick (requires --vertical-slices)")
|
||||||
|
flag.BoolVar(&smallTree, "small-tree", false, "use breadcrumb-only tree for picked files (default: full tree)")
|
||||||
|
|
||||||
flag.Usage = PrintUsage
|
flag.Usage = PrintUsage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -137,6 +143,9 @@ func Parse() (*Config, error) {
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
if len(pickPaths) > 0 && !verticalSlices {
|
||||||
|
return nil, fmt.Errorf("--pick requires --vertical-slices")
|
||||||
|
}
|
||||||
|
|
||||||
excludedExts := append(DefaultExts, exts...)
|
excludedExts := append(DefaultExts, exts...)
|
||||||
if skipFrontend {
|
if skipFrontend {
|
||||||
@@ -159,6 +168,8 @@ func Parse() (*Config, error) {
|
|||||||
OmitTest: omitTest,
|
OmitTest: omitTest,
|
||||||
OmitMd: omitMd,
|
OmitMd: omitMd,
|
||||||
IncludeInstructions: includeInstructions,
|
IncludeInstructions: includeInstructions,
|
||||||
|
PickPaths: pickPaths,
|
||||||
|
SmallTree: smallTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
if input != "" {
|
if input != "" {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"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/git"
|
"git.flo.fo/FLO/dirmd/internal/git"
|
||||||
"git.flo.fo/FLO/dirmd/internal/instructions"
|
"git.flo.fo/FLO/dirmd/internal/instructions"
|
||||||
"git.flo.fo/FLO/dirmd/internal/tree"
|
"git.flo.fo/FLO/dirmd/internal/tree"
|
||||||
@@ -150,7 +151,17 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
|
|||||||
rootSlicePath := filepath.Join(outputDir, rootSliceBasename)
|
rootSlicePath := filepath.Join(outputDir, rootSliceBasename)
|
||||||
generated = append(generated, rootSlicePath)
|
generated = append(generated, rootSlicePath)
|
||||||
|
|
||||||
if err := writeRootIndex(rootPath, absRoot, rootName, repoName, topLevelDirs, result.Commit, result.Instructions, treeRoot, result.Cfg); err != nil {
|
pickInfos := make([]PickInfo, 0)
|
||||||
|
|
||||||
|
if len(result.Cfg.PickPaths) > 0 {
|
||||||
|
pickInfos, err = writePickFiles(absRoot, absRoot, outputDir, result.Cfg, result.OmitTest, result.Commit, treeRoot, result.Cfg.SmallTree)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write pick files: %w", err)
|
||||||
|
}
|
||||||
|
generated = append(generated, pickInfoPaths(pickInfos)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeRootIndex(rootPath, absRoot, rootName, repoName, topLevelDirs, result.Commit, result.Instructions, treeRoot, result.Cfg, pickInfos); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write root index: %w", err)
|
return nil, fmt.Errorf("failed to write root index: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +189,137 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
|
|||||||
return generated, nil
|
return generated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[string][]walker.Entry, commit *git.CommitInfo, instContent string, treeRoot *tree.Node, cfg *config.Config) error {
|
type PickInfo struct {
|
||||||
|
RelPath string
|
||||||
|
FileType string
|
||||||
|
SliceDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickInfoPaths(picks []PickInfo) []string {
|
||||||
|
var paths []string
|
||||||
|
for _, p := range picks {
|
||||||
|
paths = append(paths, filepath.Join(p.SliceDir, p.FileType))
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePickFiles(absRoot, baseRoot, outputDir string, cfg *config.Config, omitTest bool, commit *git.CommitInfo, treeRoot *tree.Node, smallTree bool) ([]PickInfo, error) {
|
||||||
|
var pickInfos []PickInfo
|
||||||
|
|
||||||
|
for _, pickPath := range cfg.PickPaths {
|
||||||
|
fullPath := filepath.Join(absRoot, pickPath)
|
||||||
|
|
||||||
|
info, err := os.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot access pick path %s: %w", pickPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("pick path must be a file: %s", pickPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file passes filters
|
||||||
|
name := filepath.Base(pickPath)
|
||||||
|
filter := &filter.Filter{
|
||||||
|
MaxSize: cfg.MaxSize,
|
||||||
|
Extensions: cfg.Extensions,
|
||||||
|
Ignores: cfg.Ignores,
|
||||||
|
OmitTest: cfg.OmitTest,
|
||||||
|
FrontendOnly: cfg.FrontendOnly,
|
||||||
|
OmitMd: cfg.OmitMd,
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ShouldSkipFile(fullPath, name, info) {
|
||||||
|
return nil, fmt.Errorf("pick path %s filtered by active rules", pickPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine slice dir from first path segment
|
||||||
|
parts := strings.Split(pickPath, string(filepath.Separator))
|
||||||
|
sliceDir := outputDir
|
||||||
|
if len(parts) > 1 {
|
||||||
|
sliceDir = filepath.Join(outputDir, parts[0])
|
||||||
|
if err := os.MkdirAll(sliceDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create pick slice directory %s: %w", sliceDir, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Root-level pick
|
||||||
|
sliceDir = outputDir
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceName := filepath.Base(pickPath) + ".md"
|
||||||
|
pickPathOut := filepath.Join(sliceDir, sliceName)
|
||||||
|
|
||||||
|
if err := writePickFile(pickPathOut, absRoot, pickPath, omitTest, commit, cfg, treeRoot, smallTree); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write pick %s: %w", pickPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pickInfos = append(pickInfos, PickInfo{
|
||||||
|
RelPath: pickPath,
|
||||||
|
FileType: sliceName,
|
||||||
|
SliceDir: filepath.Base(sliceDir),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return pickInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePickFile(path, absRoot, relPath string, omitTest bool, commit *git.CommitInfo, cfg *config.Config, treeRoot *tree.Node, smallTree bool) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create pick file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "# %s [SLICE]\n", filepath.Join(absRoot, relPath))
|
||||||
|
fmt.Fprintf(&sb, "> Generated: %s\n", time.Now().Format(time.RFC3339))
|
||||||
|
sb.WriteString(git.FormatCommitLine(commit))
|
||||||
|
sb.WriteString(FormatFlags(cfg))
|
||||||
|
|
||||||
|
fullPath := filepath.Join(absRoot, relPath)
|
||||||
|
content, err := readFileForOutput(fullPath, omitTest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot read pick file %s: %w", relPath, err)
|
||||||
|
}
|
||||||
|
language := extToLang(filepath.Ext(relPath))
|
||||||
|
lines := countLines(content)
|
||||||
|
|
||||||
|
if smallTree {
|
||||||
|
breadcrumb := tree.RenderBreadcrumb(absRoot, relPath)
|
||||||
|
fmt.Fprintf(&sb, "Tree: %s\n\n", breadcrumb)
|
||||||
|
} else {
|
||||||
|
sb.WriteString("## Directory Tree\n\n")
|
||||||
|
sb.WriteString(fenceChars + "\n")
|
||||||
|
sb.WriteString(renderTreeWithLegend(treeRoot, filepath.Base(absRoot)))
|
||||||
|
sb.WriteString(fenceChars + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", relPath, infoSize(fullPath), lines)
|
||||||
|
sb.WriteString(fenceChars)
|
||||||
|
if language != "" {
|
||||||
|
sb.WriteString(language)
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
sb.WriteString(content)
|
||||||
|
if !strings.HasSuffix(content, "\n") {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s\n--- END FILE ---\n\n", fenceChars))
|
||||||
|
|
||||||
|
_, err = f.WriteString(sb.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func infoSize(path string) int64 {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[string][]walker.Entry, commit *git.CommitInfo, instContent string, treeRoot *tree.Node, cfg *config.Config, picks []PickInfo) error {
|
||||||
f, err := os.Create(path)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create root index: %w", err)
|
return fmt.Errorf("cannot create root index: %w", err)
|
||||||
@@ -207,11 +348,9 @@ func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[s
|
|||||||
sb.WriteString("| Slice File | Directory | Files | Total Bytes |\n")
|
sb.WriteString("| Slice File | Directory | Files | Total Bytes |\n")
|
||||||
sb.WriteString("|------------|-----------|-------|-------------|\n")
|
sb.WriteString("|------------|-----------|-------|-------------|\n")
|
||||||
|
|
||||||
// First row: root slice
|
|
||||||
rootSliceFile := "root_" + repoName + ".md"
|
rootSliceFile := "root_" + repoName + ".md"
|
||||||
rootFileCount := 0
|
rootFileCount := len(topLevelDirs)
|
||||||
rootTotalBytes := int64(0)
|
rootTotalBytes := int64(0)
|
||||||
// We'll calculate this in the caller, for now placeholder
|
|
||||||
fmt.Fprintf(&sb, "| %s | (root) | %d | %d |\n", rootSliceFile, rootFileCount, rootTotalBytes)
|
fmt.Fprintf(&sb, "| %s | (root) | %d | %d |\n", rootSliceFile, rootFileCount, rootTotalBytes)
|
||||||
|
|
||||||
for _, dirName := range sortedKeys(topLevelDirs) {
|
for _, dirName := range sortedKeys(topLevelDirs) {
|
||||||
@@ -226,6 +365,15 @@ func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[s
|
|||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
|
if len(picks) > 0 {
|
||||||
|
sb.WriteString("## Picks\n\n")
|
||||||
|
sb.WriteString("These files were explicitly selected via --pick flags.\n\n")
|
||||||
|
for _, p := range picks {
|
||||||
|
fmt.Fprintf(&sb, "- `%s` (%s)\n", p.RelPath, p.SliceDir)
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
_, err = f.WriteString(sb.String())
|
_, err = f.WriteString(sb.String())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,3 +168,22 @@ var frontendExts = []string{
|
|||||||
".ts", ".tsx",
|
".ts", ".tsx",
|
||||||
".vue", ".svelte",
|
".vue", ".svelte",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RenderBreadcrumb(absRoot, relPath string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
rootName := filepath.Base(absRoot)
|
||||||
|
dir := filepath.Dir(relPath)
|
||||||
|
if dir == "." {
|
||||||
|
return rootName + "/" + filepath.Base(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(filepath.ToSlash(dir), "/")
|
||||||
|
sb.WriteString(rootName)
|
||||||
|
for _, part := range parts {
|
||||||
|
sb.WriteString(" / ")
|
||||||
|
sb.WriteString(part)
|
||||||
|
}
|
||||||
|
sb.WriteString(" / ")
|
||||||
|
sb.WriteString(filepath.Base(relPath))
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user