diff --git a/cmd/code_helper/code.go b/cmd/code_helper/code.go index 2f165fff..0d65d70c 100644 --- a/cmd/code_helper/code.go +++ b/cmd/code_helper/code.go @@ -131,6 +131,75 @@ func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList return json.MarshalIndent(data, "", " ") } +// ScanFiles scans specific files and returns a JSON representation +func ScanFiles(files []string, instructions string) ([]byte, error) { + fileCount := 0 + dirSet := make(map[string]bool) + + // Create root directory item + rootItem := FileItem{ + Type: "directory", + Name: ".", + Contents: []FileItem{}, + } + + for _, filePath := range files { + // Skip directories + info, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("error accessing file %s: %v", filePath, err) + } + if info.IsDir() { + continue + } + + // Track unique directories + dir := filepath.Dir(filePath) + if dir != "." { + dirSet[dir] = true + } + + fileCount++ + + // Read file content + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("error reading file %s: %v", filePath, err) + } + + // Clean path for consistent handling + cleanPath := filepath.Clean(filePath) + if strings.HasPrefix(cleanPath, "./") { + cleanPath = cleanPath[2:] + } + + // Add file to the structure + addFileToDirectory(&rootItem, cleanPath, string(content), ".") + } + + // Create final data structure + var data []any + data = append(data, rootItem) + + // Add report + reportItem := map[string]any{ + "type": "report", + "directories": len(dirSet) + 1, + "files": fileCount, + } + data = append(data, reportItem) + + // Add instructions + instructionsItem := map[string]any{ + "type": "instructions", + "name": "code_change_instructions", + "details": instructions, + } + data = append(data, instructionsItem) + + return json.MarshalIndent(data, "", " ") +} + // addFileToDirectory adds a file to the correct directory in the structure func addFileToDirectory(root *FileItem, path, content, rootDir string) { parts := strings.Split(path, string(filepath.Separator)) diff --git a/cmd/code_helper/code_test.go b/cmd/code_helper/code_test.go new file mode 100644 index 00000000..c5b67465 --- /dev/null +++ b/cmd/code_helper/code_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScanFiles(t *testing.T) { + // Create temp directory with test files + tmpDir := t.TempDir() + + // Create test files + file1 := filepath.Join(tmpDir, "test1.go") + file2 := filepath.Join(tmpDir, "test2.go") + subDir := filepath.Join(tmpDir, "subdir") + file3 := filepath.Join(subDir, "test3.go") + + require.NoError(t, os.WriteFile(file1, []byte("package main\n"), 0644)) + require.NoError(t, os.WriteFile(file2, []byte("package main\n\nfunc main() {}\n"), 0644)) + require.NoError(t, os.MkdirAll(subDir, 0755)) + require.NoError(t, os.WriteFile(file3, []byte("package subdir\n"), 0644)) + + // Test scanning specific files + files := []string{file1, file3} + instructions := "Test instructions" + + jsonData, err := ScanFiles(files, instructions) + require.NoError(t, err) + + // Parse the JSON output + var result []any + err = json.Unmarshal(jsonData, &result) + require.NoError(t, err) + assert.Len(t, result, 3) // directory, report, instructions + + // Check report + report := result[1].(map[string]any) + assert.Equal(t, "report", report["type"]) + assert.Equal(t, float64(2), report["files"]) + + // Check instructions + instr := result[2].(map[string]any) + assert.Equal(t, "instructions", instr["type"]) + assert.Equal(t, "Test instructions", instr["details"]) +} + +func TestScanFilesSkipsDirectories(t *testing.T) { + tmpDir := t.TempDir() + + file1 := filepath.Join(tmpDir, "test.go") + subDir := filepath.Join(tmpDir, "subdir") + + require.NoError(t, os.WriteFile(file1, []byte("package main\n"), 0644)) + require.NoError(t, os.MkdirAll(subDir, 0755)) + + // Include a directory in the file list - should be skipped + files := []string{file1, subDir} + + jsonData, err := ScanFiles(files, "test") + require.NoError(t, err) + + var result []any + err = json.Unmarshal(jsonData, &result) + require.NoError(t, err) + + // Check that only 1 file was counted (directory was skipped) + report := result[1].(map[string]any) + assert.Equal(t, float64(1), report["files"]) +} + +func TestScanFilesNonExistentFile(t *testing.T) { + files := []string{"/nonexistent/file.go"} + _, err := ScanFiles(files, "test") + assert.Error(t, err) + assert.Contains(t, err.Error(), "error accessing file") +} + +func TestScanDirectory(t *testing.T) { + tmpDir := t.TempDir() + + file1 := filepath.Join(tmpDir, "main.go") + require.NoError(t, os.WriteFile(file1, []byte("package main\n"), 0644)) + + jsonData, err := ScanDirectory(tmpDir, 3, "Test instructions", []string{}) + require.NoError(t, err) + + var result []any + err = json.Unmarshal(jsonData, &result) + require.NoError(t, err) + assert.Len(t, result, 3) + + // Check instructions + instr := result[2].(map[string]any) + assert.Equal(t, "Test instructions", instr["details"]) +} diff --git a/cmd/code_helper/main.go b/cmd/code_helper/main.go index 266beff7..0b34f9e9 100644 --- a/cmd/code_helper/main.go +++ b/cmd/code_helper/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "flag" "fmt" "os" @@ -15,25 +16,65 @@ func main() { flag.Usage = printUsage flag.Parse() - // Require exactly two positional arguments: directory and instructions - if flag.NArg() != 2 { - printUsage() - os.Exit(1) + // Check if stdin has data (is a pipe) + stdinInfo, _ := os.Stdin.Stat() + hasStdin := (stdinInfo.Mode() & os.ModeCharDevice) == 0 + + var jsonData []byte + var err error + + if hasStdin { + // Stdin mode: read file list from stdin, instructions from argument + if flag.NArg() != 1 { + fmt.Fprintf(os.Stderr, "Error: When piping file list via stdin, provide exactly 1 argument: \n") + fmt.Fprintf(os.Stderr, "Usage: find . -name '*.go' | code_helper \"instructions\"\n") + os.Exit(1) + } + + instructions := flag.Arg(0) + + // Read file paths from stdin + var files []string + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" { + files = append(files, line) + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) + os.Exit(1) + } + + if len(files) == 0 { + fmt.Fprintf(os.Stderr, "Error: No files provided via stdin\n") + os.Exit(1) + } + + jsonData, err = ScanFiles(files, instructions) + } else { + // Directory mode: require directory and instructions arguments + if flag.NArg() != 2 { + printUsage() + os.Exit(1) + } + + directory := flag.Arg(0) + instructions := flag.Arg(1) + + // Validate directory + if info, err := os.Stat(directory); err != nil || !info.IsDir() { + fmt.Fprintf(os.Stderr, "Error: Directory '%s' does not exist or is not a directory\n", directory) + os.Exit(1) + } + + // Parse ignore patterns and scan directory + jsonData, err = ScanDirectory(directory, *maxDepth, instructions, strings.Split(*ignorePatterns, ",")) } - directory := flag.Arg(0) - instructions := flag.Arg(1) - - // Validate directory - if info, err := os.Stat(directory); err != nil || !info.IsDir() { - fmt.Fprintf(os.Stderr, "Error: Directory '%s' does not exist or is not a directory\n", directory) - os.Exit(1) - } - - // Parse ignore patterns and scan directory - jsonData, err := ScanDirectory(directory, *maxDepth, instructions, strings.Split(*ignorePatterns, ",")) if err != nil { - fmt.Fprintf(os.Stderr, "Error scanning directory: %v\n", err) + fmt.Fprintf(os.Stderr, "Error scanning: %v\n", err) os.Exit(1) } @@ -53,11 +94,14 @@ func printUsage() { Usage: code_helper [options] + | code_helper [options] Examples: code_helper . "Add input validation to all user inputs" code_helper -depth 4 ./my-project "Implement error handling" code_helper -out project.json ./src "Fix security issues" + find . -name '*.go' | code_helper "Refactor error handling" + git ls-files '*.py' | code_helper "Add type hints" Options: `)