1package prompt
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "sync"
8
9 "github.com/charmbracelet/crush/internal/resolver"
10)
11
12type PromptID string
13
14const (
15 PromptCoder PromptID = "coder"
16 PromptTitle PromptID = "title"
17 PromptTask PromptID = "task"
18 PromptSummarizer PromptID = "summarizer"
19 PromptDefault PromptID = "default"
20)
21
22func getContextFromPaths(workingDir string, contextPaths []string) string {
23 return processContextPaths(workingDir, contextPaths)
24}
25
26// expandPath expands ~ and environment variables in file paths
27func expandPath(path string) string {
28 // Handle tilde expansion
29 if strings.HasPrefix(path, "~/") {
30 homeDir, err := os.UserHomeDir()
31 if err == nil {
32 path = filepath.Join(homeDir, path[2:])
33 }
34 } else if path == "~" {
35 homeDir, err := os.UserHomeDir()
36 if err == nil {
37 path = homeDir
38 }
39 }
40
41 // Handle environment variable expansion using the same pattern as config
42 if strings.HasPrefix(path, "$") {
43 resolver := resolver.New()
44 if expanded, err := resolver.ResolveValue(path); err == nil {
45 path = expanded
46 }
47 }
48
49 return path
50}
51
52func processContextPaths(workDir string, paths []string) string {
53 var (
54 wg sync.WaitGroup
55 resultCh = make(chan string)
56 )
57
58 // Track processed files to avoid duplicates
59 processedFiles := make(map[string]bool)
60 var processedMutex sync.Mutex
61
62 for _, path := range paths {
63 wg.Add(1)
64 go func(p string) {
65 defer wg.Done()
66
67 // Expand ~ and environment variables before processing
68 p = expandPath(p)
69
70 // Use absolute path if provided, otherwise join with workDir
71 fullPath := p
72 if !filepath.IsAbs(p) {
73 fullPath = filepath.Join(workDir, p)
74 }
75
76 // Check if the path is a directory using os.Stat
77 info, err := os.Stat(fullPath)
78 if err != nil {
79 return // Skip if path doesn't exist or can't be accessed
80 }
81
82 if info.IsDir() {
83 filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
84 if err != nil {
85 return err
86 }
87 if !d.IsDir() {
88 // Check if we've already processed this file (case-insensitive)
89 lowerPath := strings.ToLower(path)
90
91 processedMutex.Lock()
92 alreadyProcessed := processedFiles[lowerPath]
93 if !alreadyProcessed {
94 processedFiles[lowerPath] = true
95 }
96 processedMutex.Unlock()
97
98 if !alreadyProcessed {
99 if result := processFile(path); result != "" {
100 resultCh <- result
101 }
102 }
103 }
104 return nil
105 })
106 } else {
107 // It's a file, process it directly
108 // Check if we've already processed this file (case-insensitive)
109 lowerPath := strings.ToLower(fullPath)
110
111 processedMutex.Lock()
112 alreadyProcessed := processedFiles[lowerPath]
113 if !alreadyProcessed {
114 processedFiles[lowerPath] = true
115 }
116 processedMutex.Unlock()
117
118 if !alreadyProcessed {
119 result := processFile(fullPath)
120 if result != "" {
121 resultCh <- result
122 }
123 }
124 }
125 }(path)
126 }
127
128 go func() {
129 wg.Wait()
130 close(resultCh)
131 }()
132
133 results := make([]string, 0)
134 for result := range resultCh {
135 results = append(results, result)
136 }
137
138 return strings.Join(results, "\n")
139}
140
141func processFile(filePath string) string {
142 content, err := os.ReadFile(filePath)
143 if err != nil {
144 return ""
145 }
146 return "# From:" + filePath + "\n" + string(content)
147}