mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 22:38:10 -05:00
feat(code_helper): add stdin support for piping file lists
Add ability to pipe file lists to code_helper via stdin, enabling use cases like: find . -name '*.go' | code_helper "instructions" git ls-files '*.py' | code_helper "Add type hints" The tool now detects if stdin is a pipe and accepts a single argument (instructions) in that mode, reading file paths from stdin line by line. Backward compatible with existing directory scanning mode. Signed-off-by: majiayu000 <1835304752@qq.com>
This commit is contained in:
@@ -131,6 +131,75 @@ func ScanDirectory(rootDir string, maxDepth int, instructions string, ignoreList
|
|||||||
return json.MarshalIndent(data, "", " ")
|
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
|
// addFileToDirectory adds a file to the correct directory in the structure
|
||||||
func addFileToDirectory(root *FileItem, path, content, rootDir string) {
|
func addFileToDirectory(root *FileItem, path, content, rootDir string) {
|
||||||
parts := strings.Split(path, string(filepath.Separator))
|
parts := strings.Split(path, string(filepath.Separator))
|
||||||
|
|||||||
100
cmd/code_helper/code_test.go
Normal file
100
cmd/code_helper/code_test.go
Normal file
@@ -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"])
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,7 +16,45 @@ func main() {
|
|||||||
flag.Usage = printUsage
|
flag.Usage = printUsage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Require exactly two positional arguments: directory and instructions
|
// 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: <instructions>\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 {
|
if flag.NArg() != 2 {
|
||||||
printUsage()
|
printUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -31,9 +70,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse ignore patterns and scan directory
|
// Parse ignore patterns and scan directory
|
||||||
jsonData, err := ScanDirectory(directory, *maxDepth, instructions, strings.Split(*ignorePatterns, ","))
|
jsonData, err = ScanDirectory(directory, *maxDepth, instructions, strings.Split(*ignorePatterns, ","))
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +94,14 @@ func printUsage() {
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
code_helper [options] <directory> <instructions>
|
code_helper [options] <directory> <instructions>
|
||||||
|
<file_list> | code_helper [options] <instructions>
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
code_helper . "Add input validation to all user inputs"
|
code_helper . "Add input validation to all user inputs"
|
||||||
code_helper -depth 4 ./my-project "Implement error handling"
|
code_helper -depth 4 ./my-project "Implement error handling"
|
||||||
code_helper -out project.json ./src "Fix security issues"
|
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:
|
Options:
|
||||||
`)
|
`)
|
||||||
|
|||||||
Reference in New Issue
Block a user