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