@dcode-dev/dcode-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM_README.md +78 -0
- package/README.md +341 -0
- package/bin/dcode-bin +0 -0
- package/bin/dcode.js +44 -0
- package/cmd/agent_v2.go +448 -0
- package/cmd/analyze.go +97 -0
- package/cmd/auth.go +338 -0
- package/cmd/compose.go +284 -0
- package/cmd/context.go +111 -0
- package/cmd/edit.go +116 -0
- package/cmd/env.go +10 -0
- package/cmd/fix.go +145 -0
- package/cmd/gemini.go +20 -0
- package/cmd/generate.go +47 -0
- package/cmd/interactive.go +33 -0
- package/cmd/mcp.go +196 -0
- package/cmd/patch.go +19 -0
- package/cmd/providers.go +67 -0
- package/cmd/root.go +41 -0
- package/cmd/search.go +61 -0
- package/cmd/server.go +36 -0
- package/cmd/switch.go +122 -0
- package/cmd/terminal.go +277 -0
- package/go.mod +42 -0
- package/go.sum +86 -0
- package/internal/agent/agent.go +332 -0
- package/internal/agent/parse.go +25 -0
- package/internal/agents/base.go +154 -0
- package/internal/agents/documenter.go +77 -0
- package/internal/agents/generalist.go +266 -0
- package/internal/agents/investigator.go +60 -0
- package/internal/agents/registry.go +34 -0
- package/internal/agents/reviewer.go +67 -0
- package/internal/agents/tester.go +73 -0
- package/internal/ai/client.go +634 -0
- package/internal/ai/tools.go +332 -0
- package/internal/auth/adc.go +108 -0
- package/internal/auth/apikey.go +67 -0
- package/internal/auth/factory.go +145 -0
- package/internal/auth/oauth2.go +227 -0
- package/internal/auth/store.go +216 -0
- package/internal/auth/types.go +79 -0
- package/internal/auth/vertex.go +138 -0
- package/internal/config/config.go +428 -0
- package/internal/config/policy.go +251 -0
- package/internal/context/builder.go +312 -0
- package/internal/detector/detector.go +204 -0
- package/internal/diffutil/diffutil.go +30 -0
- package/internal/fsutil/fsutil.go +35 -0
- package/internal/mcp/client.go +314 -0
- package/internal/mcp/manager.go +221 -0
- package/internal/policy/policy.go +89 -0
- package/internal/prompt/interactive.go +338 -0
- package/internal/registry/agent.go +201 -0
- package/internal/registry/tool.go +181 -0
- package/internal/scheduler/scheduler.go +250 -0
- package/internal/server/server.go +167 -0
- package/internal/tools/file.go +183 -0
- package/internal/tools/filesystem.go +286 -0
- package/internal/tools/git.go +355 -0
- package/internal/tools/memory.go +269 -0
- package/internal/tools/registry.go +49 -0
- package/internal/tools/search.go +230 -0
- package/internal/tools/shell.go +84 -0
- package/internal/websearch/search.go +40 -0
- package/internal/websearch/tavily.go +79 -0
- package/main.go +19 -0
- package/package.json +57 -0
- package/scripts/install.js +59 -0
- package/scripts/uninstall.js +28 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
package context
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"sort"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/ddhanush1/dcode/internal/detector"
|
|
12
|
+
"github.com/ddhanush1/dcode/internal/fsutil"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// ProjectContext holds information about the current project
|
|
16
|
+
type ProjectContext struct {
|
|
17
|
+
RootDir string
|
|
18
|
+
ProjectType *detector.ProjectType
|
|
19
|
+
RelevantFiles []FileInfo
|
|
20
|
+
Summary string
|
|
21
|
+
UpdatedAt time.Time
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// FileInfo contains metadata about a file
|
|
25
|
+
type FileInfo struct {
|
|
26
|
+
Path string
|
|
27
|
+
RelPath string
|
|
28
|
+
Size int64
|
|
29
|
+
ModTime time.Time
|
|
30
|
+
Language string
|
|
31
|
+
Relevance float64 // 0.0 to 1.0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Builder builds project context
|
|
35
|
+
type Builder struct {
|
|
36
|
+
rootDir string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// NewBuilder creates a new context builder
|
|
40
|
+
func NewBuilder(rootDir string) *Builder {
|
|
41
|
+
return &Builder{rootDir: rootDir}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build analyzes the project and builds context
|
|
45
|
+
func (b *Builder) Build() (*ProjectContext, error) {
|
|
46
|
+
// Detect project type
|
|
47
|
+
projectType, err := detector.Detect(b.rootDir)
|
|
48
|
+
if err != nil {
|
|
49
|
+
return nil, fmt.Errorf("failed to detect project: %w", err)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get relevant files
|
|
53
|
+
files, err := b.getRelevantFiles(100) // Limit to 100 files
|
|
54
|
+
if err != nil {
|
|
55
|
+
return nil, fmt.Errorf("failed to scan files: %w", err)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Score and sort files by relevance
|
|
59
|
+
b.scoreFiles(files, projectType)
|
|
60
|
+
sort.Slice(files, func(i, j int) bool {
|
|
61
|
+
return files[i].Relevance > files[j].Relevance
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Generate summary
|
|
65
|
+
summary := b.generateSummary(projectType, files)
|
|
66
|
+
|
|
67
|
+
ctx := &ProjectContext{
|
|
68
|
+
RootDir: b.rootDir,
|
|
69
|
+
ProjectType: projectType,
|
|
70
|
+
RelevantFiles: files,
|
|
71
|
+
Summary: summary,
|
|
72
|
+
UpdatedAt: time.Now(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return ctx, nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func (b *Builder) getRelevantFiles(maxFiles int) ([]FileInfo, error) {
|
|
79
|
+
var files []FileInfo
|
|
80
|
+
|
|
81
|
+
err := filepath.Walk(b.rootDir, func(path string, info os.FileInfo, err error) error {
|
|
82
|
+
if err != nil {
|
|
83
|
+
return nil // Skip errors
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if info.IsDir() {
|
|
87
|
+
// Skip common directories
|
|
88
|
+
name := info.Name()
|
|
89
|
+
if name == ".git" || name == "node_modules" || name == "vendor" ||
|
|
90
|
+
name == "__pycache__" || name == "dist" || name == "build" {
|
|
91
|
+
return filepath.SkipDir
|
|
92
|
+
}
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if file is relevant
|
|
97
|
+
if detector.IsRelevantFile(path) {
|
|
98
|
+
relPath, _ := filepath.Rel(b.rootDir, path)
|
|
99
|
+
files = append(files, FileInfo{
|
|
100
|
+
Path: path,
|
|
101
|
+
RelPath: relPath,
|
|
102
|
+
Size: info.Size(),
|
|
103
|
+
ModTime: info.ModTime(),
|
|
104
|
+
Language: detectLanguage(path),
|
|
105
|
+
Relevance: 0.5, // Default relevance
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Limit number of files
|
|
109
|
+
if maxFiles > 0 && len(files) >= maxFiles {
|
|
110
|
+
return filepath.SkipDir
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return nil
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if err != nil && err != filepath.SkipDir {
|
|
118
|
+
return nil, err
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return files, nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func (b *Builder) scoreFiles(files []FileInfo, projectType *detector.ProjectType) {
|
|
125
|
+
for i := range files {
|
|
126
|
+
score := 0.5 // Base score
|
|
127
|
+
|
|
128
|
+
path := files[i].RelPath
|
|
129
|
+
base := filepath.Base(path)
|
|
130
|
+
|
|
131
|
+
// Entry points get highest priority
|
|
132
|
+
for _, ep := range projectType.EntryPoints {
|
|
133
|
+
if base == ep {
|
|
134
|
+
score = 1.0
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Config files are important
|
|
140
|
+
for _, cf := range projectType.ConfigFiles {
|
|
141
|
+
if base == cf {
|
|
142
|
+
score = 0.9
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Files in root directory are more relevant
|
|
148
|
+
if !strings.Contains(path, string(filepath.Separator)) {
|
|
149
|
+
score += 0.2
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Language-specific scoring
|
|
153
|
+
if files[i].Language == projectType.Language {
|
|
154
|
+
score += 0.1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Common important files
|
|
158
|
+
switch base {
|
|
159
|
+
case "README.md", "README.txt", "README":
|
|
160
|
+
score = 0.95
|
|
161
|
+
case "Makefile", "Dockerfile", ".env":
|
|
162
|
+
score = 0.85
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Boost test files slightly
|
|
166
|
+
if strings.Contains(base, "_test") || strings.Contains(base, ".test.") {
|
|
167
|
+
score += 0.05
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Penalize very large files
|
|
171
|
+
if files[i].Size > 100*1024 { // > 100KB
|
|
172
|
+
score -= 0.1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Penalize generated files
|
|
176
|
+
if strings.Contains(path, "generated") || strings.Contains(path, "gen") {
|
|
177
|
+
score -= 0.2
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Ensure score is in valid range
|
|
181
|
+
if score < 0 {
|
|
182
|
+
score = 0
|
|
183
|
+
}
|
|
184
|
+
if score > 1 {
|
|
185
|
+
score = 1
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
files[i].Relevance = score
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func (b *Builder) generateSummary(projectType *detector.ProjectType, files []FileInfo) string {
|
|
193
|
+
var sb strings.Builder
|
|
194
|
+
|
|
195
|
+
sb.WriteString("# Project Overview\n\n")
|
|
196
|
+
|
|
197
|
+
// Project type info
|
|
198
|
+
if projectType.Language != "" {
|
|
199
|
+
sb.WriteString(fmt.Sprintf("**Language:** %s\n", projectType.Language))
|
|
200
|
+
}
|
|
201
|
+
if projectType.Framework != "" {
|
|
202
|
+
sb.WriteString(fmt.Sprintf("**Framework:** %s\n", projectType.Framework))
|
|
203
|
+
}
|
|
204
|
+
if projectType.BuildSystem != "" {
|
|
205
|
+
sb.WriteString(fmt.Sprintf("**Build System:** %s\n", projectType.BuildSystem))
|
|
206
|
+
}
|
|
207
|
+
sb.WriteString(fmt.Sprintf("**Total Files:** %d\n\n", len(files)))
|
|
208
|
+
|
|
209
|
+
// Entry points
|
|
210
|
+
if len(projectType.EntryPoints) > 0 {
|
|
211
|
+
sb.WriteString("**Entry Points:**\n")
|
|
212
|
+
for _, ep := range projectType.EntryPoints {
|
|
213
|
+
sb.WriteString(fmt.Sprintf("- %s\n", ep))
|
|
214
|
+
}
|
|
215
|
+
sb.WriteString("\n")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Top relevant files
|
|
219
|
+
sb.WriteString("**Key Files (by relevance):**\n")
|
|
220
|
+
count := 0
|
|
221
|
+
for _, f := range files {
|
|
222
|
+
if count >= 10 { // Show top 10
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
sb.WriteString(fmt.Sprintf("- %s (%.1f%%)\n", f.RelPath, f.Relevance*100))
|
|
226
|
+
count++
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// File type breakdown
|
|
230
|
+
langCount := make(map[string]int)
|
|
231
|
+
for _, f := range files {
|
|
232
|
+
if f.Language != "" {
|
|
233
|
+
langCount[f.Language]++
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if len(langCount) > 0 {
|
|
237
|
+
sb.WriteString("\n**File Types:**\n")
|
|
238
|
+
for lang, count := range langCount {
|
|
239
|
+
sb.WriteString(fmt.Sprintf("- %s: %d files\n", lang, count))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return sb.String()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func detectLanguage(path string) string {
|
|
247
|
+
ext := filepath.Ext(path)
|
|
248
|
+
switch ext {
|
|
249
|
+
case ".go":
|
|
250
|
+
return "go"
|
|
251
|
+
case ".py":
|
|
252
|
+
return "python"
|
|
253
|
+
case ".js":
|
|
254
|
+
return "javascript"
|
|
255
|
+
case ".ts":
|
|
256
|
+
return "typescript"
|
|
257
|
+
case ".tsx":
|
|
258
|
+
return "tsx"
|
|
259
|
+
case ".jsx":
|
|
260
|
+
return "jsx"
|
|
261
|
+
case ".java":
|
|
262
|
+
return "java"
|
|
263
|
+
case ".rs":
|
|
264
|
+
return "rust"
|
|
265
|
+
case ".c", ".h":
|
|
266
|
+
return "c"
|
|
267
|
+
case ".cpp", ".hpp":
|
|
268
|
+
return "cpp"
|
|
269
|
+
case ".rb":
|
|
270
|
+
return "ruby"
|
|
271
|
+
case ".php":
|
|
272
|
+
return "php"
|
|
273
|
+
case ".cs":
|
|
274
|
+
return "csharp"
|
|
275
|
+
case ".md":
|
|
276
|
+
return "markdown"
|
|
277
|
+
case ".json":
|
|
278
|
+
return "json"
|
|
279
|
+
case ".yaml", ".yml":
|
|
280
|
+
return "yaml"
|
|
281
|
+
case ".toml":
|
|
282
|
+
return "toml"
|
|
283
|
+
case ".sql":
|
|
284
|
+
return "sql"
|
|
285
|
+
default:
|
|
286
|
+
return ""
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// GetTopFiles returns the top N most relevant files
|
|
291
|
+
func (ctx *ProjectContext) GetTopFiles(n int) []FileInfo {
|
|
292
|
+
if n > len(ctx.RelevantFiles) {
|
|
293
|
+
n = len(ctx.RelevantFiles)
|
|
294
|
+
}
|
|
295
|
+
return ctx.RelevantFiles[:n]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ReadTopFiles reads content of top N files
|
|
299
|
+
func (ctx *ProjectContext) ReadTopFiles(n int) (map[string]string, error) {
|
|
300
|
+
topFiles := ctx.GetTopFiles(n)
|
|
301
|
+
contents := make(map[string]string)
|
|
302
|
+
|
|
303
|
+
for _, f := range topFiles {
|
|
304
|
+
content, err := fsutil.ReadFile(f.Path)
|
|
305
|
+
if err != nil {
|
|
306
|
+
continue // Skip files we can't read
|
|
307
|
+
}
|
|
308
|
+
contents[f.RelPath] = content
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return contents, nil
|
|
312
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
package detector
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ProjectType represents the detected project type
|
|
10
|
+
type ProjectType struct {
|
|
11
|
+
Language string // go, python, javascript, typescript, etc.
|
|
12
|
+
Framework string // gin, django, react, nextjs, etc.
|
|
13
|
+
BuildSystem string // go.mod, package.json, requirements.txt, etc.
|
|
14
|
+
EntryPoints []string // main files
|
|
15
|
+
ConfigFiles []string // important config files
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Detect analyzes a directory and determines project type
|
|
19
|
+
func Detect(dir string) (*ProjectType, error) {
|
|
20
|
+
pt := &ProjectType{
|
|
21
|
+
EntryPoints: make([]string, 0),
|
|
22
|
+
ConfigFiles: make([]string, 0),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for various project indicators
|
|
26
|
+
files, err := os.ReadDir(dir)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return nil, err
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for _, file := range files {
|
|
32
|
+
name := file.Name()
|
|
33
|
+
|
|
34
|
+
// Skip hidden files/directories
|
|
35
|
+
if strings.HasPrefix(name, ".") && name != ".env" {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
switch name {
|
|
40
|
+
// Go
|
|
41
|
+
case "go.mod":
|
|
42
|
+
pt.Language = "go"
|
|
43
|
+
pt.BuildSystem = "go.mod"
|
|
44
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
45
|
+
|
|
46
|
+
// Python
|
|
47
|
+
case "requirements.txt", "pyproject.toml", "setup.py", "Pipfile":
|
|
48
|
+
pt.Language = "python"
|
|
49
|
+
pt.BuildSystem = name
|
|
50
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
51
|
+
|
|
52
|
+
// Node.js
|
|
53
|
+
case "package.json":
|
|
54
|
+
pt.Language = "javascript"
|
|
55
|
+
pt.BuildSystem = "npm"
|
|
56
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
57
|
+
|
|
58
|
+
// Rust
|
|
59
|
+
case "Cargo.toml":
|
|
60
|
+
pt.Language = "rust"
|
|
61
|
+
pt.BuildSystem = "cargo"
|
|
62
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
63
|
+
|
|
64
|
+
// Java
|
|
65
|
+
case "pom.xml":
|
|
66
|
+
pt.Language = "java"
|
|
67
|
+
pt.BuildSystem = "maven"
|
|
68
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
69
|
+
case "build.gradle":
|
|
70
|
+
pt.Language = "java"
|
|
71
|
+
pt.BuildSystem = "gradle"
|
|
72
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
73
|
+
|
|
74
|
+
// Common entry points
|
|
75
|
+
case "main.go":
|
|
76
|
+
pt.EntryPoints = append(pt.EntryPoints, name)
|
|
77
|
+
case "main.py", "app.py", "manage.py":
|
|
78
|
+
pt.EntryPoints = append(pt.EntryPoints, name)
|
|
79
|
+
case "index.js", "index.ts", "app.js", "app.ts":
|
|
80
|
+
pt.EntryPoints = append(pt.EntryPoints, name)
|
|
81
|
+
|
|
82
|
+
// Config files
|
|
83
|
+
case ".env", "Dockerfile", "docker-compose.yml", "Makefile":
|
|
84
|
+
pt.ConfigFiles = append(pt.ConfigFiles, name)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Detect framework based on package.json, go.mod, etc.
|
|
89
|
+
pt.Framework = detectFramework(dir, pt.Language)
|
|
90
|
+
|
|
91
|
+
return pt, nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func detectFramework(dir, lang string) string {
|
|
95
|
+
switch lang {
|
|
96
|
+
case "go":
|
|
97
|
+
// Check go.mod for common frameworks
|
|
98
|
+
if gomod, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil {
|
|
99
|
+
content := string(gomod)
|
|
100
|
+
if strings.Contains(content, "github.com/gin-gonic/gin") {
|
|
101
|
+
return "gin"
|
|
102
|
+
}
|
|
103
|
+
if strings.Contains(content, "github.com/labstack/echo") {
|
|
104
|
+
return "echo"
|
|
105
|
+
}
|
|
106
|
+
if strings.Contains(content, "github.com/gofiber/fiber") {
|
|
107
|
+
return "fiber"
|
|
108
|
+
}
|
|
109
|
+
if strings.Contains(content, "github.com/spf13/cobra") {
|
|
110
|
+
return "cobra-cli"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "python":
|
|
115
|
+
// Check requirements.txt
|
|
116
|
+
if req, err := os.ReadFile(filepath.Join(dir, "requirements.txt")); err == nil {
|
|
117
|
+
content := strings.ToLower(string(req))
|
|
118
|
+
if strings.Contains(content, "django") {
|
|
119
|
+
return "django"
|
|
120
|
+
}
|
|
121
|
+
if strings.Contains(content, "flask") {
|
|
122
|
+
return "flask"
|
|
123
|
+
}
|
|
124
|
+
if strings.Contains(content, "fastapi") {
|
|
125
|
+
return "fastapi"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "javascript":
|
|
130
|
+
// Check package.json
|
|
131
|
+
if pkg, err := os.ReadFile(filepath.Join(dir, "package.json")); err == nil {
|
|
132
|
+
content := string(pkg)
|
|
133
|
+
if strings.Contains(content, "\"react\"") {
|
|
134
|
+
if strings.Contains(content, "\"next\"") {
|
|
135
|
+
return "nextjs"
|
|
136
|
+
}
|
|
137
|
+
return "react"
|
|
138
|
+
}
|
|
139
|
+
if strings.Contains(content, "\"vue\"") {
|
|
140
|
+
return "vue"
|
|
141
|
+
}
|
|
142
|
+
if strings.Contains(content, "\"express\"") {
|
|
143
|
+
return "express"
|
|
144
|
+
}
|
|
145
|
+
if strings.Contains(content, "\"@nestjs") {
|
|
146
|
+
return "nestjs"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ""
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// IsRelevantFile determines if a file is relevant for AI context
|
|
155
|
+
func IsRelevantFile(path string) bool {
|
|
156
|
+
// Skip common non-relevant patterns
|
|
157
|
+
skipPatterns := []string{
|
|
158
|
+
"node_modules/", "vendor/", ".git/", "__pycache__/",
|
|
159
|
+
".cache/", "dist/", "build/", ".next/", "target/",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for _, pattern := range skipPatterns {
|
|
163
|
+
if strings.Contains(path, pattern) {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
base := filepath.Base(path)
|
|
169
|
+
// Skip binary/media/lock files
|
|
170
|
+
skipSuffixes := []string{
|
|
171
|
+
".min.js", ".map", ".png", ".jpg", ".jpeg", ".gif",
|
|
172
|
+
".ico", ".woff", ".woff2", ".ttf", ".eot",
|
|
173
|
+
".lock", "go.sum", "package-lock.json", "yarn.lock",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for _, suffix := range skipSuffixes {
|
|
177
|
+
if strings.HasSuffix(base, suffix) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Include common source code extensions
|
|
183
|
+
ext := filepath.Ext(path)
|
|
184
|
+
relevantExts := []string{
|
|
185
|
+
".go", ".py", ".js", ".ts", ".tsx", ".jsx",
|
|
186
|
+
".java", ".c", ".cpp", ".h", ".hpp",
|
|
187
|
+
".rs", ".rb", ".php", ".cs",
|
|
188
|
+
".sql", ".md", ".txt", ".yml", ".yaml",
|
|
189
|
+
".json", ".toml", ".xml", ".html", ".css", ".scss",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for _, e := range relevantExts {
|
|
193
|
+
if ext == e {
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Include files without extensions (likely scripts or configs)
|
|
199
|
+
if ext == "" && !strings.HasPrefix(base, ".") {
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package diffutil
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
|
|
6
|
+
"github.com/pmezard/go-difflib/difflib"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func Unified(fromName, toName, fromContent, toContent string) string {
|
|
10
|
+
u := difflib.UnifiedDiff{
|
|
11
|
+
A: difflib.SplitLines(normalizeNewline(fromContent)),
|
|
12
|
+
B: difflib.SplitLines(normalizeNewline(toContent)),
|
|
13
|
+
FromFile: fromName,
|
|
14
|
+
ToFile: toName,
|
|
15
|
+
Context: 3,
|
|
16
|
+
}
|
|
17
|
+
text, err := difflib.GetUnifiedDiffString(u)
|
|
18
|
+
if err != nil {
|
|
19
|
+
return "(diff unavailable)"
|
|
20
|
+
}
|
|
21
|
+
if strings.TrimSpace(text) == "" {
|
|
22
|
+
return "(no changes)"
|
|
23
|
+
}
|
|
24
|
+
return text
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func normalizeNewline(s string) string {
|
|
28
|
+
// avoid CRLF noise
|
|
29
|
+
return strings.ReplaceAll(s, "\r\n", "\n")
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package fsutil
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func ReadFile(path string) (string, error) {
|
|
11
|
+
b, err := os.ReadFile(path)
|
|
12
|
+
if err != nil {
|
|
13
|
+
return "", err
|
|
14
|
+
}
|
|
15
|
+
return string(b), nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// WriteFileAtomic writes content to path using a temp file + rename.
|
|
19
|
+
func WriteFileAtomic(path string, content []byte, perm os.FileMode) error {
|
|
20
|
+
dir := filepath.Dir(path)
|
|
21
|
+
base := filepath.Base(path)
|
|
22
|
+
tmp := filepath.Join(dir, fmt.Sprintf(".%s.%d.tmp", base, time.Now().UnixNano()))
|
|
23
|
+
if err := os.WriteFile(tmp, content, perm); err != nil {
|
|
24
|
+
return err
|
|
25
|
+
}
|
|
26
|
+
return os.Rename(tmp, path)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func FilePermOrDefault(path string, def os.FileMode) os.FileMode {
|
|
30
|
+
st, err := os.Stat(path)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return def
|
|
33
|
+
}
|
|
34
|
+
return st.Mode()
|
|
35
|
+
}
|