├── .github └── workflows │ ├── build.yaml │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal └── codetoprompt │ ├── codetoprompt.go │ ├── codetoprompt_test.go │ └── testdata │ ├── dir_foo │ ├── dir_to_ignore_qux │ │ └── file_qux.txt │ ├── file_bar.txt │ └── file_to_ignore_bar.txt │ └── file_foo.txt └── main.go /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build executables 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | persist-credentials: false 17 | fetch-depth: 0 18 | 19 | - name: Setup Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: '1.20' 23 | 24 | - name: Build executables 25 | run: | 26 | go build -o codetoprompt-linux-amd64 27 | OOS=windows GOARCH=amd64 go build -o codetoprompt-windows-amd64.exe 28 | GOOS=darwin GOARCH=amd64 go build -o codetoprompt-darwin-amd64 29 | 30 | - name: Upload executables for Linux 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: codetoprompt-linux-amd64 34 | path: codetoprompt-linux-amd64 35 | 36 | - name: Upload executables for Windows 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: codetoprompt-windows-amd64.exe 40 | path: codetoprompt-windows-amd64.exe 41 | 42 | - name: Upload executables for macOS 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: codetoprompt-darwin-amd64 46 | path: codetoprompt-darwin-amd64 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | linter: 10 | name: linter 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 1 16 | - name: vet & fmt 17 | run: | 18 | go vet ./... 19 | go fmt ./... 20 | 21 | unit-tests: 22 | runs-on: ubuntu-latest 23 | name: unit-tests 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | with: 29 | persist-credentials: false 30 | fetch-depth: 0 31 | 32 | - name: setup go 33 | uses: actions/setup-go@v2 34 | with: 35 | go-version: '1.20' 36 | 37 | - uses: actions/cache@v2 38 | with: 39 | path: ~/go/pkg/mod 40 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 41 | restore-keys: | 42 | ${{ runner.os }}-go- 43 | 44 | - name: unit tests 45 | run: go test -v -race -vet=all -count=1 ./... 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alessandro Resta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [CTP] codetoprompt 2 | 3 | [CTP] codetoprompt is a Go package that loads files from a specified directory and outputs their contents in a format that can be used as a prompt for chat-GPT questions. The package can either display the output in the terminal or write it to a file. 4 | 5 | The package accepts flags to specify the directory to load files from, the file to output the results to (if any), and which directories or files to exclude. Additionally, the package can optionally include or exclude blank lines in the output. 6 | 7 | ## Installation 8 | 9 | If you have Go installed, you can install the package by running the following command: 10 | 11 | ```bash 12 | $ go install github.com/alesr/codetoprompt@latest 13 | ``` 14 | 15 | You can also use Homebrew to install the package: 16 | 17 | ```bash 18 | brew install --build-bottle alesr/codetoprompt/ctp 19 | ``` 20 | 21 | Alternatively, you can download the binary for your platform from the artifacts built by the CI pipeline. The latest version can be found [here](https://github.com/alesr/codetoprompt/releases/tag/v1.0.0) 22 | 23 | 24 | As a suggestion is to rename the binary to `ctp` and add it to your path. 25 | 26 | ## Flags 27 | 28 | The following flags are available: 29 | 30 | ``` 31 | Usage of codetoprompt: 32 | -blanklines 33 | include blank lines in the output file (default true) 34 | -dir string 35 | the root directory you want to load files from 36 | -exclude string 37 | exclude a directory or a file from the output file 38 | -out string 39 | output destination with possible values: clipboard | stdout | file. The default value is set to clipboard. 40 | -path string 41 | the filepath to the output file 42 | ``` 43 | 44 | 45 | ## Usage 46 | 47 | To use the package, run the following command: 48 | 49 | ```shell 50 | $ ctp -dir . -out stdout -exclude go.mod,go.sum,.git,LICENSE,.gitignore,README.md,.github 51 | ``` 52 | 53 | The above command will load all files from the current directory, excluding the files specified in the exclude flag, and output the results to the file specified in the out flag. 54 | 55 | ### Output Example 56 | 57 | The output of the above command will be in the following format: 58 | 59 | ``` 60 | --- 61 | Filename: codetoprompt.go 62 | 63 | package codetoprompt 64 | import ( 65 | "errors" 66 | "flag" 67 | "fmt" 68 | "io" 69 | "os" 70 | "path/filepath" 71 | "strings" 72 | ) 73 | const ( 74 | fileOverwritePrompt string = "The output file already exists. Do you want to overwrite it? (y/n)" 75 | ) 76 | var ( 77 | dir string 78 | outputPath string 79 | exclude string 80 | excludeBlankLines bool 81 | errOutputFileAlreadyExists = errors.New("output file already exists") 82 | ) 83 | type file struct { 84 | name string 85 | path string 86 | content []byte 87 | } 88 | func parseFlags() { 89 | flag.StringVar(&dir, "dir", "", "the root directory you want to load files from") 90 | flag.StringVar(&outputPath, "out", "", "the filepath to the output file, if not provided, the output will be displayed in the terminal") 91 | flag.StringVar(&exclude, "exclude", "", "exclude a directory or a file from the output file") 92 | flag.BoolVar(&excludeBlankLines, "blanklines", true, "include blank lines in the output file") 93 | flag.Parse() 94 | } 95 | func Run() error { 96 | parseFlags() 97 | if err := validateFlagsInput(); err != nil { 98 | if !errors.Is(err, errOutputFileAlreadyExists) { 99 | return fmt.Errorf("error validating flags: %w", err) 100 | } 101 | if !overwriteFile(outputPath) { 102 | fmt.Println("Aborting...") 103 | os.Exit(0) 104 | } 105 | } 106 | ignoreList := strings.Split(exclude, ",") 107 | if outputPath != "" { 108 | // Adding the output file to the ignore list to avoid writing it to itself 109 | ignoreList = append(ignoreList, outputPath) 110 | } 111 | files, err := loadFiles(dir, ignoreList) 112 | if err != nil { 113 | return fmt.Errorf("error loading files: %w", err) 114 | } 115 | if outputPath != "" { 116 | fmt.Println("Writing files to output file...") 117 | outputFile, err := createFile(outputPath) 118 | if err != nil { 119 | return fmt.Errorf("error creating output file: %w", err) 120 | } 121 | if err := writeFiles(files, outputFile, excludeBlankLines); err != nil { 122 | return fmt.Errorf("error writing files: %w", err) 123 | } 124 | os.Exit(0) 125 | } 126 | for _, file := range files { 127 | fmt.Println("Filename: ", file.name) 128 | fmt.Println(string(file.content)) 129 | fmt.Println("---") 130 | } 131 | return nil 132 | } 133 | func validateFlagsInput() error { 134 | if dir == "" { 135 | return errors.New("root path not provided") 136 | } 137 | if outputPath != "" { 138 | alreadyExists, err := fileAlreadyExists(outputPath) 139 | if err != nil { 140 | return fmt.Errorf("error checking if output file exists: %w", err) 141 | } 142 | if alreadyExists { 143 | return errOutputFileAlreadyExists 144 | } 145 | } 146 | return nil 147 | } 148 | func fileAlreadyExists(filePath string) (bool, error) { 149 | if _, err := os.Stat(filePath); err != nil { 150 | if os.IsNotExist(err) { 151 | return false, nil 152 | } 153 | return false, fmt.Errorf("error checking if file exists: %w", err) 154 | } 155 | return true, nil 156 | } 157 | func overwriteFile(filePath string) bool { 158 | if _, err := os.Stat(filePath); err == nil { 159 | var input string 160 | fmt.Println(fileOverwritePrompt) 161 | fmt.Scanln(&input) 162 | switch strings.ToLower(input) { 163 | case "y", "yes": 164 | return true 165 | } 166 | } 167 | return false 168 | } 169 | func loadFiles(rootPath string, ignoreList []string) ([]file, error) { 170 | var files []file 171 | if err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { 172 | if err != nil { 173 | return fmt.Errorf("could not access path %q: %v", path, err) 174 | } 175 | if info.IsDir() { 176 | for _, item := range ignoreList { 177 | if info.Name() == item { 178 | return filepath.SkipDir 179 | } 180 | } 181 | } 182 | if !info.IsDir() { 183 | for _, item := range ignoreList { 184 | if info.Name() == item { 185 | return nil 186 | } 187 | } 188 | content, err := os.ReadFile(path) 189 | if err != nil { 190 | return fmt.Errorf("could not read file %q: %v", path, err) 191 | } 192 | files = append(files, file{ 193 | name: info.Name(), 194 | path: path, 195 | content: content, 196 | }) 197 | } 198 | return nil 199 | }); err != nil { 200 | return nil, fmt.Errorf("could not walk the path %q: %v", rootPath, err) 201 | } 202 | return files, nil 203 | } 204 | func createFile(outputPath string) (*os.File, error) { 205 | file, err := os.Create(outputPath) 206 | if err != nil { 207 | return nil, fmt.Errorf("could not create file: %w", err) 208 | } 209 | return file, nil 210 | } 211 | func writeFiles(files []file, output io.WriteCloser, includeBlanklines bool) error { 212 | defer output.Close() 213 | if _, err := output.Write([]byte("---\n")); err != nil { 214 | return fmt.Errorf("could not write opening code separator for file: %w", err) 215 | } 216 | for _, file := range files { 217 | var strBulider strings.Builder 218 | strBulider.WriteString("Filename: ") 219 | strBulider.WriteString(file.name) 220 | strBulider.WriteString("\n\n") 221 | if includeBlanklines { 222 | for _, line := range strings.Split(string(file.content), "\n") { 223 | if line == "" { 224 | continue 225 | } 226 | strBulider.WriteString(line) 227 | strBulider.WriteString("\n") 228 | } 229 | } else { 230 | strBulider.WriteString(string(file.content)) 231 | } 232 | strBulider.WriteString("---\n") 233 | if _, err := output.Write([]byte(strBulider.String())); err != nil { 234 | return fmt.Errorf("could not write file %q: %v", file.name, err) 235 | } 236 | } 237 | return nil 238 | } 239 | --- 240 | Filename: main.go 241 | 242 | package main 243 | import ( 244 | "log" 245 | "github.com/alesr/codetoprompt/internal/codetoprompt" 246 | ) 247 | func main() { 248 | if err := codetoprompt.Run(); err != nil { 249 | log.Fatalln(err) 250 | } 251 | } 252 | --- 253 | ``` 254 | 255 | ## License 256 | 257 | [MIT](https://choosealicense.com/licenses/mit/) 258 | 259 | ## Contributing 260 | 261 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 262 | 263 | Please make sure to update tests as appropriate. 264 | 265 | ## Authors 266 | 267 | - [@alesr](https://www.github.com/alesr) 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alesr/codetoprompt 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/atotto/clipboard v0.1.4 7 | github.com/stretchr/testify v1.8.2 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 14 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /internal/codetoprompt/codetoprompt.go: -------------------------------------------------------------------------------- 1 | package codetoprompt 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/atotto/clipboard" 14 | ) 15 | 16 | const ( 17 | fileOverwritePrompt string = "The output file already exists. Do you want to overwrite it? (y/n)" 18 | ) 19 | 20 | var ( 21 | dir string 22 | outputDest string 23 | outputPath string 24 | exclude string 25 | excludeBlankLines bool 26 | 27 | errOutputFileAlreadyExists error = errors.New("output file already exists") 28 | errNoFilesToWrite error = errors.New("no files to write") 29 | errNoOutputFileProvided error = errors.New("no output file provided") 30 | errNoFilePathProvided error = errors.New("no file path provided") 31 | errNoRootPathProvided error = errors.New("no root path provided") 32 | ) 33 | 34 | type file struct { 35 | name string 36 | path string 37 | content []byte 38 | } 39 | 40 | func parseFlags() { 41 | flag.StringVar(&dir, "dir", "", "the root directory you want to load files from") 42 | flag.StringVar(&outputDest, "out", "", "output destination with possible values: clipboard | stdout | file. The default value is set to clipboard.") 43 | flag.StringVar(&outputPath, "path", "", "the filepath to the output file") 44 | flag.StringVar(&exclude, "exclude", "", "exclude a directory or a file from the output file") 45 | flag.BoolVar(&excludeBlankLines, "blanklines", true, "include blank lines in the output file") 46 | 47 | flag.Parse() 48 | } 49 | 50 | // Run is the entry point to the program. 51 | // It parses the flags, validates them, and loads the files 52 | // If the output path is not empty, it writes the files to the output path 53 | // Otherwise, it prints the files to the console 54 | func Run() error { 55 | 56 | parseFlags() 57 | 58 | if err := validateFlagsInput(); err != nil { 59 | if !errors.Is(err, errOutputFileAlreadyExists) { 60 | return fmt.Errorf("error validating flags: %w", err) 61 | } 62 | 63 | if !overwriteFile(outputPath) { 64 | fmt.Println("Aborting...") 65 | os.Exit(0) 66 | } 67 | } 68 | 69 | ignoreList := strings.Split(exclude, ",") 70 | 71 | if outputPath != "" { 72 | // Adding the output file to the ignore list to avoid writing it to itself 73 | ignoreList = append(ignoreList, outputPath) 74 | } 75 | 76 | files, err := loadFiles(dir, ignoreList) 77 | if err != nil { 78 | return fmt.Errorf("error loading files: %w", err) 79 | } 80 | 81 | if outputDest == "file" && outputPath != "" { 82 | 83 | fmt.Println("Writing files to output file...") 84 | outputFile, err := createFile(outputPath) 85 | if err != nil { 86 | return fmt.Errorf("error creating output file: %w", err) 87 | } 88 | 89 | defer outputFile.Close() 90 | 91 | if err := writeFiles(files, outputFile, excludeBlankLines); err != nil { 92 | return fmt.Errorf("error writing files: %w", err) 93 | } 94 | os.Exit(0) 95 | } 96 | 97 | if outputDest == "stdout" { 98 | for _, file := range files { 99 | fmt.Println("Filename: ", file.name) 100 | fmt.Println(string(file.content)) 101 | fmt.Println("---") 102 | } 103 | os.Exit(0) 104 | } 105 | 106 | var buff bytes.Buffer 107 | if err := writeFiles(files, &buff, excludeBlankLines); err != nil { 108 | return fmt.Errorf("error writing files: %w", err) 109 | } 110 | 111 | fmt.Println("Writing files to clipboard...") 112 | if err = clipboard.WriteAll(buff.String()); err != nil { 113 | return fmt.Errorf("error writing to clipboard: %w", err) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func validateFlagsInput() error { 120 | if dir == "" { 121 | return errors.New("root path not provided") 122 | } 123 | 124 | if outputPath != "" { 125 | alreadyExists, err := fileAlreadyExists(outputPath) 126 | if err != nil { 127 | return fmt.Errorf("error checking if output file exists: %w", err) 128 | } 129 | 130 | if alreadyExists { 131 | return errOutputFileAlreadyExists 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | func fileAlreadyExists(filePath string) (bool, error) { 138 | if _, err := os.Stat(filePath); err != nil { 139 | if os.IsNotExist(err) { 140 | return false, nil 141 | } 142 | return false, fmt.Errorf("error checking if file exists: %w", err) 143 | } 144 | return true, nil 145 | } 146 | 147 | func overwriteFile(filePath string) bool { 148 | if _, err := os.Stat(filePath); err == nil { 149 | var input string 150 | fmt.Println(fileOverwritePrompt) 151 | fmt.Scanln(&input) 152 | 153 | switch strings.ToLower(input) { 154 | case "y", "yes": 155 | return true 156 | } 157 | } 158 | return false 159 | } 160 | 161 | func loadFiles(rootPath string, ignoreList []string) ([]file, error) { 162 | if rootPath == "" { 163 | return nil, errNoRootPathProvided 164 | } 165 | var files []file 166 | 167 | if err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { 168 | if err != nil { 169 | return fmt.Errorf("could not access path %q: %v", path, err) 170 | } 171 | 172 | if info.IsDir() { 173 | for _, item := range ignoreList { 174 | if info.Name() == item { 175 | return filepath.SkipDir 176 | } 177 | } 178 | } 179 | 180 | if !info.IsDir() { 181 | for _, item := range ignoreList { 182 | if info.Name() == item { 183 | return nil 184 | } 185 | } 186 | 187 | content, err := os.ReadFile(path) 188 | if err != nil { 189 | return fmt.Errorf("could not read file %q: %v", path, err) 190 | } 191 | 192 | files = append(files, file{ 193 | name: info.Name(), 194 | path: path, 195 | content: content, 196 | }) 197 | } 198 | return nil 199 | }); err != nil { 200 | return nil, fmt.Errorf("could not walk the path %q: %v", rootPath, err) 201 | } 202 | return files, nil 203 | } 204 | 205 | func createFile(outputPath string) (*os.File, error) { 206 | if outputPath == "" { 207 | return nil, errNoFilePathProvided 208 | } 209 | 210 | file, err := os.Create(outputPath) 211 | if err != nil { 212 | return nil, fmt.Errorf("could not create file: %w", err) 213 | } 214 | return file, nil 215 | } 216 | 217 | func writeFiles(files []file, output io.Writer, includeBlanklines bool) error { 218 | if files == nil { 219 | return errNoFilesToWrite 220 | } 221 | 222 | if output == nil { 223 | return errNoOutputFileProvided 224 | } 225 | 226 | if _, err := output.Write([]byte("---\n")); err != nil { 227 | return fmt.Errorf("could not write opening code separator for file: %w", err) 228 | } 229 | 230 | for _, file := range files { 231 | var strBulider strings.Builder 232 | 233 | strBulider.WriteString("Filename: ") 234 | strBulider.WriteString(file.name) 235 | strBulider.WriteString("\n\n") 236 | 237 | if includeBlanklines { 238 | for _, line := range strings.Split(string(file.content), "\n") { 239 | if line == "" { 240 | continue 241 | } 242 | strBulider.WriteString(line) 243 | strBulider.WriteString("\n") 244 | } 245 | 246 | } else { 247 | strBulider.WriteString(string(file.content)) 248 | } 249 | 250 | strBulider.WriteString("---\n") 251 | 252 | if _, err := output.Write([]byte(strBulider.String())); err != nil { 253 | return fmt.Errorf("could not write file %q: %v", file.name, err) 254 | } 255 | } 256 | return nil 257 | } 258 | -------------------------------------------------------------------------------- /internal/codetoprompt/codetoprompt_test.go: -------------------------------------------------------------------------------- 1 | package codetoprompt 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestLoadFiles(t *testing.T) { 15 | testCases := []struct { 16 | name string 17 | givenRootPath string 18 | givenIgnore []string 19 | expectedFiles []file 20 | expectedError error 21 | }{ 22 | { 23 | name: "no root path", 24 | givenRootPath: "", 25 | givenIgnore: []string{}, 26 | expectedFiles: nil, 27 | expectedError: errNoRootPathProvided, 28 | }, 29 | { 30 | name: "success", 31 | givenRootPath: "testdata", 32 | givenIgnore: []string{"dir_to_ignore_qux", "file_to_ignore_bar.txt"}, 33 | expectedFiles: []file{ 34 | { 35 | name: "file_foo.txt", 36 | path: "testdata/file_foo.txt", 37 | content: []byte("line 1\n\nline 3\n"), 38 | }, 39 | { 40 | name: "file_bar.txt", 41 | path: "testdata/dir_foo/file_bar.txt", 42 | content: []byte("line 1\n\nline 3\n\nline 5\nline 6\n"), 43 | }, 44 | }, 45 | expectedError: nil, 46 | }, 47 | } 48 | 49 | for _, tc := range testCases { 50 | t.Run(tc.name, func(t *testing.T) { 51 | observedFiles, err := loadFiles(tc.givenRootPath, tc.givenIgnore) 52 | 53 | if tc.expectedError == nil { 54 | require.NoError(t, err) 55 | 56 | for _, expectedFile := range tc.expectedFiles { 57 | assert.Contains(t, observedFiles, expectedFile) 58 | } 59 | 60 | } else { 61 | assert.True(t, errors.Is(err, tc.expectedError)) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestCreateFile(t *testing.T) { 68 | testCases := []struct { 69 | name string 70 | givenFilePath string 71 | expectedError error 72 | }{ 73 | { 74 | name: "no file path", 75 | givenFilePath: "", 76 | expectedError: errNoFilePathProvided, 77 | }, 78 | { 79 | name: "file is created", 80 | givenFilePath: "foo", 81 | expectedError: nil, 82 | }, 83 | } 84 | 85 | for _, tc := range testCases { 86 | t.Run(tc.name, func(t *testing.T) { 87 | _, err := createFile(tc.givenFilePath) 88 | 89 | if tc.expectedError == nil { 90 | require.NoError(t, err) 91 | } else { 92 | assert.True(t, errors.Is(err, tc.expectedError)) 93 | } 94 | 95 | if tc.givenFilePath != "" && tc.expectedError == nil { 96 | require.NoError(t, os.Remove(tc.givenFilePath)) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestWriteFiles(t *testing.T) { 103 | testCases := []struct { 104 | name string 105 | givenFiles []file 106 | givenOutput io.Writer 107 | givenIncludeBlanklines bool 108 | expectedError error 109 | }{ 110 | { 111 | name: "no files", 112 | givenFiles: nil, 113 | givenOutput: nil, 114 | givenIncludeBlanklines: false, 115 | expectedError: errNoFilesToWrite, 116 | }, 117 | { 118 | name: "no output", 119 | givenFiles: []file{{name: "test", path: "test", content: []byte("test")}}, 120 | givenOutput: nil, 121 | givenIncludeBlanklines: false, 122 | expectedError: errNoOutputFileProvided, 123 | }, 124 | { 125 | name: "no blank lines", 126 | givenFiles: []file{{name: "test", path: "test", content: []byte("test")}}, 127 | givenOutput: bytes.NewBuffer(nil), 128 | givenIncludeBlanklines: false, 129 | expectedError: nil, 130 | }, 131 | { 132 | name: "with blank lines", 133 | givenFiles: []file{{name: "test", path: "test", content: []byte("test")}}, 134 | givenOutput: bytes.NewBuffer(nil), 135 | givenIncludeBlanklines: true, 136 | expectedError: nil, 137 | }, 138 | } 139 | 140 | for _, tc := range testCases { 141 | t.Run(tc.name, func(t *testing.T) { 142 | err := writeFiles(tc.givenFiles, tc.givenOutput, tc.givenIncludeBlanklines) 143 | 144 | if tc.expectedError == nil { 145 | require.NoError(t, err) 146 | } else { 147 | assert.True(t, errors.Is(err, tc.expectedError)) 148 | } 149 | 150 | // Check if the output file contains the expected content 151 | if tc.givenOutput != nil { 152 | output := tc.givenOutput.(*bytes.Buffer) 153 | assert.Contains(t, output.String(), "---") 154 | assert.Contains(t, output.String(), "Filename: test") 155 | assert.Contains(t, output.String(), "test") 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal/codetoprompt/testdata/dir_foo/dir_to_ignore_qux/file_qux.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | -------------------------------------------------------------------------------- /internal/codetoprompt/testdata/dir_foo/file_bar.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | 3 | line 3 4 | 5 | line 5 6 | line 6 7 | -------------------------------------------------------------------------------- /internal/codetoprompt/testdata/dir_foo/file_to_ignore_bar.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | line 3 4 | -------------------------------------------------------------------------------- /internal/codetoprompt/testdata/file_foo.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | 3 | line 3 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/alesr/codetoprompt/internal/codetoprompt" 7 | ) 8 | 9 | func main() { 10 | if err := codetoprompt.Run(); err != nil { 11 | log.Fatalln(err) 12 | } 13 | } 14 | --------------------------------------------------------------------------------