From d78c6863919db76598dff042ad875cda75343830 Mon Sep 17 00:00:00 2001 From: Shaharia Azam Date: Wed, 10 Jul 2024 17:22:59 +0200 Subject: [PATCH] Initial commit --- .github/FUNDING.yml | 3 + .github/workflows/ci.yaml | 22 +++ .github/workflows/release.yaml | 57 +++++++ LICENSE | 21 +++ README.md | 125 ++++++++++++++ cora.go | 287 +++++++++++++++++++++++++++++++++ cora_test.go | 226 ++++++++++++++++++++++++++ go.mod | 16 ++ go.sum | 18 +++ 9 files changed, 775 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cora.go create mode 100644 cora_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b92a584 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [shaharia-lab, shahariaazam] \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..802a2e5 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,22 @@ +name: CI/CD + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + - name: Install dependencies + run: go mod download + - name: Run tests + run: go test -v ./... \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..19838dc --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,57 @@ +name: Build and Upload Release Assets + +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + build-and-upload: + name: Build and Upload Release Assets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + - name: Install dependencies + run: go mod download + - name: Install UPX + run: sudo apt-get update && sudo apt-get install -y upx + - name: Build and Compress + run: | + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o cora-linux-amd64 . + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o cora-darwin-amd64 . + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o cora-windows-amd64.exe . + upx --best cora-linux-amd64 cora-darwin-amd64 cora-windows-amd64.exe + - name: Upload Linux Binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./cora-linux-amd64 + asset_name: cora-linux-amd64 + asset_content_type: application/octet-stream + - name: Upload macOS Binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./cora-darwin-amd64 + asset_name: cora-darwin-amd64 + asset_content_type: application/octet-stream + - name: Upload Windows Binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./cora-windows-amd64.exe + asset_name: cora-windows-amd64.exe + asset_content_type: application/octet-stream \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2381b32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Shaharia Lab OÜ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1703980 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# Cora + +

+ Cora Logo +

+ +**CORA**: **CO**ncatenate and **R**ead **A**ll + +**Cora** is a command-line tool for concatenating files in a directory into a single output file. The name CORA stands for **"COncatenate and Read All"** reflecting its primary function of combining multiple files while providing options to include or exclude specific files based on patterns. + +## Features + +- Recursively walk through directories +- Include or exclude files based on glob patterns +- Customize separators between concatenated files +- Add prefixes to file paths in the output +- Debug mode for detailed logging + +## Use Cases + +Cora can be incredibly useful in various scenarios, particularly when dealing with large codebases or multiple files that need to be combined. Here are some key use cases: + +1. **LLM Context Preparation**: When working with Large Language Models (LLMs) like GPT-3 or GPT-4, providing comprehensive context is crucial for accurate responses. Cora can concatenate an entire codebase into a single file, making it easy to input as context for LLMs. This is especially useful for: + - Code review and analysis + - Generating documentation + - Answering questions about complex codebases + +2. **Documentation Generation**: Combine multiple markdown files or source code files to create comprehensive documentation for your project. + +3. **Code Auditing**: Merge multiple source files into a single document for easier review and analysis, especially when working with security auditing tools. + +4. **Project Submissions**: Combine all relevant files for project submissions in academic or professional settings. + +5. **Backup and Archiving**: Create a single file containing all important documents from a directory structure, making it easier to backup or share entire projects. + +6. **Log Analysis**: Concatenate multiple log files for comprehensive analysis, while using exclude patterns to filter out irrelevant files. + +7. **Content Management**: Combine multiple content pieces (e.g., blog posts, articles) into a single file for bulk editing or publishing. + +8. **Data Preprocessing**: Merge multiple data files into a single file for easier processing in data analysis pipelines. + +By using Cora, you can streamline these processes, saving time and reducing the complexity of managing multiple files in various scenarios. + +## Installation + +To install Cora, make sure you have Go installed on your system, then run: + +```bash +go install github.com/shaharia-lab/cora@latest +``` + +This command will download the source code, compile it, and install the `cora` binary in your `$GOPATH/bin` directory. Make sure your `$GOPATH/bin` is added to your system's `PATH` to run `cora` from any location. + +## Usage + +After installation, you can run Cora from anywhere in your terminal: + +```bash +cora [flags] +``` + +### Flags + +- `-s, --source`: Source directory to concatenate files from (required) +- `-o, --output`: Output file to write concatenated files to (required) +- `-e, --exclude`: Glob patterns to exclude (can be specified multiple times) +- `-i, --include`: Glob patterns to include (can be specified multiple times) +- `-d, --debug`: Enable debugging mode +- `-p, --separator`: Separator to use between concatenated files (default: "\n---\n") +- `-x, --path-prefix`: Prefix to add before the path of included files (default: "## ") + +### Example + +```bash +cora -s /path/to/source -o output.md -e "*.log" -i "*.md" -i "*.txt" -d +``` + +This command will concatenate all `.md` and `.txt` files from `/path/to/source`, excluding any `.log` files, and save the result to `output.md` with debug logging enabled. + +## Development + +### Prerequisites + +- Go 1.16 or higher + +### Building + +To build the project locally, run: + +```bash +go build +``` + +### Running Tests + +To run the tests, use: + +```bash +go test ./... +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- [Cobra](https://github.com/spf13/cobra) - A Commander for modern Go CLI interactions +- [testify](https://github.com/stretchr/testify) - A toolkit with common assertions and mocks that plays nicely with the standard library + +## Contact + +Shaharia Lab OÜ - [shaharialab.com](https://shaharialab.com) - hello@shaharialab.com + +Project Link: [https://github.com/shaharia-lab/cora](https://github.com/shaharia-lab/cora) \ No newline at end of file diff --git a/cora.go b/cora.go new file mode 100644 index 0000000..80ce17e --- /dev/null +++ b/cora.go @@ -0,0 +1,287 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +const ( + defaultBufferSize = 64 * 1024 // 64KB +) + +func main() { + if err := newRootCmd().Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func newRootCmd() *cobra.Command { + cfg := &config{} + + rootCmd := &cobra.Command{ + Use: "cora", + Short: "Concatenate files in a directory into a single file.", + Long: `Concatenate files in a directory into a single file.`, + RunE: cfg.run, + } + + rootCmd.Flags().StringVarP(&cfg.SourceDirectory, "source", "s", "", "Source directory to concatenate files from") + rootCmd.Flags().StringVarP(&cfg.OutputFile, "output", "o", "", "Output file to write concatenated files to") + rootCmd.Flags().StringSliceVarP(&cfg.ExcludePatterns, "exclude", "e", nil, "Glob patterns to exclude") + rootCmd.Flags().StringSliceVarP(&cfg.IncludePatterns, "include", "i", nil, "Glob patterns to include") + rootCmd.Flags().BoolVarP(&cfg.EnableDebugging, "debug", "d", false, "Enable debugging mode") + rootCmd.Flags().StringVarP(&cfg.Separator, "separator", "p", "\n---\n", "Separator to use between concatenated files") + rootCmd.Flags().StringVarP(&cfg.PathPrefix, "path-prefix", "x", "## ", "Prefix to add before the path of included files") + + return rootCmd +} + +type config struct { + SourceDirectory string + OutputFile string + ExcludePatterns []string + IncludePatterns []string + Separator string + PathPrefix string + EnableDebugging bool +} + +func (cfg *config) run(cmd *cobra.Command, args []string) error { + if err := cfg.validate(); err != nil { + return err + } + + debugLog := newDebugLog(cfg.EnableDebugging) + w := newWalker(cfg.SourceDirectory, cfg.ExcludePatterns, cfg.IncludePatterns, debugLog) + c := newConcatenator(cfg.OutputFile, cfg.Separator, cfg.PathPrefix, debugLog) + + return cfg.process(w, c) +} + +func (cfg *config) validate() error { + if cfg.SourceDirectory == "" { + return fmt.Errorf("source directory is required") + } + if cfg.OutputFile == "" { + return fmt.Errorf("output file is required") + } + return nil +} + +func (cfg *config) process(w *walker, c *concatenator) error { + filePaths, err := w.walk() + if err != nil { + return fmt.Errorf("failed to walk directory: %w", err) + } + + if err := c.concatenate(filePaths); err != nil { + return fmt.Errorf("failed to concatenate files: %w", err) + } + + return nil +} + +type walker struct { + sourceDirectory string + excludePatterns []string + includePatterns []string + debugLog *debugLog +} + +func newWalker(sourceDirectory string, excludePatterns, includePatterns []string, debugLog *debugLog) *walker { + return &walker{ + sourceDirectory: sourceDirectory, + excludePatterns: excludePatterns, + includePatterns: includePatterns, + debugLog: debugLog, + } +} + +func (w *walker) walk() ([]string, error) { + var files []string + + err := filepath.WalkDir(w.sourceDirectory, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + excluded, err := matchesGlob(w.sourceDirectory, path, w.excludePatterns) + if err != nil { + return err + } + + if excluded { + w.debugLog.print(fmt.Sprintf("Excluding %s", path)) + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + + if !d.IsDir() { + if len(w.includePatterns) > 0 { + included, err := matchesGlob(w.sourceDirectory, path, w.includePatterns) + if err != nil { + return err + } + if included { + files = append(files, path) + w.debugLog.print(fmt.Sprintf("Including %s", path)) + } else { + w.debugLog.print(fmt.Sprintf("Skipping %s (not in include patterns)", path)) + } + } else { + files = append(files, path) + w.debugLog.print(fmt.Sprintf("Including %s", path)) + } + } + + return nil + }) + + return files, err +} + +type concatenator struct { + outputPath string + separator []byte + debugLog *debugLog + pathPrefix []byte +} + +func newConcatenator(outputPath, separator, pathPrefix string, debugLog *debugLog) *concatenator { + return &concatenator{ + outputPath: outputPath, + separator: []byte(separator), + pathPrefix: []byte(pathPrefix), + debugLog: debugLog, + } +} + +func (c *concatenator) concatenate(filePaths []string) error { + if err := os.MkdirAll(filepath.Dir(c.outputPath), 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + outFile, err := os.Create(c.outputPath) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + + writer := bufio.NewWriterSize(outFile, defaultBufferSize) + defer writer.Flush() + + for i, filePath := range filePaths { + if i > 0 { + if _, err := writer.Write(c.separator); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + } + + if err := c.writeFileHeader(writer, filePath); err != nil { + return err + } + + if err := c.appendFileContent(writer, filePath); err != nil { + return err + } + + if _, err := writer.Write([]byte{'\n'}); err != nil { + return fmt.Errorf("failed to write newline after file content: %w", err) + } + } + + return nil +} + +func (c *concatenator) writeFileHeader(writer *bufio.Writer, filePath string) error { + if _, err := writer.Write(c.pathPrefix); err != nil { + return fmt.Errorf("failed to write path prefix: %w", err) + } + + if _, err := writer.WriteString(filePath); err != nil { + return fmt.Errorf("failed to write file path: %w", err) + } + + if _, err := writer.Write([]byte{'\n'}); err != nil { + return fmt.Errorf("failed to write newline after file path: %w", err) + } + + return nil +} + +func (c *concatenator) appendFileContent(writer *bufio.Writer, filePath string) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", filePath, err) + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + return fmt.Errorf("failed to copy content from %s: %w", filePath, err) + } + + return nil +} + +func matchesGlob(rootPath, filePath string, patterns []string) (bool, error) { + relPath, err := filepath.Rel(rootPath, filePath) + if err != nil { + return false, err + } + + relPath = filepath.ToSlash(relPath) + + for _, pattern := range patterns { + pattern = filepath.ToSlash(pattern) + + if !strings.Contains(pattern, "/") { + matched, err := filepath.Match(pattern, filepath.Base(relPath)) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } else { + matched, err := filepath.Match(pattern, relPath) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + } + + return false, nil +} + +type debugLog struct { + enabled bool +} + +func newDebugLog(enabled bool) *debugLog { + return &debugLog{ + enabled: enabled, + } +} + +func (d *debugLog) print(message string) { + if !d.enabled { + return + } + + log.Println(message) +} diff --git a/cora_test.go b/cora_test.go new file mode 100644 index 0000000..c93a8cb --- /dev/null +++ b/cora_test.go @@ -0,0 +1,226 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConcatenator(t *testing.T) { + tempDir := t.TempDir() + inputFiles := []string{ + filepath.Join(tempDir, "file1.txt"), + filepath.Join(tempDir, "file2.txt"), + filepath.Join(tempDir, "file3.txt"), + } + outputFile := filepath.Join(tempDir, "output.txt") + + for i, file := range inputFiles { + err := os.WriteFile(file, []byte(fmt.Sprintf("Content of file %d", i+1)), 0644) + require.NoError(t, err) + } + + debugLog := newDebugLog(false) + concatenator := newConcatenator(outputFile, "\n---\n", "File: ", debugLog) + err := concatenator.concatenate(inputFiles) + require.NoError(t, err) + + content, err := os.ReadFile(outputFile) + require.NoError(t, err) + + var expectedContent bytes.Buffer + for i, file := range inputFiles { + if i > 0 { + expectedContent.WriteString("\n---\n") + } + expectedContent.WriteString(fmt.Sprintf("File: %s\n", file)) + expectedContent.WriteString(fmt.Sprintf("Content of file %d\n", i+1)) + } + + assert.Equal(t, expectedContent.String(), string(content)) +} + +func TestConcatenatorLargeFiles(t *testing.T) { + tempDir := t.TempDir() + inputFiles := make([]string, 100) + for i := 0; i < 100; i++ { + inputFiles[i] = filepath.Join(tempDir, fmt.Sprintf("file%d.txt", i)) + } + outputFile := filepath.Join(tempDir, "output.txt") + + largeContent := bytes.Repeat([]byte("a"), 1024*1024) + for _, file := range inputFiles { + err := os.WriteFile(file, largeContent, 0644) + require.NoError(t, err) + } + + debugLog := newDebugLog(false) + concatenator := newConcatenator(outputFile, "\n", "File: ", debugLog) + err := concatenator.concatenate(inputFiles) + require.NoError(t, err) + + stat, err := os.Stat(outputFile) + require.NoError(t, err) + + filePathLen := len(inputFiles[0]) + prefixLen := len("File: ") + separatorLen := len("\n") + singleFileSize := int64(1024*1024 + filePathLen + prefixLen + 2) + expectedSize := 100*singleFileSize + 99*int64(separatorLen) + 90 + + content, err := os.ReadFile(outputFile) + require.NoError(t, err) + + assert.Equal(t, expectedSize, stat.Size()) + assert.Equal(t, int(expectedSize), len(content)) +} + +func TestWalker(t *testing.T) { + root := t.TempDir() + createTestFiles(t, root) + + debugLog := newDebugLog(false) + + tests := []struct { + name string + excludePatterns []string + includePatterns []string + expectedFiles []string + }{ + {"No patterns", []string{}, []string{}, []string{"file1.txt", "file2.txt", "file3.txt", "ignoreme.txt"}}, + {"Exclude one dir", []string{"ignoreme"}, []string{}, []string{"file1.txt", "file2.txt", "file3.txt"}}, + {"Include specific files", []string{}, []string{"file1.txt", "file3.txt"}, []string{"file1.txt", "file3.txt"}}, + {"Include and exclude", []string{"ignoreme"}, []string{"file*.txt"}, []string{"file1.txt", "file2.txt", "file3.txt"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + walker := newWalker(root, tt.excludePatterns, tt.includePatterns, debugLog) + files, err := walker.walk() + + assert.NoError(t, err) + + var baseNames []string + for _, file := range files { + baseNames = append(baseNames, filepath.Base(file)) + } + + assert.ElementsMatch(t, tt.expectedFiles, baseNames) + }) + } +} + +func createTestFiles(t *testing.T, root string) { + files := []string{ + "file1.txt", + "file2.txt", + "file3.txt", + filepath.Join("ignoreme", "ignoreme.txt"), + } + + for _, file := range files { + path := filepath.Join(root, file) + err := os.MkdirAll(filepath.Dir(path), 0755) + assert.NoError(t, err) + + err = os.WriteFile(path, []byte("test"), 0644) + assert.NoError(t, err) + } +} + +func TestMatchesGlob(t *testing.T) { + tests := []struct { + name string + rootPath string + filePath string + patterns []string + expected bool + }{ + { + name: "Match single file", + rootPath: "/root", + filePath: "/root/file.txt", + patterns: []string{"*.txt"}, + expected: true, + }, + { + name: "Match file in subdirectory", + rootPath: "/root", + filePath: "/root/subdir/file.go", + patterns: []string{"**/*.go"}, + expected: true, + }, + { + name: "No match", + rootPath: "/root", + filePath: "/root/file.txt", + patterns: []string{"*.go"}, + expected: false, + }, + { + name: "Match with multiple patterns", + rootPath: "/root", + filePath: "/root/subdir/file.js", + patterns: []string{"*.go", "**/*.js"}, + expected: true, + }, + { + name: "Match file name only", + rootPath: "/root", + filePath: "/root/subdir/config.json", + patterns: []string{"config.json"}, + expected: true, + }, + { + name: "Match directory name", + rootPath: "/root", + filePath: "/root/ignoreme", + patterns: []string{"ignoreme"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := matchesGlob(tt.rootPath, tt.filePath, tt.patterns) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestDebugLog(t *testing.T) { + tests := []struct { + name string + enabled bool + message string + }{ + {"Enabled", true, "Test message"}, + {"Disabled", false, "Test message"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + debugLog := newDebugLog(tt.enabled) + + // Capture log output + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + debugLog.print(tt.message) + + if tt.enabled { + assert.Contains(t, buf.String(), tt.message) + } else { + assert.Empty(t, buf.String()) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa09698 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/shaharia-lab/cora + +go 1.22 + +require ( + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4353b61 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=