├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd ├── init.go ├── root.go └── version.go ├── docs ├── CHANGELOG.md ├── README.md ├── configuration.md ├── getting-started.md ├── templates.md └── upgrading.md ├── go.mod ├── go.sum ├── internal ├── project │ ├── api │ │ ├── generator.go │ │ └── init.go │ ├── cli │ │ ├── generator.go │ │ └── init.go │ ├── create.go │ ├── structure.go │ └── template_loader.go ├── templates │ ├── files.go │ └── loader.go └── version │ └── version.go ├── main.go ├── pkg ├── questions │ └── questions.go └── utils │ ├── file_utils.go │ ├── input_reader.go │ └── logger.go ├── scripts ├── build.sh ├── install.bat ├── install.ps1 └── install.sh ├── templates ├── api │ ├── docker-compose.tpl │ ├── dockerfile.tpl │ ├── env.tpl │ ├── gitignore.tpl │ ├── go-mod.tpl │ ├── handlers.tpl │ ├── logging.tpl │ ├── main.tpl │ ├── middleware.tpl │ ├── postgres.tpl │ ├── rabbitmq.tpl │ ├── redis.tpl │ ├── routes.tpl │ ├── server.tpl │ └── service-init.tpl ├── cli │ ├── command.tpl │ ├── commands.tpl │ ├── config.tpl │ ├── gitignore.tpl │ ├── go-mod.tpl │ ├── main.tpl │ ├── readme.tpl │ ├── root.tpl │ ├── utils.tpl │ └── version.tpl └── templates.go └── tests ├── commands_test.go ├── init_test.go ├── template_test.go └── utils_test.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Sova CLI 2 | 3 | First off, thank you for considering contributing to Sova CLI! It's people like you that make Sova CLI such a great tool. 4 | 5 | ## Code of Conduct 6 | 7 | This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | Before creating bug reports, please check the issue list as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: 14 | 15 | * Use a clear and descriptive title 16 | * Describe the exact steps which reproduce the problem 17 | * Provide specific examples to demonstrate the steps 18 | * Describe the behavior you observed after following the steps 19 | * Explain which behavior you expected to see instead and why 20 | * Include screenshots if possible 21 | 22 | ### Suggesting Enhancements 23 | 24 | Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: 25 | 26 | * Use a clear and descriptive title 27 | * Provide a step-by-step description of the suggested enhancement 28 | * Provide specific examples to demonstrate the steps 29 | * Describe the current behavior and explain which behavior you expected to see instead 30 | * Explain why this enhancement would be useful 31 | 32 | ### Pull Requests 33 | 34 | * Fork the repo and create your branch from `main` 35 | * If you've added code that should be tested, add tests 36 | * If you've changed APIs, update the documentation 37 | * Ensure the test suite passes 38 | * Make sure your code lints 39 | * Issue that pull request! 40 | 41 | ## Development Setup 42 | 43 | 1. Fork and clone the repository 44 | ```bash 45 | git clone https://github.com/go-sova/sova-cli.git 46 | ``` 47 | 48 | 2. Install dependencies 49 | ```bash 50 | go mod download 51 | ``` 52 | 53 | 3. Run tests 54 | ```bash 55 | go test ./... 56 | ``` 57 | 58 | 4. Build the project 59 | ```bash 60 | go build 61 | ``` 62 | 63 | ## Project Structure 64 | 65 | ``` 66 | . 67 | ├── cmd/ # Command implementations 68 | ├── internal/ # Private application code 69 | ├── pkg/ # Public libraries 70 | ├── docs/ # Documentation 71 | └── tests/ # Test files 72 | ``` 73 | 74 | ## Coding Style 75 | 76 | * Follow standard Go project layout 77 | * Use `gofmt` for formatting 78 | * Follow Go naming conventions 79 | * Write descriptive commit messages 80 | * Add tests for new features 81 | 82 | ## Testing 83 | 84 | * Write unit tests for new features 85 | * Ensure all tests pass before submitting PR 86 | * Include integration tests when needed 87 | * Test edge cases and error conditions 88 | 89 | ## Documentation 90 | 91 | * Update README.md if needed 92 | * Add godoc comments to public functions 93 | * Update wiki pages if needed 94 | * Include examples for new features 95 | 96 | ## Commit Messages 97 | 98 | * Use the present tense ("Add feature" not "Added feature") 99 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 100 | * Limit the first line to 72 characters or less 101 | * Reference issues and pull requests liberally after the first line 102 | 103 | ## Pull Request Process 104 | 105 | 1. Update the README.md with details of changes if needed 106 | 2. Update the docs/ with details of changes if needed 107 | 3. The PR will be merged once you have the sign-off of at least one maintainer 108 | 109 | ## Release Process 110 | 111 | 1. Update version number in relevant files 112 | 2. Update CHANGELOG.md 113 | 3. Create a new GitHub release 114 | 4. Tag the release with version number 115 | 5. Update installation instructions if needed 116 | 117 | ## Questions? 118 | 119 | Feel free to open an issue with your question or contact the maintainers directly. 120 | 121 | Thank you for contributing to Sova CLI! 🚀 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sova CLI Contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Sova CLI 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![Go Version](https://img.shields.io/badge/go-%3E%3D1.21-blue)](https://golang.org/dl/) 5 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-sova/sova-cli)](https://goreportcard.com/report/github.com/go-sova/sova-cli) 7 | 8 | A modern CLI tool for scaffolding Go projects with best practices. Generate production-ready project templates in seconds. 9 | 10 | ## 🚀 Quick Install 11 | 12 | **Linux/macOS**: 13 | ```bash 14 | curl -fsSL https://raw.githubusercontent.com/go-sova/sova-cli/master/scripts/install.sh | bash 15 | ``` 16 | 17 | **Windows** (Command Prompt): 18 | ```cmd 19 | curl -fsSL -o install.bat https://raw.githubusercontent.com/go-sova/sova-cli/master/scripts/install.bat && install.bat 20 | ``` 21 | 22 | **Using Go**: 23 | ```bash 24 | go install github.com/go-sova/sova-cli@latest 25 | ``` 26 | 27 | **Manual Installation**: 28 | Download the latest release from [GitHub Releases](https://github.com/go-sova/sova-cli/releases/latest) 29 | 30 | ## 💡 Usage 31 | 32 | Create a new project: 33 | ```bash 34 | # Basic project 35 | sova init my-project 36 | 37 | ``` 38 | 39 | ## 📦 Features 40 | 41 | - Multiple project templates (Web, CLI, Library) 42 | - Standardized project structure 43 | - Customizable templates 44 | - Interactive prompts 45 | - Cross-platform support 46 | 47 | ## 📚 Documentation 48 | 49 | - [Getting Started](https://github.com/go-sova/sova-cli/wiki/getting-started) 50 | - [Templates](https://github.com/go-sova/sova-cli/wiki/templates) 51 | - [Configuration](https://github.com/go-sova/sova-cli/wiki/configuration) 52 | - [Contributing](CONTRIBUTING.md) 53 | 54 | ## 🤝 Contributing 55 | 56 | We love your input! Check out our [Contributing Guide](CONTRIBUTING.md) for ways to get started. Every contribution counts: 57 | 58 | 1. Fork the repo 59 | 2. Create your feature branch (`git checkout -b feature/amazing`) 60 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 61 | 4. Push to the branch (`git push origin feature/amazing`) 62 | 5. Open a Pull Request 63 | 64 | ## 📝 License 65 | 66 | Copyright © 2024 [Sova CLI Contributors](https://github.com/go-sova/sova-cli/graphs/contributors) 67 | 68 | This project is [MIT](LICENSE) licensed. By contributing, you agree that your contributions will be licensed under its MIT License. 69 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-sova/sova-cli/internal/project/api" 7 | "github.com/go-sova/sova-cli/internal/project/cli" 8 | "github.com/go-sova/sova-cli/pkg/questions" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var initCmd = &cobra.Command{ 13 | Use: "init [project-name]", 14 | Short: "Initialize a new project", 15 | Long: `Initialize a new project with the specified name. 16 | This command will guide you through the project setup process. 17 | If you don't provide a project name, you'll be prompted to enter one. 18 | You can choose between different project types: 19 | - api: A Go API project with clean architecture 20 | - cli: A Go CLI project with clean architecture`, 21 | Args: cobra.MaximumNArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | var projectName string 24 | var projectType string 25 | var err error 26 | 27 | if len(args) > 0 { 28 | projectName = args[0] 29 | } else { 30 | projectName, err = questions.AskProjectName() 31 | if err != nil { 32 | fmt.Printf("Error: %v\n", err) 33 | return 34 | } 35 | } 36 | 37 | projectType, err = questions.AskProjectType() 38 | if err != nil { 39 | fmt.Printf("Error: %v\n", err) 40 | return 41 | } 42 | 43 | switch projectType { 44 | case "api": 45 | apiCmd := api.InitCmd 46 | apiCmd.SetArgs([]string{projectName}) 47 | err = apiCmd.Execute() 48 | case "cli": 49 | cliCmd := cli.InitCmd 50 | cliCmd.SetArgs([]string{projectName}) 51 | err = cliCmd.Execute() 52 | default: 53 | err = fmt.Errorf("unsupported project type: %s", projectType) 54 | } 55 | 56 | if err != nil { 57 | fmt.Printf("Error: %v\n", err) 58 | } 59 | }, 60 | } 61 | 62 | func init() { 63 | rootCmd.AddCommand(initCmd) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | "github.com/go-sova/sova-cli/templates" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var ( 15 | cfgFile string 16 | verbose bool 17 | ) 18 | 19 | var rootCmd = &cobra.Command{ 20 | Use: "sova", 21 | Short: "Sova CLI - A tool for initializing projects", 22 | Long: `Sova CLI is a powerful tool for initializing projects 23 | with predefined templates and structures. 24 | 25 | Available Commands: 26 | init Initialize a new project with your desired settings 27 | version Display version information 28 | help Help about any command 29 | 30 | Use 'sova init' to create a new project with your desired settings.`, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | cmd.Help() 33 | }, 34 | } 35 | 36 | var templateFS fs.FS 37 | 38 | func Execute() error { 39 | return rootCmd.Execute() 40 | } 41 | 42 | func init() { 43 | cobra.OnInitialize(initConfig) 44 | 45 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sova.yaml)") 46 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output") 47 | 48 | rootCmd.Flags().BoolP("version", "V", false, "display version information") 49 | 50 | rootCmd.CompletionOptions.DisableDefaultCmd = true 51 | 52 | viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) 53 | 54 | // Initialize template filesystem 55 | templateFS = templates.GetTemplateFS() 56 | } 57 | 58 | func initConfig() { 59 | if cfgFile != "" { 60 | viper.SetConfigFile(cfgFile) 61 | } else { 62 | home, err := os.UserHomeDir() 63 | cobra.CheckErr(err) 64 | 65 | viper.AddConfigPath(home) 66 | viper.SetConfigType("yaml") 67 | viper.SetConfigName(".sova") 68 | } 69 | 70 | viper.AutomaticEnv() 71 | 72 | if err := viper.ReadInConfig(); err == nil { 73 | if verbose { 74 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) 75 | } 76 | } 77 | } 78 | 79 | func PrintSuccess(format string, a ...interface{}) { 80 | color.Green(format, a...) 81 | } 82 | 83 | func PrintInfo(format string, a ...interface{}) { 84 | color.Blue(format, a...) 85 | } 86 | 87 | func PrintWarning(format string, a ...interface{}) { 88 | color.Yellow(format, a...) 89 | } 90 | 91 | func PrintError(format string, a ...interface{}) { 92 | color.Red(format, a...) 93 | } 94 | 95 | // GetTemplate returns the contents of a template file 96 | func GetTemplate(category, name string) (string, error) { 97 | templatePath := templates.GetTemplatePath(category, name) 98 | content, err := fs.ReadFile(templateFS, templatePath) 99 | if err != nil { 100 | return "", fmt.Errorf("failed to read template %s: %w", templatePath, err) 101 | } 102 | return string(content), nil 103 | } 104 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/go-sova/sova-cli/internal/version" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Print the version number of Sova CLI", 14 | Long: `Display the version, build, and runtime information for Sova CLI.`, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | info := version.GetInfo() 17 | 18 | if cmd.Flag("json").Value.String() == "true" { 19 | jsonOutput, _ := json.MarshalIndent(info, "", " ") 20 | fmt.Println(string(jsonOutput)) 21 | return 22 | } 23 | 24 | fmt.Printf("Sova CLI v%s\n", info.Version) 25 | if cmd.Flag("verbose").Value.String() == "true" { 26 | fmt.Printf("Build Date: %s\n", info.BuildDate) 27 | fmt.Printf("Git Commit: %s\n", info.GitCommit) 28 | fmt.Printf("Go Version: %s\n", info.GoVersion) 29 | fmt.Printf("Platform: %s\n", info.Platform) 30 | } 31 | }, 32 | } 33 | 34 | func init() { 35 | versionCmd.Flags().BoolP("verbose", "v", false, "print detailed version information") 36 | versionCmd.Flags().Bool("json", false, "print version information as JSON") 37 | rootCmd.AddCommand(versionCmd) 38 | } 39 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Sova CLI will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.1] - 2025-03-18 11 | 12 | ### Added 13 | - Comprehensive `.gitignore` templates for both API and CLI projects 14 | - Detailed documentation in `docs/` directory 15 | - Getting started guide with project setup instructions 16 | - Templates documentation explaining available options and features 17 | 18 | ### Fixed 19 | - Import path issues in generated code 20 | - Removed obsolete `version` attribute from `docker-compose.yml` template 21 | - Project structure organization for better code management 22 | - Synchronization issues with `go.mod` and `go.sum` files 23 | 24 | ### Changed 25 | - Improved project templates organization 26 | - Enhanced documentation structure 27 | - Updated Docker Compose configuration for better compatibility 28 | - Reorganized internal package structure for cleaner architecture 29 | 30 | ### Development 31 | - Added `testify` package for enhanced testing capabilities 32 | - Improved test coverage for project components 33 | - Better error handling in project generation 34 | - Enhanced template validation 35 | 36 | ## [0.1.0] - 2025-03-17 37 | 38 | ### Added 39 | - Initial release of Sova CLI 40 | - Basic project generation for API and CLI projects 41 | - Docker Compose integration for API projects 42 | - Command management system for CLI projects 43 | - Basic documentation -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Sova CLI Documentation 2 | 3 | Welcome to the Sova CLI documentation! This directory contains comprehensive documentation for using and understanding the Sova CLI tool. 4 | 5 | ## Documentation Structure 6 | 7 | - [Getting Started](getting-started.md) - Quick start guide and installation instructions 8 | - [Templates](templates.md) - Detailed information about available project templates 9 | - [CHANGELOG](CHANGELOG.md) - History of changes and updates 10 | 11 | ## Quick Links 12 | 13 | ### For New Users 14 | 1. Start with the [Getting Started](getting-started.md) guide 15 | 2. Review available templates in [Templates](templates.md) 16 | 3. Check system requirements and installation steps 17 | 18 | ### For Project Setup 19 | 1. API Project setup and configuration 20 | 2. CLI Project setup and configuration 21 | 3. Available integrations and features 22 | 23 | ### For Contributors 24 | 1. Project structure and architecture 25 | 2. Development guidelines 26 | 3. Testing requirements 27 | 28 | ## Documentation Updates 29 | 30 | The documentation is regularly updated to reflect new features, improvements, and fixes. Recent updates include: 31 | 32 | 1. New `.gitignore` templates for both API and CLI projects 33 | 2. Enhanced project structure documentation 34 | 3. Updated Docker configuration guidelines 35 | 4. Improved testing and development guides 36 | 37 | ## Getting Help 38 | 39 | If you need help or have questions: 40 | 41 | 1. Check the relevant documentation section 42 | 2. Look for similar issues in the project repository 43 | 3. Create a new issue if you can't find a solution 44 | 45 | ## Contributing to Documentation 46 | 47 | We welcome contributions to improve the documentation: 48 | 49 | 1. Fix typos or unclear explanations 50 | 2. Add examples and use cases 51 | 3. Suggest new documentation topics 52 | 4. Improve existing guides 53 | 54 | Please submit a pull request with your changes. -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration Guide 2 | 3 | Sova CLI can be configured using configuration files and command-line flags. 4 | 5 | ## Global Configuration 6 | 7 | Create `.sova.yaml` in your home directory: 8 | 9 | ```yaml 10 | # Default settings for new projects 11 | defaults: 12 | template: web 13 | license: MIT 14 | goVersion: "1.21" 15 | author: "Meyank Singh" 16 | 17 | # Template settings 18 | templates: 19 | directory: ~/.sova/templates 20 | default: web 21 | 22 | # Project settings 23 | project: 24 | structure: 25 | enableTests: true 26 | enableDocs: true 27 | enableScripts: true 28 | 29 | # Tool settings 30 | tools: 31 | enableFormatting: true 32 | enableLinting: true 33 | enableTesting: true 34 | ``` 35 | 36 | ## Project Configuration 37 | 38 | Create `.sova.yaml` in your project directory: 39 | 40 | ```yaml 41 | # Project information 42 | name: my-awesome-project 43 | version: 1.0.0 44 | description: A fantastic Go project 45 | author: Your Name 46 | license: MIT 47 | 48 | # Build settings 49 | build: 50 | main: ./cmd/main.go 51 | output: ./bin/app 52 | ldflags: -s -w 53 | 54 | # Dependencies 55 | dependencies: 56 | - github.com/spf13/cobra 57 | - github.com/spf13/viper 58 | 59 | # Development tools 60 | tools: 61 | formatter: gofmt 62 | linter: golangci-lint 63 | testRunner: go test 64 | ``` 65 | 66 | ## Environment Variables 67 | 68 | Sova CLI respects the following environment variables: 69 | 70 | ```bash 71 | # Configuration 72 | SOVA_CONFIG=/path/to/config.yaml 73 | SOVA_TEMPLATE_DIR=~/.sova/templates 74 | 75 | # Project defaults 76 | SOVA_DEFAULT_TEMPLATE=web 77 | SOVA_DEFAULT_LICENSE=MIT 78 | SOVA_DEFAULT_AUTHOR="Meyank Singh" 79 | 80 | # Development 81 | SOVA_DEBUG=true 82 | SOVA_VERBOSE=true 83 | ``` 84 | 85 | ## Command Line Flags 86 | 87 | Global flags available for all commands: 88 | 89 | ```bash 90 | # General 91 | --config string Config file path 92 | --verbose Enable verbose output 93 | --debug Enable debug mode 94 | 95 | # Project initialization 96 | --template string Template to use 97 | --force Force overwrite existing files 98 | --no-git Don't initialize git repository 99 | 100 | # Component generation 101 | --output string Output directory 102 | --dry-run Show what would be done 103 | ``` 104 | 105 | ## Template Configuration 106 | 107 | Template-specific configuration in `template.yaml`: 108 | 109 | ```yaml 110 | name: custom-template 111 | version: 1.0.0 112 | description: Custom project template 113 | 114 | # Files to include 115 | files: 116 | - source: main.go.tmpl 117 | target: cmd/main.go 118 | - source: config.go.tmpl 119 | target: internal/config/config.go 120 | 121 | # Directories to create 122 | directories: 123 | - cmd 124 | - internal 125 | - pkg 126 | - docs 127 | 128 | # Dependencies to add 129 | dependencies: 130 | - name: github.com/spf13/cobra 131 | version: v1.7.0 132 | - name: github.com/spf13/viper 133 | version: v1.16.0 134 | 135 | # Hooks 136 | hooks: 137 | pre-generate: 138 | - command: go mod init 139 | post-generate: 140 | - command: go mod tidy 141 | ``` -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Sova CLI 2 | 3 | Welcome to Sova CLI! This guide will help you get up and running quickly. 4 | 5 | ## Installation 6 | 7 | ### Prerequisites 8 | - Go 1.21 or higher 9 | - Git (for development) 10 | 11 | ### Install Methods 12 | 13 | 1. **Quick Install (Linux/macOS)**: 14 | ```bash 15 | curl -fsSL https://raw.githubusercontent.com/meyanksingh/go-sova/master/scripts/install.sh | bash 16 | ``` 17 | 18 | 2. **Go Install**: 19 | ```bash 20 | go install github.com/go-sova/sova-cli@latest 21 | ``` 22 | 23 | 3. **Manual Installation**: 24 | - Download from [Releases](https://github.com/go-sova/sova-cli/releases) 25 | - Extract and add to your PATH 26 | 27 | ## Quick Start 28 | 29 | ### Creating an API Project 30 | 31 | 1. Create a new API project: 32 | ```bash 33 | sova-cli create api my-api 34 | ``` 35 | 36 | 2. Choose your integrations when prompted: 37 | - PostgreSQL database 38 | - Redis cache 39 | - RabbitMQ message queue 40 | - Zap logging 41 | 42 | 3. Navigate to your project: 43 | ```bash 44 | cd my-api 45 | ``` 46 | 47 | 4. Start the services: 48 | ```bash 49 | docker compose up -d 50 | ``` 51 | 52 | 5. Run your application: 53 | ```bash 54 | go run cmd/main.go 55 | ``` 56 | 57 | Your API will be available at `http://localhost:8080` 58 | 59 | ### Creating a CLI Project 60 | 61 | 1. Create a new CLI project: 62 | ```bash 63 | sova-cli create cli my-cli 64 | ``` 65 | 66 | 2. Navigate to your project: 67 | ```bash 68 | cd my-cli 69 | ``` 70 | 71 | 3. Build your CLI: 72 | ```bash 73 | go build -o my-cli cmd/main.go 74 | ``` 75 | 76 | 4. Run your CLI: 77 | ```bash 78 | ./my-cli --help 79 | ``` 80 | 81 | ## Project Structure 82 | 83 | ### API Project Structure 84 | ``` 85 | my-api/ 86 | ├── cmd/main.go # Entry point 87 | ├── internal/ # Internal packages 88 | │ ├── config/ # Configuration 89 | │ ├── handlers/ # HTTP handlers 90 | │ ├── middleware/ # HTTP middleware 91 | │ ├── models/ # Data models 92 | │ ├── routes/ # Route definitions 93 | │ ├── server/ # Server setup 94 | │ └── service/ # Business logic 95 | ├── docker-compose.yml # Docker services 96 | └── .env # Environment variables 97 | ``` 98 | 99 | ### CLI Project Structure 100 | ``` 101 | my-cli/ 102 | ├── cmd/ 103 | │ ├── root/ # Root command 104 | │ └── version/ # Version command 105 | └── internal/ 106 | ├── commands/ # Command implementations 107 | ├── config/ # Configuration 108 | └── utils/ # Utilities 109 | ``` 110 | 111 | ## Configuration 112 | 113 | ### API Project 114 | 115 | 1. Environment Variables (`.env`): 116 | ```env 117 | PORT=8080 118 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/my-api 119 | REDIS_URL=localhost:6379 120 | RABBITMQ_URL=amqp://guest:guest@localhost:5672/ 121 | ``` 122 | 123 | 2. Docker Services (`docker-compose.yml`): 124 | - PostgreSQL (port: 5432) 125 | - Redis (port: 6379) 126 | - RabbitMQ (ports: 5672, 15672) 127 | 128 | ### CLI Project 129 | 130 | Configuration is managed through: 131 | - Command-line flags 132 | - Configuration files 133 | - Environment variables 134 | 135 | ## Development 136 | 137 | ### API Development 138 | 139 | 1. Start dependencies: 140 | ```bash 141 | docker compose up -d 142 | ``` 143 | 144 | 2. Run with hot reload (using air): 145 | ```bash 146 | air 147 | ``` 148 | 149 | 3. Access endpoints: 150 | - Health check: `GET http://localhost:8080/api/health` 151 | - Ping: `GET http://localhost:8080/api/ping` 152 | 153 | ### CLI Development 154 | 155 | 1. Add new commands: 156 | ```bash 157 | sova-cli add command my-command 158 | ``` 159 | 160 | 2. Build and test: 161 | ```bash 162 | go build -o my-cli cmd/main.go 163 | ./my-cli my-command 164 | ``` 165 | 166 | ## Testing 167 | 168 | Run tests: 169 | ```bash 170 | go test ./... 171 | ``` 172 | 173 | ## Common Issues 174 | 175 | 1. Docker services not starting: 176 | - Check if Docker is running 177 | - Verify ports are not in use 178 | - Check docker-compose.yml configuration 179 | 180 | 2. Import path issues: 181 | - Verify module name in go.mod 182 | - Check import paths in source files 183 | - Run `go mod tidy` 184 | 185 | 3. Build errors: 186 | - Run `go mod tidy` 187 | - Check Go version compatibility 188 | - Verify all dependencies are installed 189 | 190 | ## Next Steps 191 | 192 | 1. Customize your project: 193 | - Add new routes (API) 194 | - Create new commands (CLI) 195 | - Implement business logic 196 | 197 | 2. Deploy your application: 198 | - Build for production 199 | - Set up CI/CD 200 | - Configure production environment 201 | 202 | 3. Explore advanced features: 203 | - Custom middleware 204 | - Additional integrations 205 | - Extended configuration -------------------------------------------------------------------------------- /docs/templates.md: -------------------------------------------------------------------------------- 1 | # Project Templates 2 | 3 | This document describes the available project templates and their structure in Sova CLI. 4 | 5 | ## API Template 6 | 7 | The API template creates a Go web service with a clean architecture structure. 8 | 9 | ### Directory Structure 10 | ``` 11 | 📦 project/ 12 | ├── cmd/ # Application entry point 13 | ├── internal/ # Private application code 14 | │ ├── handlers/ # HTTP handlers 15 | │ ├── middleware/# Middleware components 16 | │ ├── models/ # Data models 17 | │ ├── server/ # Server implementation 18 | │ └── service/ # Service layer 19 | ├── pkg/ # Public libraries 20 | ├── api/ # API definitions 21 | ├── routes/ # Route definitions 22 | ├── docs/ # Documentation 23 | └── scripts/ # Build scripts 24 | ``` 25 | 26 | ### Features 27 | - Clean architecture structure 28 | - HTTP server using Gin framework 29 | - Environment configuration with .env 30 | - Docker support with docker-compose 31 | - Optional integrations: 32 | - PostgreSQL database 33 | - Redis cache 34 | - RabbitMQ message queue 35 | - Zap logging middleware 36 | 37 | ### Docker Services 38 | When enabled, the following services are available: 39 | - PostgreSQL (port: 5432) 40 | - Redis (port: 6379) 41 | - RabbitMQ (ports: 5672, 15672) 42 | 43 | ### Configuration 44 | - Environment variables in `.env` 45 | - Docker volumes for data persistence 46 | - Customizable service configurations 47 | 48 | ## CLI Template 49 | 50 | The CLI template creates a command-line application using Cobra. 51 | 52 | ### Directory Structure 53 | ``` 54 | 📦 project/ 55 | ├── cmd/ 56 | │ ├── root/ # Root command 57 | │ └── version/ # Version command 58 | ├── internal/ 59 | │ ├── commands/ # Command implementations 60 | │ ├── config/ # Configuration 61 | │ └── utils/ # Utility functions 62 | ├── pkg/ # Public packages 63 | ├── docs/ # Documentation 64 | ├── scripts/ # Build and deployment scripts 65 | └── tests/ # Integration tests 66 | ``` 67 | 68 | ### Features 69 | - Cobra-based CLI structure 70 | - Command management 71 | - Configuration handling 72 | - Utility functions for CLI operations 73 | 74 | ## Common Features 75 | 76 | Both templates include: 77 | - Go modules support 78 | - `.gitignore` with appropriate exclusions 79 | - Documentation structure 80 | - Test setup 81 | - Build scripts 82 | 83 | ## Recent Updates 84 | 85 | 1. Fixed Import Paths 86 | - Moved routes to `internal/routes` 87 | - Updated import paths in templates 88 | - Fixed module name references 89 | 90 | 2. Docker Compose 91 | - Removed obsolete version attribute 92 | - Added volume configurations 93 | - Improved service definitions 94 | 95 | 3. Project Structure 96 | - Reorganized internal packages 97 | - Added consistent directory structure 98 | - Improved template organization 99 | 100 | 4. Git Configuration 101 | - Added comprehensive `.gitignore` templates 102 | - Separate configurations for API and CLI projects 103 | - Docker-specific ignores for API projects 104 | 105 | ## Usage 106 | 107 | Create a new API project: 108 | ```bash 109 | sova-cli create api my-project 110 | ``` 111 | 112 | Create a new CLI project: 113 | ```bash 114 | sova-cli create cli my-project 115 | ``` 116 | 117 | ## Configuration Options 118 | 119 | ### API Projects 120 | - `UsePostgres`: Enable PostgreSQL support 121 | - `UseRedis`: Enable Redis support 122 | - `UseRabbitMQ`: Enable RabbitMQ support 123 | - `UseZap`: Enable Zap logging middleware 124 | 125 | ### CLI Projects 126 | - Basic CLI structure with extensible commands 127 | - Configuration management with Viper 128 | 129 | ## Creating Custom Templates 130 | 131 | 1. Create a template directory: 132 | ```bash 133 | mkdir -p ~/.sova/templates/my-template 134 | ``` 135 | 136 | 2. Add template files: 137 | ```bash 138 | my-template/ 139 | ├── template.yaml # Template configuration 140 | ├── files/ # Template files 141 | └── hooks/ # Custom scripts 142 | ``` 143 | 144 | 3. Template Configuration (template.yaml): 145 | ```yaml 146 | name: my-template 147 | description: My custom template 148 | version: 1.0.0 149 | files: 150 | - source: files/main.go 151 | target: cmd/main.go 152 | - source: files/config.go 153 | target: internal/config/config.go 154 | ``` 155 | 156 | 4. Use your template: 157 | ```bash 158 | sova init my-project --template my-template 159 | ``` 160 | 161 | ## Template Variables 162 | 163 | Available variables in templates: 164 | 165 | - `{{.ProjectName}}` - Project name 166 | - `{{.Description}}` - Project description 167 | - `{{.Author}}` - Author name 168 | - `{{.Year}}` - Current year 169 | - `{{.GoVersion}}` - Go version 170 | - `{{.License}}` - License type 171 | 172 | ## Examples 173 | 174 | 1. **Custom main.go**: 175 | ```go 176 | package main 177 | 178 | import "fmt" 179 | 180 | func main() { 181 | fmt.Println("Welcome to {{.ProjectName}}!") 182 | } 183 | ``` 184 | 185 | 2. **Custom README.md**: 186 | ```markdown 187 | # {{.ProjectName}} 188 | 189 | {{.Description}} 190 | 191 | ## Author 192 | {{.Author}} 193 | 194 | ## License 195 | {{.License}} © {{.Year}} 196 | ``` -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading Sova CLI 2 | 3 | This guide provides instructions for upgrading your Sova CLI installation to the latest version. 4 | 5 | ## Upgrading from 0.1.0 to 0.1.1 6 | 7 | ### Quick Upgrade 8 | 9 | If you installed Sova CLI using `go install`: 10 | 11 | ```bash 12 | go install github.com/go-sova/sova-cli@latest 13 | ``` 14 | 15 | ### Manual Upgrade 16 | 17 | 1. Download the latest release from the [Releases page](https://github.com/go-sova/sova-cli/releases) 18 | 2. Replace your existing binary with the new version 19 | 3. Verify the installation: 20 | ```bash 21 | sova-cli version 22 | ``` 23 | 24 | ### Post-Upgrade Steps 25 | 26 | 1. Update existing projects: 27 | ```bash 28 | cd your-project 29 | sova-cli update 30 | ``` 31 | This will: 32 | - Update `.gitignore` files 33 | - Fix import paths 34 | - Update Docker Compose configurations 35 | 36 | 2. Review and apply changes: 37 | - Check the new `.gitignore` patterns 38 | - Verify Docker Compose configurations 39 | - Update import paths if necessary 40 | 41 | ### Breaking Changes 42 | 43 | Version 0.1.1 includes no breaking changes. However, some improvements require attention: 44 | 45 | 1. Docker Compose: 46 | - The `version` attribute has been removed 47 | - Review your existing `docker-compose.yml` files 48 | 49 | 2. Import Paths: 50 | - Routes have been moved to `internal/routes` 51 | - Update your imports if you've modified the generated code 52 | 53 | ### New Features 54 | 55 | 1. Enhanced `.gitignore` templates: 56 | - API projects now include Docker-specific ignores 57 | - CLI projects include build and distribution ignores 58 | 59 | 2. Improved Documentation: 60 | - New getting started guide 61 | - Detailed template documentation 62 | - Project structure guidelines 63 | 64 | ## Troubleshooting 65 | 66 | If you encounter issues after upgrading: 67 | 68 | 1. Clean Go module cache: 69 | ```bash 70 | go clean -modcache 71 | ``` 72 | 73 | 2. Regenerate Go module files: 74 | ```bash 75 | go mod tidy 76 | ``` 77 | 78 | 3. Verify template updates: 79 | ```bash 80 | sova-cli doctor 81 | ``` 82 | 83 | ## Support 84 | 85 | If you need help with the upgrade: 86 | 87 | 1. Check the [documentation](README.md) 88 | 2. Open an issue on GitHub 89 | 3. Join our community channels 90 | 91 | ## Rolling Back 92 | 93 | If you need to roll back to a previous version: 94 | 95 | ```bash 96 | # Using go install 97 | go install github.com/go-sova/sova-cli@v0.1.0 98 | 99 | # Or download the specific release from GitHub 100 | # https://github.com/go-sova/sova-cli/releases/tag/v0.1.0 101 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-sova/sova-cli 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.23.5 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 9 | github.com/fatih/color v1.18.0 10 | github.com/spf13/cobra v1.9.1 11 | github.com/spf13/viper v1.20.0 12 | ) 13 | 14 | require ( 15 | github.com/fsnotify/fsnotify v1.8.0 // indirect 16 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 17 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 18 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 19 | github.com/mattn/go-colorable v0.1.13 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 22 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 23 | github.com/sagikazarmark/locafero v0.7.0 // indirect 24 | github.com/sourcegraph/conc v0.3.0 // indirect 25 | github.com/spf13/afero v1.12.0 // indirect 26 | github.com/spf13/cast v1.7.1 // indirect 27 | github.com/spf13/pflag v1.0.6 // indirect 28 | github.com/subosito/gotenv v1.6.0 // indirect 29 | go.uber.org/atomic v1.9.0 // indirect 30 | go.uber.org/multierr v1.9.0 // indirect 31 | golang.org/x/sys v0.29.0 // indirect 32 | golang.org/x/term v0.28.0 // indirect 33 | golang.org/x/text v0.21.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= 2 | github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 3 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 4 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 7 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 12 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 13 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 14 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 15 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 16 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 17 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 18 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 19 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 20 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 22 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= 23 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 24 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 25 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 26 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 34 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 35 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 36 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 37 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 38 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 39 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 40 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 41 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 45 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 46 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 47 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 48 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 49 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 50 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 51 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 52 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 53 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 54 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 55 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 56 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 57 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 58 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 59 | github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= 60 | github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 63 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 67 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 68 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 69 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 70 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 71 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 72 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 73 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 74 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 75 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 76 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 77 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 78 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 79 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 81 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 82 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 90 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 91 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 92 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 93 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 94 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 97 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 98 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 99 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 100 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 101 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 102 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 103 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 104 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 106 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 107 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 110 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 111 | -------------------------------------------------------------------------------- /internal/project/api/generator.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-sova/sova-cli/pkg/questions" 9 | "github.com/go-sova/sova-cli/pkg/utils" 10 | "github.com/go-sova/sova-cli/templates" 11 | ) 12 | 13 | type APIProjectGenerator struct { 14 | ProjectName string 15 | ProjectDir string 16 | Answers *questions.ProjectAnswers 17 | templateLoader *templates.TemplateLoader 18 | fileGenerator *templates.FileGenerator 19 | logger *utils.Logger 20 | } 21 | 22 | func NewAPIProjectGenerator(projectName, projectDir string, answers *questions.ProjectAnswers) *APIProjectGenerator { 23 | loader := templates.NewTemplateLoader() 24 | return &APIProjectGenerator{ 25 | ProjectName: projectName, 26 | ProjectDir: projectDir, 27 | Answers: answers, 28 | templateLoader: loader, 29 | fileGenerator: templates.NewFileGenerator(loader), 30 | logger: utils.NewLoggerWithPrefix(utils.Info, "APIProjectGenerator"), 31 | } 32 | } 33 | 34 | func (g *APIProjectGenerator) SetLogger(logger *utils.Logger) { 35 | g.logger = logger 36 | g.templateLoader.SetLogger(logger) 37 | g.fileGenerator.SetLogger(logger) 38 | } 39 | 40 | func (g *APIProjectGenerator) Generate() (map[string]string, []string, error) { 41 | dirs := []string{ 42 | "cmd", 43 | "internal/server", 44 | "internal/service", 45 | "internal/handlers", 46 | "internal/middleware", 47 | "internal/routes", 48 | } 49 | 50 | fileTemplates := map[string]string{ 51 | "cmd/main.go": "api/main.tpl", 52 | "internal/server/server.go": "api/server.tpl", 53 | "internal/routes/routes.go": "api/routes.tpl", 54 | "internal/service/service.go": "api/service-init.tpl", 55 | "internal/handlers/handlers.go": "api/handlers.tpl", 56 | "internal/middleware/auth.go": "api/middleware.tpl", 57 | ".env": "api/env.tpl", 58 | "docker-compose.yml": "api/docker-compose.tpl", 59 | "Dockerfile": "api/dockerfile.tpl", 60 | "go.mod": "api/go-mod.tpl", 61 | ".gitignore": "api/gitignore.tpl", 62 | } 63 | 64 | if g.Answers.UseZap { 65 | fileTemplates["internal/middleware/logging.go"] = "api/logging.tpl" 66 | } 67 | 68 | if g.Answers.UsePostgres { 69 | fileTemplates["internal/service/postgres.go"] = "api/postgres.tpl" 70 | } 71 | 72 | if g.Answers.UseRedis { 73 | fileTemplates["internal/service/redis.go"] = "api/redis.tpl" 74 | } 75 | 76 | if g.Answers.UseRabbitMQ { 77 | fileTemplates["internal/service/rabbitmq.go"] = "api/rabbitmq.tpl" 78 | } 79 | 80 | files := make(map[string]string) 81 | for filePath, templateName := range fileTemplates { 82 | files[filePath] = templateName 83 | } 84 | 85 | return files, dirs, nil 86 | } 87 | 88 | func (g *APIProjectGenerator) WriteFiles(files map[string]string) error { 89 | for filePath, templateName := range files { 90 | fullPath := filepath.Join(g.ProjectDir, filePath) 91 | 92 | data := map[string]interface{}{ 93 | "ProjectName": g.ProjectName, 94 | "ProjectDescription": "A Go API with clean architecture", 95 | "ModuleName": g.ProjectName, 96 | "GoVersion": "1.21", 97 | "UsePostgres": g.Answers.UsePostgres, 98 | "UseRedis": g.Answers.UseRedis, 99 | "UseRabbitMQ": g.Answers.UseRabbitMQ, 100 | "UseZap": g.Answers.UseZap, 101 | } 102 | 103 | dir := filepath.Dir(fullPath) 104 | if err := os.MkdirAll(dir, 0755); err != nil { 105 | return fmt.Errorf("failed to create directory %s: %v", dir, err) 106 | } 107 | 108 | if err := g.fileGenerator.GenerateFile(templateName, fullPath, data); err != nil { 109 | return fmt.Errorf("failed to generate file %s from template %s: %v", filePath, templateName, err) 110 | } 111 | 112 | fmt.Printf("Created file: %s\n", fullPath) 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/project/api/init.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-sova/sova-cli/pkg/questions" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var InitCmd = &cobra.Command{ 13 | Use: "api [project-name]", 14 | Short: "Initialize a new Go API project", 15 | Long: `Initialize a new Go API project with a clean architecture structure. 16 | This command will create a new directory with the project name and set up all necessary files and directories.`, 17 | Args: cobra.ExactArgs(1), 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | projectName := args[0] 20 | projectDir := filepath.Join(".", projectName) 21 | 22 | if _, err := os.Stat(projectDir); !os.IsNotExist(err) { 23 | return fmt.Errorf("directory %s already exists", projectDir) 24 | } 25 | 26 | if err := os.MkdirAll(projectDir, 0755); err != nil { 27 | return fmt.Errorf("failed to create project directory: %v", err) 28 | } 29 | 30 | answers, err := questions.AskProjectQuestions("api") 31 | if err != nil { 32 | return fmt.Errorf("failed to get project configuration: %v", err) 33 | } 34 | 35 | answers.ProjectName = projectName 36 | 37 | generator := NewAPIProjectGenerator(projectName, projectDir, answers) 38 | 39 | files, dirs, err := generator.Generate() 40 | if err != nil { 41 | return fmt.Errorf("failed to generate project files: %v", err) 42 | } 43 | 44 | for _, dir := range dirs { 45 | dirPath := filepath.Join(projectDir, dir) 46 | if err := os.MkdirAll(dirPath, 0755); err != nil { 47 | return fmt.Errorf("failed to create directory %s: %v", dir, err) 48 | } 49 | fmt.Printf("Created directory: %s\n", dirPath) 50 | } 51 | 52 | if err := generator.WriteFiles(files); err != nil { 53 | return fmt.Errorf("failed to write files: %v", err) 54 | } 55 | 56 | fmt.Printf("\nProject %s created successfully!\n", projectName) 57 | fmt.Println("\nNext steps:") 58 | fmt.Printf("cd %s\n", projectName) 59 | fmt.Println("go mod tidy") 60 | fmt.Println("docker compose up -d") 61 | fmt.Println("go run cmd/main.go") 62 | fmt.Println("\nYour API will be available at http://localhost:8080") 63 | fmt.Println("Test the ping endpoint: curl http://localhost:8080/api/ping") 64 | 65 | return nil 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /internal/project/cli/generator.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-sova/sova-cli/pkg/questions" 9 | "github.com/go-sova/sova-cli/pkg/utils" 10 | "github.com/go-sova/sova-cli/templates" 11 | ) 12 | 13 | type CLIProjectGenerator struct { 14 | ProjectName string 15 | ProjectDir string 16 | Answers *questions.ProjectAnswers 17 | templateLoader *templates.TemplateLoader 18 | fileGenerator *templates.FileGenerator 19 | logger *utils.Logger 20 | } 21 | 22 | func NewCLIProjectGenerator(projectName, projectDir string, answers *questions.ProjectAnswers) *CLIProjectGenerator { 23 | loader := templates.NewTemplateLoader() 24 | return &CLIProjectGenerator{ 25 | ProjectName: projectName, 26 | ProjectDir: projectDir, 27 | Answers: answers, 28 | templateLoader: loader, 29 | fileGenerator: templates.NewFileGenerator(loader), 30 | logger: utils.NewLoggerWithPrefix(utils.Info, "CLIProjectGenerator"), 31 | } 32 | } 33 | 34 | func (g *CLIProjectGenerator) SetLogger(logger *utils.Logger) { 35 | g.logger = logger 36 | g.templateLoader.SetLogger(logger) 37 | g.fileGenerator.SetLogger(logger) 38 | } 39 | 40 | func (g *CLIProjectGenerator) Generate() (map[string]string, []string, error) { 41 | dirs := []string{ 42 | "cmd", 43 | "internal", 44 | "pkg", 45 | "docs", 46 | "scripts", 47 | "test", 48 | "cmd/root", 49 | "internal/commands", 50 | "internal/config", 51 | } 52 | 53 | fileTemplates := map[string]string{ 54 | "cmd/root/root.go": "cli/root.tpl", 55 | "cmd/version/version.go": "cli/version.tpl", 56 | "internal/commands/cmd.go": "cli/commands.tpl", 57 | "internal/config/config.go": "cli/config.tpl", 58 | "internal/utils/utils.go": "cli/utils.tpl", 59 | ".gitignore": "cli/gitignore.tpl", 60 | } 61 | 62 | files := make(map[string]string) 63 | for filePath, templateName := range fileTemplates { 64 | files[filePath] = templateName 65 | } 66 | 67 | return files, dirs, nil 68 | } 69 | 70 | func (g *CLIProjectGenerator) WriteFiles(files map[string]string) error { 71 | for filePath, templateName := range files { 72 | fullPath := filepath.Join(g.ProjectDir, filePath) 73 | 74 | data := map[string]interface{}{ 75 | "ProjectName": g.ProjectName, 76 | "ProjectDescription": "A CLI application with clean architecture", 77 | "ModuleName": g.ProjectName, 78 | "GoVersion": "1.21", 79 | } 80 | 81 | dir := filepath.Dir(fullPath) 82 | if err := os.MkdirAll(dir, 0755); err != nil { 83 | return fmt.Errorf("failed to create directory %s: %v", dir, err) 84 | } 85 | 86 | if err := g.fileGenerator.GenerateFile(templateName, fullPath, data); err != nil { 87 | return fmt.Errorf("failed to generate file %s from template %s: %v", filePath, templateName, err) 88 | } 89 | 90 | fmt.Printf("Created file: %s\n", fullPath) 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/project/cli/init.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-sova/sova-cli/pkg/questions" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var InitCmd = &cobra.Command{ 13 | Use: "cli [project-name]", 14 | Short: "Initialize a new Go CLI project", 15 | Long: `Initialize a new Go CLI project with a clean architecture structure. 16 | This command will create a new directory with the project name and set up all necessary files and directories.`, 17 | Args: cobra.ExactArgs(1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | projectName := args[0] 20 | projectDir := filepath.Join(".", projectName) 21 | 22 | if _, err := os.Stat(projectDir); !os.IsNotExist(err) { 23 | fmt.Printf("Error: directory %s already exists\n", projectDir) 24 | return 25 | } 26 | 27 | if err := os.MkdirAll(projectDir, 0755); err != nil { 28 | fmt.Printf("Error: failed to create project directory: %v\n", err) 29 | return 30 | } 31 | 32 | answers, err := questions.AskProjectQuestions("cli") 33 | if err != nil { 34 | fmt.Printf("Error: failed to get project configuration: %v\n", err) 35 | return 36 | } 37 | 38 | answers.ProjectName = projectName 39 | 40 | generator := NewCLIProjectGenerator(projectName, projectDir, answers) 41 | 42 | files, dirs, err := generator.Generate() 43 | if err != nil { 44 | fmt.Printf("Error: failed to generate project files: %v\n", err) 45 | return 46 | } 47 | 48 | for _, dir := range dirs { 49 | dirPath := filepath.Join(projectDir, dir) 50 | if err := os.MkdirAll(dirPath, 0755); err != nil { 51 | fmt.Printf("Error: failed to create directory %s: %v\n", dir, err) 52 | return 53 | } 54 | fmt.Printf("Created directory: %s\n", dirPath) 55 | } 56 | 57 | if err := generator.WriteFiles(files); err != nil { 58 | fmt.Printf("Error: failed to write files: %v\n", err) 59 | return 60 | } 61 | 62 | fmt.Printf("\nProject %s created successfully!\n", projectName) 63 | fmt.Println("\nNext steps:") 64 | fmt.Printf("1. cd %s\n", projectName) 65 | fmt.Println("2. go mod tidy") 66 | fmt.Println("3. go run main.go") 67 | fmt.Println("\nTry your CLI commands:") 68 | fmt.Printf(" ./%s command1\n", projectName) 69 | fmt.Printf(" ./%s command2\n", projectName) 70 | }, 71 | } 72 | -------------------------------------------------------------------------------- /internal/project/create.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/go-sova/sova-cli/pkg/questions" 10 | "github.com/go-sova/sova-cli/pkg/utils" 11 | "github.com/go-sova/sova-cli/templates" 12 | ) 13 | 14 | type ProjectCreator struct { 15 | logger *utils.Logger 16 | templateLoader *templates.TemplateLoader 17 | fileGenerator *templates.FileGenerator 18 | } 19 | 20 | func NewProjectCreator() *ProjectCreator { 21 | loader := templates.NewTemplateLoader() 22 | return &ProjectCreator{ 23 | logger: utils.NewLoggerWithPrefix(utils.Info, "ProjectCreator"), 24 | templateLoader: loader, 25 | fileGenerator: templates.NewFileGenerator(loader), 26 | } 27 | } 28 | 29 | func (c *ProjectCreator) SetLogger(logger *utils.Logger) { 30 | c.logger = logger 31 | c.templateLoader.SetLogger(logger) 32 | c.fileGenerator.SetLogger(logger) 33 | } 34 | 35 | type ProjectData struct { 36 | ProjectName string 37 | ProjectDescription string 38 | ModuleName string 39 | GoVersion string 40 | Author string 41 | License string 42 | Year string 43 | } 44 | 45 | func (c *ProjectCreator) CreateProject(projectName, projectDir, templateName string, force bool) error { 46 | c.logger.Info("Creating project: %s in directory: %s", projectName, projectDir) 47 | c.logger.Info("Using template: %s", templateName) 48 | 49 | if utils.DirExists(projectDir) { 50 | if !force { 51 | return fmt.Errorf("directory already exists: %s", projectDir) 52 | } 53 | c.logger.Warning("Overwriting existing directory: %s", projectDir) 54 | } 55 | 56 | structure, err := GetProjectStructure(templateName, projectName) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | projectData, err := c.getProjectData(projectName, structure.Description) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | dirs, files := structure.GetAbsolutePaths(projectDir) 67 | for _, dir := range dirs { 68 | c.logger.Debug("Creating directory: %s", dir) 69 | if err := utils.CreateDirIfNotExists(dir); err != nil { 70 | return fmt.Errorf("failed to create directory: %w", err) 71 | } 72 | } 73 | 74 | // Loop through files and handle template category subdirectories 75 | for filePath, templateName := range files { 76 | c.logger.Debug("Generating file: %s from template: %s", filePath, templateName) 77 | if err := c.fileGenerator.GenerateFile(templateName, filePath, projectData); err != nil { 78 | return fmt.Errorf("failed to generate file: %w", err) 79 | } 80 | } 81 | 82 | c.logger.Info("Project created successfully!") 83 | return nil 84 | } 85 | 86 | func (c *ProjectCreator) getProjectData(projectName, projectDescription string) (*ProjectData, error) { 87 | return &ProjectData{ 88 | ProjectName: projectName, 89 | ProjectDescription: projectDescription, 90 | ModuleName: projectName, 91 | GoVersion: "1.21", 92 | Author: "Meyank Singh", 93 | License: "MIT", 94 | Year: fmt.Sprintf("%d", time.Now().Year()), 95 | }, nil 96 | } 97 | 98 | func (c *ProjectCreator) ListAvailableTemplates() ([]string, error) { 99 | return []string{"default", "go-api", "cli"}, nil 100 | } 101 | 102 | func (c *ProjectCreator) GetTemplateDescription(templateName string) (string, error) { 103 | switch templateName { 104 | case "default": 105 | return "A basic Go project with a minimal structure", nil 106 | case "go-api": 107 | return "A Go web application with a complete structure for web development", nil 108 | case "cli": 109 | return "A command-line interface application with Cobra", nil 110 | default: 111 | return "", fmt.Errorf("unknown template: %s", templateName) 112 | } 113 | } 114 | 115 | func CreateProject(projectName, projectDir string, answers *questions.ProjectAnswers) error { 116 | structure, err := GetProjectStructure(answers.ProjectType, projectName) 117 | if err != nil { 118 | return fmt.Errorf("failed to get project structure: %v", err) 119 | } 120 | 121 | dirs, files := structure.GetAbsolutePaths(projectDir) 122 | 123 | for _, dir := range dirs { 124 | if err := utils.CreateDirIfNotExists(dir); err != nil { 125 | return fmt.Errorf("failed to create directory %s: %v", dir, err) 126 | } 127 | } 128 | 129 | for path, template := range files { 130 | dir := filepath.Dir(path) 131 | if err := os.MkdirAll(dir, 0755); err != nil { 132 | return fmt.Errorf("failed to create directory %s: %v", dir, err) 133 | } 134 | 135 | if err := utils.CopyFile(template, path); err != nil { 136 | return fmt.Errorf("failed to copy file %s to %s: %v", template, path, err) 137 | } 138 | } 139 | 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /internal/project/structure.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | ) 7 | 8 | type ProjectStructure struct { 9 | Name string 10 | Description string 11 | Directories []string 12 | Files map[string]string 13 | } 14 | 15 | func APIProjectStructure(projectName string) *ProjectStructure { 16 | structure := &ProjectStructure{ 17 | Name: projectName, 18 | Description: "A Go API project created with Sova CLI", 19 | Directories: []string{ 20 | "cmd", 21 | "internal", 22 | "pkg", 23 | "api", 24 | "docs", 25 | "scripts", 26 | "test", 27 | "internal/handlers", 28 | "internal/middleware", 29 | "internal/models", 30 | "internal/server", 31 | "internal/service", 32 | "internal/routes", 33 | }, 34 | Files: map[string]string{ 35 | "cmd/main.go": "api/main.tpl", 36 | "internal/config/config.go": "api/config.tpl", 37 | "internal/handlers/handlers.go": "api/handlers.tpl", 38 | "internal/middleware/middleware.go": "api/middleware.tpl", 39 | "internal/models/models.go": "api/models.tpl", 40 | "internal/server/server.go": "api/server.tpl", 41 | "internal/routes/routes.go": "api/routes.tpl", 42 | "internal/service/service.go": "api/service-init.tpl", 43 | "internal/service/postgres.go": "api/postgres.tpl", 44 | "internal/service/redis.go": "api/redis.tpl", 45 | "internal/service/rabbitmq.go": "api/rabbitmq.tpl", 46 | "internal/middleware/logging.go": "api/logging.tpl", 47 | ".env": "api/env.tpl", 48 | "docker-compose.yml": "api/docker-compose.tpl", 49 | "go.mod": "api/go-mod.tpl", 50 | ".gitignore": "api/gitignore.tpl", 51 | }, 52 | } 53 | 54 | return structure 55 | } 56 | 57 | func CLIProjectStructure(projectName string) *ProjectStructure { 58 | structure := &ProjectStructure{ 59 | Name: projectName, 60 | Description: "A CLI project created with Sova CLI", 61 | Directories: []string{ 62 | "cmd", 63 | "internal", 64 | "pkg", 65 | "docs", 66 | "scripts", 67 | "test", 68 | "cmd/root", 69 | "internal/commands", 70 | "internal/config", 71 | }, 72 | Files: map[string]string{ 73 | "cmd/root/root.go": "cli/root.tpl", 74 | "cmd/version/version.go": "cli/version.tpl", 75 | "internal/commands/cmd.go": "cli/commands.tpl", 76 | "internal/config/config.go": "cli/config.tpl", 77 | "internal/utils/utils.go": "cli/utils.tpl", 78 | ".gitignore": "cli/gitignore.tpl", 79 | }, 80 | } 81 | 82 | return structure 83 | } 84 | 85 | func GetProjectStructure(templateName, projectName string) (*ProjectStructure, error) { 86 | switch templateName { 87 | case "api": 88 | return APIProjectStructure(projectName), nil 89 | case "cli": 90 | return CLIProjectStructure(projectName), nil 91 | default: 92 | return nil, fmt.Errorf("unknown template: %s", templateName) 93 | } 94 | } 95 | 96 | func (s *ProjectStructure) GetAbsolutePaths(baseDir string) ([]string, map[string]string) { 97 | dirs := make([]string, len(s.Directories)) 98 | files := make(map[string]string) 99 | 100 | for i, dir := range s.Directories { 101 | dirs[i] = filepath.Join(baseDir, dir) 102 | } 103 | 104 | for path, template := range s.Files { 105 | files[filepath.Join(baseDir, path)] = template 106 | } 107 | 108 | return dirs, files 109 | } 110 | -------------------------------------------------------------------------------- /internal/project/template_loader.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-sova/sova-cli/pkg/utils" 7 | "github.com/go-sova/sova-cli/templates" 8 | ) 9 | 10 | type TemplateManager struct { 11 | logger *utils.Logger 12 | templateLoader *templates.TemplateLoader 13 | } 14 | 15 | func NewTemplateManager() *TemplateManager { 16 | loader := templates.NewTemplateLoader() 17 | return &TemplateManager{ 18 | logger: utils.NewLoggerWithPrefix(utils.Info, "TemplateManager"), 19 | templateLoader: loader, 20 | } 21 | } 22 | 23 | func (m *TemplateManager) SetLogger(logger *utils.Logger) { 24 | m.logger = logger 25 | m.templateLoader.SetLogger(logger) 26 | } 27 | 28 | func (m *TemplateManager) ListTemplates() ([]string, error) { 29 | m.logger.Debug("Listing templates") 30 | return []string{"api", "cli"}, nil 31 | } 32 | 33 | func (m *TemplateManager) GetTemplateDescription(templateName string) (string, error) { 34 | m.logger.Debug("Getting description for template: %s", templateName) 35 | 36 | switch templateName { 37 | case "api": 38 | return "A Go API project with a complete structure for API development", nil 39 | case "cli": 40 | return "A command-line interface application with Cobra", nil 41 | } 42 | 43 | return "", fmt.Errorf("unknown template: %s", templateName) 44 | } 45 | 46 | func (m *TemplateManager) ValidateTemplate(templateName string) error { 47 | m.logger.Debug("Validating template: %s", templateName) 48 | 49 | switch templateName { 50 | case "api", "cli": 51 | return nil 52 | } 53 | 54 | return fmt.Errorf("unknown template: %s", templateName) 55 | } 56 | -------------------------------------------------------------------------------- /internal/templates/files.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "text/template" 9 | 10 | "github.com/go-sova/sova-cli/pkg/utils" 11 | ) 12 | 13 | type FileGenerator struct { 14 | loader *TemplateLoader 15 | logger *utils.Logger 16 | } 17 | 18 | func NewFileGenerator(loader *TemplateLoader) *FileGenerator { 19 | return &FileGenerator{ 20 | loader: loader, 21 | logger: utils.NewLoggerWithPrefix(utils.Info, "FileGenerator"), 22 | } 23 | } 24 | 25 | func (g *FileGenerator) SetLogger(logger *utils.Logger) { 26 | g.logger = logger 27 | } 28 | 29 | func (g *FileGenerator) GenerateFile(templateName, outputPath string, data interface{}) error { 30 | g.logger.Debug("Generating file from template: %s -> %s", templateName, outputPath) 31 | 32 | if utils.FileExists(outputPath) { 33 | g.logger.Warning("Output file already exists: %s", outputPath) 34 | } 35 | 36 | tmpl, err := g.loader.LoadTemplate(templateName) 37 | if err != nil { 38 | return fmt.Errorf("failed to load template: %w", err) 39 | } 40 | 41 | var buf bytes.Buffer 42 | if err := tmpl.Execute(&buf, data); err != nil { 43 | return fmt.Errorf("failed to execute template: %w", err) 44 | } 45 | 46 | dir := filepath.Dir(outputPath) 47 | if err := utils.CreateDirIfNotExists(dir); err != nil { 48 | return fmt.Errorf("failed to create directory: %w", err) 49 | } 50 | 51 | if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil { 52 | return fmt.Errorf("failed to write file: %w", err) 53 | } 54 | 55 | g.logger.Info("Generated file: %s", outputPath) 56 | return nil 57 | } 58 | 59 | func (g *FileGenerator) GenerateFileWithFuncs(templateName, outputPath string, data interface{}, funcs template.FuncMap) error { 60 | g.logger.Debug("Generating file from template with funcs: %s -> %s", templateName, outputPath) 61 | 62 | if utils.FileExists(outputPath) { 63 | g.logger.Warning("Output file already exists: %s", outputPath) 64 | } 65 | 66 | tmpl, err := g.loader.LoadTemplateWithFuncs(templateName, funcs) 67 | if err != nil { 68 | return fmt.Errorf("failed to load template: %w", err) 69 | } 70 | 71 | var buf bytes.Buffer 72 | if err := tmpl.Execute(&buf, data); err != nil { 73 | return fmt.Errorf("failed to execute template: %w", err) 74 | } 75 | 76 | dir := filepath.Dir(outputPath) 77 | if err := utils.CreateDirIfNotExists(dir); err != nil { 78 | return fmt.Errorf("failed to create directory: %w", err) 79 | } 80 | 81 | if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil { 82 | return fmt.Errorf("failed to write file: %w", err) 83 | } 84 | 85 | g.logger.Info("Generated file: %s", outputPath) 86 | return nil 87 | } 88 | 89 | func (g *FileGenerator) GenerateMultipleFiles(templates map[string]string, outputDir string, data interface{}) error { 90 | g.logger.Debug("Generating multiple files in: %s", outputDir) 91 | 92 | for templateName, outputFile := range templates { 93 | outputPath := filepath.Join(outputDir, outputFile) 94 | if err := g.GenerateFile(templateName, outputPath, data); err != nil { 95 | return fmt.Errorf("failed to generate file %s: %w", outputFile, err) 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (g *FileGenerator) GenerateMultipleFilesWithFuncs(templates map[string]string, outputDir string, data interface{}, funcs template.FuncMap) error { 103 | g.logger.Debug("Generating multiple files with funcs in: %s", outputDir) 104 | 105 | for templateName, outputFile := range templates { 106 | outputPath := filepath.Join(outputDir, outputFile) 107 | if err := g.GenerateFileWithFuncs(templateName, outputPath, data, funcs); err != nil { 108 | return fmt.Errorf("failed to generate file %s: %w", outputFile, err) 109 | } 110 | } 111 | 112 | return nil 113 | } -------------------------------------------------------------------------------- /internal/templates/loader.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "text/template" 5 | 6 | "github.com/go-sova/sova-cli/pkg/utils" 7 | ) 8 | 9 | type TemplateLoader struct { 10 | logger *utils.Logger 11 | } 12 | 13 | func NewTemplateLoader() *TemplateLoader { 14 | return &TemplateLoader{ 15 | logger: utils.NewLoggerWithPrefix(utils.Info, "TemplateLoader"), 16 | } 17 | } 18 | 19 | func (l *TemplateLoader) SetLogger(logger *utils.Logger) { 20 | l.logger = logger 21 | } 22 | 23 | func (l *TemplateLoader) LoadTemplate(name string) (*template.Template, error) { 24 | return template.New(name).ParseFiles(name) 25 | } 26 | 27 | func (l *TemplateLoader) LoadTemplateWithFuncs(name string, funcs template.FuncMap) (*template.Template, error) { 28 | return template.New(name).Funcs(funcs).ParseFiles(name) 29 | } -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // Version is the current version of Sova CLI 10 | Version = "dev" 11 | 12 | // BuildDate is the date when the binary was built 13 | BuildDate = "unknown" 14 | 15 | // GitCommit is the git commit hash 16 | GitCommit = "unknown" 17 | 18 | // GoVersion is the version of Go used to build the binary 19 | GoVersion = runtime.Version() 20 | 21 | // Platform is the operating system and architecture 22 | Platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) 23 | ) 24 | 25 | // Info returns version information 26 | type Info struct { 27 | Version string `json:"version"` 28 | BuildDate string `json:"build_date"` 29 | GitCommit string `json:"git_commit"` 30 | GoVersion string `json:"go_version"` 31 | Platform string `json:"platform"` 32 | } 33 | 34 | // GetInfo returns the version information 35 | func GetInfo() Info { 36 | return Info{ 37 | Version: Version, 38 | BuildDate: BuildDate, 39 | GitCommit: GitCommit, 40 | GoVersion: GoVersion, 41 | Platform: Platform, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-sova/sova-cli/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.Execute(); err != nil { 12 | fmt.Fprintln(os.Stderr, err) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/questions/questions.go: -------------------------------------------------------------------------------- 1 | package questions 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/AlecAivazis/survey/v2" 7 | ) 8 | 9 | type ProjectAnswers struct { 10 | ProjectName string 11 | ProjectType string 12 | UseZap bool 13 | UsePostgres bool 14 | UseRedis bool 15 | UseRabbitMQ bool 16 | } 17 | 18 | func AskProjectName() (string, error) { 19 | var name string 20 | prompt := &survey.Input{ 21 | Message: "What is your project name?", 22 | Help: "The name of your new project", 23 | } 24 | 25 | err := survey.AskOne(prompt, &name) 26 | if err != nil { 27 | return "", fmt.Errorf("failed to get project name: %v", err) 28 | } 29 | 30 | if name == "" { 31 | return "", fmt.Errorf("project name cannot be empty") 32 | } 33 | 34 | return name, nil 35 | } 36 | 37 | func AskProjectType() (string, error) { 38 | var projectType string 39 | prompt := &survey.Select{ 40 | Message: "What type of project are you building?", 41 | Options: []string{"api", "cli"}, 42 | Default: "api", 43 | } 44 | 45 | err := survey.AskOne(prompt, &projectType) 46 | if err != nil { 47 | return "", fmt.Errorf("failed to get project type: %v", err) 48 | } 49 | 50 | return projectType, nil 51 | } 52 | 53 | func AskProjectQuestions(projectType string) (*ProjectAnswers, error) { 54 | answers := &ProjectAnswers{ 55 | ProjectType: projectType, 56 | } 57 | 58 | switch projectType { 59 | case "api": 60 | prompt := &survey.Confirm{ 61 | Message: "Would you like to use zap as a logger?", 62 | Default: true, 63 | } 64 | err := survey.AskOne(prompt, &answers.UseZap) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | prompt = &survey.Confirm{ 70 | Message: "Would you like to use PostgreSQL?", 71 | Default: true, 72 | } 73 | err = survey.AskOne(prompt, &answers.UsePostgres) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | prompt = &survey.Confirm{ 79 | Message: "Would you like to use Redis?", 80 | Default: false, 81 | } 82 | err = survey.AskOne(prompt, &answers.UseRedis) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | prompt = &survey.Confirm{ 88 | Message: "Would you like to use RabbitMQ?", 89 | Default: false, 90 | } 91 | err = survey.AskOne(prompt, &answers.UseRabbitMQ) 92 | if err != nil { 93 | return nil, err 94 | } 95 | case "cli": 96 | 97 | prompt := &survey.Confirm{ 98 | Message: "Would you like to use zap as a logger?", 99 | Default: false, 100 | } 101 | err := survey.AskOne(prompt, &answers.UseZap) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | answers.UsePostgres = false 107 | answers.UseRedis = false 108 | answers.UseRabbitMQ = false 109 | default: 110 | return nil, fmt.Errorf("unsupported project type: %s", projectType) 111 | } 112 | 113 | return answers, nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/utils/file_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func FileExists(filename string) bool { 13 | info, err := os.Stat(filename) 14 | if os.IsNotExist(err) { 15 | return false 16 | } 17 | return !info.IsDir() 18 | } 19 | 20 | func DirExists(dirname string) bool { 21 | info, err := os.Stat(dirname) 22 | if os.IsNotExist(err) { 23 | return false 24 | } 25 | return info.IsDir() 26 | } 27 | 28 | func CreateDirIfNotExists(dirname string) error { 29 | if !DirExists(dirname) { 30 | return os.MkdirAll(dirname, os.ModePerm) 31 | } 32 | return nil 33 | } 34 | 35 | func WriteFile(filename string, data []byte) error { 36 | dir := filepath.Dir(filename) 37 | if err := CreateDirIfNotExists(dir); err != nil { 38 | return err 39 | } 40 | return os.WriteFile(filename, data, 0644) 41 | } 42 | 43 | func ReadFile(filename string) ([]byte, error) { 44 | if !FileExists(filename) { 45 | return nil, fmt.Errorf("file not found: %s", filename) 46 | } 47 | return os.ReadFile(filename) 48 | } 49 | 50 | func CopyFile(src, dst string) error { 51 | srcInfo, err := os.Stat(src) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if !srcInfo.Mode().IsRegular() { 57 | return fmt.Errorf("%s is not a regular file", src) 58 | } 59 | 60 | srcFile, err := os.Open(src) 61 | if err != nil { 62 | return err 63 | } 64 | defer srcFile.Close() 65 | 66 | dstDir := filepath.Dir(dst) 67 | if err := CreateDirIfNotExists(dstDir); err != nil { 68 | return err 69 | } 70 | 71 | dstFile, err := os.Create(dst) 72 | if err != nil { 73 | return err 74 | } 75 | defer dstFile.Close() 76 | 77 | _, err = io.Copy(dstFile, srcFile) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return os.Chmod(dst, srcInfo.Mode()) 83 | } 84 | 85 | func CopyDir(src, dst string) error { 86 | srcInfo, err := os.Stat(src) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if !srcInfo.IsDir() { 92 | return fmt.Errorf("%s is not a directory", src) 93 | } 94 | 95 | if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { 96 | return err 97 | } 98 | 99 | entries, err := os.ReadDir(src) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | for _, entry := range entries { 105 | srcPath := filepath.Join(src, entry.Name()) 106 | dstPath := filepath.Join(dst, entry.Name()) 107 | 108 | if entry.IsDir() { 109 | if err := CopyDir(srcPath, dstPath); err != nil { 110 | return err 111 | } 112 | } else { 113 | if err := CopyFile(srcPath, dstPath); err != nil { 114 | return err 115 | } 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func GetFileExtension(filename string) string { 123 | return strings.TrimPrefix(filepath.Ext(filename), ".") 124 | } 125 | 126 | func GetFileNameWithoutExtension(filename string) string { 127 | return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)) 128 | } 129 | 130 | func IsTextFile(filename string) bool { 131 | textExtensions := map[string]bool{ 132 | "txt": true, 133 | "md": true, 134 | "go": true, 135 | "js": true, 136 | "ts": true, 137 | "html": true, 138 | "css": true, 139 | "json": true, 140 | "yaml": true, 141 | "yml": true, 142 | "xml": true, 143 | "sh": true, 144 | "bat": true, 145 | "py": true, 146 | "rb": true, 147 | "java": true, 148 | "c": true, 149 | "cpp": true, 150 | "h": true, 151 | "hpp": true, 152 | } 153 | 154 | ext := GetFileExtension(filename) 155 | return textExtensions[ext] 156 | } 157 | 158 | func GetCurrentYear() string { 159 | return time.Now().Format("2006") 160 | } 161 | -------------------------------------------------------------------------------- /pkg/utils/input_reader.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | ) 12 | 13 | type InputReader struct { 14 | reader *bufio.Reader 15 | } 16 | 17 | func NewInputReader() *InputReader { 18 | return &InputReader{ 19 | reader: bufio.NewReader(os.Stdin), 20 | } 21 | } 22 | 23 | func (r *InputReader) ReadInput(prompt string) (string, error) { 24 | fmt.Print(prompt) 25 | input, err := r.reader.ReadString('\n') 26 | if err != nil { 27 | return "", err 28 | } 29 | return strings.TrimSpace(input), nil 30 | } 31 | 32 | func (r *InputReader) ReadInputWithDefault(prompt, defaultValue string) (string, error) { 33 | fmt.Printf("%s [%s]: ", prompt, defaultValue) 34 | input, err := r.reader.ReadString('\n') 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | input = strings.TrimSpace(input) 40 | if input == "" { 41 | return defaultValue, nil 42 | } 43 | return input, nil 44 | } 45 | 46 | func (r *InputReader) ReadInputWithOptions(prompt string, options []string, defaultOption string) (string, error) { 47 | fmt.Println(prompt) 48 | for i, option := range options { 49 | if option == defaultOption { 50 | color.New(color.FgGreen).Printf("[%d] %s (default)\n", i+1, option) 51 | } else { 52 | fmt.Printf("[%d] %s\n", i+1, option) 53 | } 54 | } 55 | 56 | fmt.Print("Enter your choice: ") 57 | input, err := r.reader.ReadString('\n') 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | input = strings.TrimSpace(input) 63 | 64 | if input == "" { 65 | return defaultOption, nil 66 | } 67 | 68 | if num, err := strconv.Atoi(input); err == nil { 69 | if num >= 1 && num <= len(options) { 70 | return options[num-1], nil 71 | } 72 | return "", fmt.Errorf("invalid option: %d", num) 73 | } 74 | 75 | for _, option := range options { 76 | if strings.EqualFold(input, option) { 77 | return option, nil 78 | } 79 | } 80 | 81 | return "", fmt.Errorf("invalid option: %s", input) 82 | } 83 | 84 | func (r *InputReader) ConfirmAction(prompt string) (bool, error) { 85 | fmt.Printf("%s (y/n): ", prompt) 86 | input, err := r.reader.ReadString('\n') 87 | if err != nil { 88 | return false, err 89 | } 90 | 91 | input = strings.ToLower(strings.TrimSpace(input)) 92 | return input == "y" || input == "yes", nil 93 | } 94 | 95 | func (r *InputReader) ReadInt(prompt string) (int, error) { 96 | fmt.Print(prompt) 97 | input, err := r.reader.ReadString('\n') 98 | if err != nil { 99 | return 0, err 100 | } 101 | 102 | input = strings.TrimSpace(input) 103 | return strconv.Atoi(input) 104 | } 105 | 106 | func (r *InputReader) ReadIntWithDefault(prompt string, defaultValue int) (int, error) { 107 | fmt.Printf("%s [%d]: ", prompt, defaultValue) 108 | input, err := r.reader.ReadString('\n') 109 | if err != nil { 110 | return 0, err 111 | } 112 | 113 | input = strings.TrimSpace(input) 114 | if input == "" { 115 | return defaultValue, nil 116 | } 117 | 118 | return strconv.Atoi(input) 119 | } 120 | 121 | var DefaultInputReader = NewInputReader() 122 | 123 | func ReadInput(prompt string) (string, error) { 124 | return DefaultInputReader.ReadInput(prompt) 125 | } 126 | 127 | func ReadInputWithDefault(prompt, defaultValue string) (string, error) { 128 | return DefaultInputReader.ReadInputWithDefault(prompt, defaultValue) 129 | } 130 | 131 | func ReadInputWithOptions(prompt string, options []string, defaultOption string) (string, error) { 132 | return DefaultInputReader.ReadInputWithOptions(prompt, options, defaultOption) 133 | } 134 | 135 | func ConfirmAction(prompt string) (bool, error) { 136 | return DefaultInputReader.ConfirmAction(prompt) 137 | } 138 | 139 | func ReadInt(prompt string) (int, error) { 140 | return DefaultInputReader.ReadInt(prompt) 141 | } 142 | 143 | func ReadIntWithDefault(prompt string, defaultValue int) (int, error) { 144 | return DefaultInputReader.ReadIntWithDefault(prompt, defaultValue) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | type LogLevel int 13 | 14 | const ( 15 | Debug LogLevel = iota 16 | Info 17 | Warning 18 | Error 19 | Fatal 20 | ) 21 | 22 | var levelNames = map[LogLevel]string{ 23 | Debug: "DEBUG", 24 | Info: "INFO", 25 | Warning: "WARNING", 26 | Error: "ERROR", 27 | Fatal: "FATAL", 28 | } 29 | 30 | type Logger struct { 31 | level LogLevel 32 | output io.Writer 33 | prefix string 34 | } 35 | 36 | func NewLogger(level LogLevel) *Logger { 37 | return &Logger{ 38 | level: level, 39 | output: os.Stderr, 40 | prefix: "", 41 | } 42 | } 43 | 44 | func NewLoggerWithPrefix(level LogLevel, prefix string) *Logger { 45 | return &Logger{ 46 | level: level, 47 | output: os.Stderr, 48 | prefix: prefix, 49 | } 50 | } 51 | 52 | func (l *Logger) SetOutput(output io.Writer) { 53 | l.output = output 54 | } 55 | 56 | func (l *Logger) SetLevel(level LogLevel) { 57 | l.level = level 58 | } 59 | 60 | func (l *Logger) SetPrefix(prefix string) { 61 | l.prefix = prefix 62 | } 63 | 64 | func (l *Logger) Log(level LogLevel, format string, args ...interface{}) { 65 | if level >= l.level { 66 | timestamp := time.Now().Format("2006-01-02 15:04:05") 67 | prefix := "" 68 | if l.prefix != "" { 69 | prefix = fmt.Sprintf("[%s] ", l.prefix) 70 | } 71 | 72 | message := fmt.Sprintf(format, args...) 73 | logLine := fmt.Sprintf("%s %s[%s] %s\n", timestamp, prefix, levelNames[level], message) 74 | 75 | switch level { 76 | case Debug: 77 | fmt.Fprint(l.output, logLine) 78 | case Info: 79 | color.New(color.FgBlue).Fprint(l.output, logLine) 80 | case Warning: 81 | color.New(color.FgYellow).Fprint(l.output, logLine) 82 | case Error, Fatal: 83 | color.New(color.FgRed).Fprint(l.output, logLine) 84 | } 85 | } 86 | 87 | if level == Fatal { 88 | os.Exit(1) 89 | } 90 | } 91 | 92 | func (l *Logger) Debug(format string, args ...interface{}) { 93 | l.Log(Debug, format, args...) 94 | } 95 | 96 | func (l *Logger) Info(format string, args ...interface{}) { 97 | l.Log(Info, format, args...) 98 | } 99 | 100 | func (l *Logger) Warning(format string, args ...interface{}) { 101 | l.Log(Warning, format, args...) 102 | } 103 | 104 | func (l *Logger) Error(format string, args ...interface{}) { 105 | l.Log(Error, format, args...) 106 | } 107 | 108 | func (l *Logger) Fatal(format string, args ...interface{}) { 109 | l.Log(Fatal, format, args...) 110 | } 111 | 112 | var DefaultLogger = NewLogger(Info) 113 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION="v0.1.1" 4 | BINARY_NAME="sova" 5 | BUILD_DIR="dist" 6 | BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S') 7 | GIT_COMMIT=$(git rev-parse --short HEAD) 8 | MODULE="github.com/go-sova/sova-cli/internal/version" 9 | 10 | # Create build directory 11 | mkdir -p $BUILD_DIR 12 | 13 | # Build flags 14 | BUILD_FLAGS=( 15 | "-X '${MODULE}.Version=${VERSION}'" 16 | "-X '${MODULE}.BuildDate=${BUILD_DATE}'" 17 | "-X '${MODULE}.GitCommit=${GIT_COMMIT}'" 18 | ) 19 | BUILD_FLAGS_STR=$(IFS=' '; echo "${BUILD_FLAGS[*]}") 20 | 21 | # Build for different platforms 22 | GOOS=linux GOARCH=amd64 go build -ldflags "${BUILD_FLAGS_STR}" -o $BUILD_DIR/${BINARY_NAME}_linux_amd64 ./main.go 23 | tar -czf $BUILD_DIR/${BINARY_NAME}_linux_amd64.tar.gz -C $BUILD_DIR ${BINARY_NAME}_linux_amd64 24 | rm $BUILD_DIR/${BINARY_NAME}_linux_amd64 25 | 26 | GOOS=darwin GOARCH=amd64 go build -ldflags "${BUILD_FLAGS_STR}" -o $BUILD_DIR/${BINARY_NAME}_darwin_amd64 ./main.go 27 | tar -czf $BUILD_DIR/${BINARY_NAME}_darwin_amd64.tar.gz -C $BUILD_DIR ${BINARY_NAME}_darwin_amd64 28 | rm $BUILD_DIR/${BINARY_NAME}_darwin_amd64 29 | 30 | GOOS=windows GOARCH=amd64 go build -ldflags "${BUILD_FLAGS_STR}" -o $BUILD_DIR/${BINARY_NAME}_windows_amd64.exe ./main.go 31 | tar -czf $BUILD_DIR/${BINARY_NAME}_windows_amd64.tar.gz -C $BUILD_DIR ${BINARY_NAME}_windows_amd64.exe 32 | rm $BUILD_DIR/${BINARY_NAME}_windows_amd64.exe 33 | 34 | # Create checksums 35 | cd $BUILD_DIR 36 | if [[ "$OSTYPE" == "darwin"* ]]; then 37 | # macOS 38 | shasum -a 256 *.tar.gz > checksums.txt 39 | else 40 | # Linux and others 41 | sha256sum *.tar.gz > checksums.txt 42 | fi 43 | cd .. 44 | 45 | echo "Build complete! Archives are available in the $BUILD_DIR directory" -------------------------------------------------------------------------------- /scripts/install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | :: Define download URL 5 | set URL=https://raw.githubusercontent.com/go-sova/sova-cli/master/scripts/install.ps1 6 | set PS_SCRIPT=install.ps1 7 | 8 | :: Check if curl exists, else use PowerShell to download 9 | where curl >nul 2>nul 10 | if %errorlevel% neq 0 ( 11 | echo curl not found, using PowerShell to download... 12 | powershell -Command "Invoke-WebRequest -Uri '%URL%' -OutFile '%PS_SCRIPT%'" 13 | ) else ( 14 | echo Downloading install.ps1 using curl... 15 | curl -fsSL %URL% -o %PS_SCRIPT% 16 | ) 17 | 18 | :: Run the PowerShell script with Execution Policy Bypass 19 | powershell -NoProfile -ExecutionPolicy Bypass -File %PS_SCRIPT% 20 | 21 | del %PS_SCRIPT% 22 | 23 | endlocal 24 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | # Set error handling 2 | $ErrorActionPreference = "Stop" 3 | 4 | # Define variables 5 | $repoOwner = "go-sova" 6 | $repoName = "sova-cli" 7 | $cliName = "sova" 8 | $arch = "amd64" 9 | $installDir = "$env:LOCALAPPDATA\$cliName" 10 | 11 | function Cleanup { 12 | param ( 13 | [string]$tarFile 14 | ) 15 | if (Test-Path $tarFile) { 16 | Remove-Item -Path $tarFile -Force -ErrorAction SilentlyContinue 17 | } 18 | } 19 | 20 | function Get-InstalledVersion { 21 | $sovaBinary = Join-Path $installDir "sova.exe" 22 | if (Test-Path $sovaBinary) { 23 | try { 24 | $version = & $sovaBinary --version 2>$null 25 | return $version 26 | } catch { 27 | return $null 28 | } 29 | } 30 | return $null 31 | } 32 | 33 | Write-Host "Installing $cliName..." -ForegroundColor Cyan 34 | 35 | # Check if already installed 36 | $installedVersion = Get-InstalledVersion 37 | if ($installedVersion) { 38 | Write-Host "Current version: $installedVersion" 39 | } 40 | 41 | # Ensure the install directory exists 42 | if (!(Test-Path -Path $installDir)) { 43 | New-Item -ItemType Directory -Path $installDir -Force | Out-Null 44 | } 45 | 46 | # Fetch the latest release tag from GitHub API 47 | try { 48 | $latestRelease = (Invoke-RestMethod -Uri "https://api.github.com/repos/$repoOwner/$repoName/releases/latest" -Headers @{"User-Agent"="Mozilla/5.0"}).tag_name 49 | if (!$latestRelease) { 50 | throw "No release found" 51 | } 52 | 53 | if ($installedVersion -eq $latestRelease) { 54 | Write-Host "Latest version ($latestRelease) already installed." 55 | exit 0 56 | } 57 | } catch { 58 | Write-Host "Error: Failed to check latest version." -ForegroundColor Red 59 | exit 1 60 | } 61 | 62 | # Download the CLI archive 63 | $assetName = "${cliName}_windows_${arch}.tar.gz" 64 | $downloadUrl = "https://github.com/$repoOwner/$repoName/releases/download/$latestRelease/$assetName" 65 | $tarFile = "$env:TEMP\$assetName" 66 | 67 | try { 68 | Write-Host "Downloading version $latestRelease..." -ForegroundColor Cyan 69 | Invoke-WebRequest -Uri $downloadUrl -OutFile $tarFile -Headers @{"User-Agent"="Mozilla/5.0"} 70 | } catch { 71 | Write-Host "Error: Download failed. Please check your internet connection." -ForegroundColor Red 72 | Cleanup -tarFile $tarFile 73 | exit 1 74 | } 75 | 76 | # Extract and set up the executable 77 | try { 78 | Write-Host "Installing..." -ForegroundColor Cyan 79 | 80 | # Extract archive 81 | tar -xzf $tarFile -C $installDir 82 | if ($LASTEXITCODE -ne 0) { 83 | throw "Failed to extract archive" 84 | } 85 | 86 | # Clean up macOS metadata files 87 | Get-ChildItem -Path $installDir -Filter "._*" | Remove-Item -Force 88 | 89 | # Rename the executable 90 | $targetExe = "sova_windows_amd64.exe" 91 | $exeFile = Get-ChildItem -Path $installDir -Filter $targetExe | Select-Object -First 1 92 | 93 | if ($exeFile) { 94 | $targetPath = Join-Path $installDir "sova.exe" 95 | if (Test-Path $targetPath) { 96 | Remove-Item $targetPath -Force 97 | } 98 | Move-Item -Path $exeFile.FullName -Destination $targetPath -Force 99 | 100 | # Verify the binary works 101 | $testResult = & $targetPath --version 2>$null 102 | if ($LASTEXITCODE -ne 0) { 103 | throw "Binary verification failed" 104 | } 105 | } else { 106 | throw "Required files not found" 107 | } 108 | 109 | # Add to PATH 110 | $path = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User) 111 | if ($installDir -notin $path) { 112 | [System.Environment]::SetEnvironmentVariable("Path", "$path;$installDir", [System.EnvironmentVariableTarget]::User) 113 | } 114 | 115 | # Cleanup 116 | Cleanup -tarFile $tarFile 117 | 118 | Write-Host "`n$cliName $latestRelease installed successfully!" -ForegroundColor Green 119 | Write-Host "Please restart your terminal to use $cliName." 120 | } catch { 121 | Write-Host "Error: Installation failed - $($_.Exception.Message)" -ForegroundColor Red 122 | Cleanup -tarFile $tarFile 123 | exit 1 124 | } 125 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Stop script on error 4 | 5 | OS_TYPE=$(uname -s 2>/dev/null || echo "Windows") 6 | ARCH="amd64" 7 | 8 | case "$OS_TYPE" in 9 | Linux*) OS="linux";; 10 | Darwin*) OS="darwin";; 11 | CYGWIN*|MINGW*|MSYS*) OS="windows";; 12 | Windows) OS="windows";; 13 | *) echo "Unsupported OS: $OS_TYPE"; exit 1;; 14 | esac 15 | 16 | if [ "$OS" == "windows" ]; then 17 | echo "Detected Windows: Please use the Windows installer (install.bat or install.ps1)" 18 | exit 1 19 | fi 20 | 21 | REPO_OWNER="go-sova" 22 | REPO_NAME="sova-cli" 23 | CLI_NAME="sova" 24 | INSTALL_DIR="/usr/local/bin" 25 | 26 | echo "Detected OS: $OS" 27 | echo "Fetching latest release of $CLI_NAME..." 28 | 29 | LATEST_RELEASE=$(curl -fsSL "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 30 | 31 | if [ -z "$LATEST_RELEASE" ]; then 32 | echo "Error: Failed to get the latest release version." 33 | exit 1 34 | fi 35 | 36 | echo "Latest release found: $LATEST_RELEASE" 37 | 38 | ASSET_NAME="${CLI_NAME}_${OS}_${ARCH}.tar.gz" 39 | DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/$LATEST_RELEASE/$ASSET_NAME" 40 | 41 | echo "Downloading $CLI_NAME from $DOWNLOAD_URL..." 42 | curl -fsSL -o "$ASSET_NAME" "$DOWNLOAD_URL" 43 | 44 | if [ ! -f "$ASSET_NAME" ]; then 45 | echo "Error: Download failed." 46 | exit 1 47 | fi 48 | 49 | echo "Extracting files..." 50 | tar -xzf "$ASSET_NAME" 51 | 52 | EXTRACTED_BINARY="${CLI_NAME}_${OS}_${ARCH}" 53 | 54 | if [ ! -f "$EXTRACTED_BINARY" ]; then 55 | echo "Error: Extracted binary not found." 56 | rm -f "$ASSET_NAME" 57 | exit 1 58 | fi 59 | 60 | mv "$EXTRACTED_BINARY" "$CLI_NAME" 61 | 62 | echo "Installing $CLI_NAME to $INSTALL_DIR..." 63 | chmod +x "$CLI_NAME" 64 | sudo mv "$CLI_NAME" "$INSTALL_DIR/$CLI_NAME" 65 | 66 | rm -f "$ASSET_NAME" 67 | 68 | echo "Installation completed successfully." 69 | echo "Run '$CLI_NAME --help' to verify the installation." 70 | -------------------------------------------------------------------------------- /templates/api/docker-compose.tpl: -------------------------------------------------------------------------------- 1 | services: 2 | {{if .UsePostgres}}postgres: 3 | image: postgres:latest 4 | ports: 5 | - "5432:5432" 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | - POSTGRES_DB={{.ProjectName}} 10 | volumes: 11 | - postgres_data:/var/lib/postgresql/data{{end}} 12 | 13 | {{if .UseRedis}}redis: 14 | image: redis:latest 15 | ports: 16 | - "6379:6379" 17 | volumes: 18 | - redis_data:/data{{end}} 19 | 20 | {{if .UseRabbitMQ}}rabbitmq: 21 | image: rabbitmq:3-management 22 | ports: 23 | - "5672:5672" 24 | - "15672:15672" 25 | environment: 26 | - RABBITMQ_DEFAULT_USER=guest 27 | - RABBITMQ_DEFAULT_PASS=guest 28 | volumes: 29 | - rabbitmq_data:/var/lib/rabbitmq{{end}} 30 | 31 | volumes: 32 | {{if .UsePostgres}}postgres_data:{{end}} 33 | {{if .UseRedis}}redis_data:{{end}} 34 | {{if .UseRabbitMQ}}rabbitmq_data:{{end}} -------------------------------------------------------------------------------- /templates/api/dockerfile.tpl: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.21-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy go mod and sum files 7 | COPY go.mod go.sum ./ 8 | 9 | # Download dependencies 10 | RUN go mod download 11 | 12 | # Copy the source code 13 | COPY . . 14 | 15 | # Build the application 16 | RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/main.go 17 | 18 | # Final stage 19 | FROM alpine:latest 20 | 21 | WORKDIR /app 22 | 23 | # Copy the binary from builder 24 | COPY --from=builder /app/main . 25 | 26 | # Copy any additional necessary files 27 | COPY .env . 28 | 29 | # Expose the port 30 | EXPOSE 8080 31 | 32 | # Run the application 33 | CMD ["./main"] -------------------------------------------------------------------------------- /templates/api/env.tpl: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | PORT=8080 3 | 4 | {{if .UsePostgres}}# Database Configuration 5 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/{{.ProjectName}}?sslmode=disable 6 | {{end}} 7 | 8 | {{if .UseRedis}}# Redis Configuration 9 | REDIS_URL=localhost:6379 10 | {{end}} 11 | 12 | {{if .UseRabbitMQ}}# RabbitMQ Configuration 13 | RABBITMQ_URL=amqp://guest:guest@localhost:5672/ 14 | {{end}} -------------------------------------------------------------------------------- /templates/api/gitignore.tpl: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | {{.ProjectName}} 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | 21 | # Environment variables 22 | .env 23 | 24 | # IDE specific files 25 | .idea/ 26 | .vscode/ 27 | *.swp 28 | *.swo 29 | 30 | # OS specific files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # Logs 40 | *.log 41 | logs/ 42 | 43 | # Docker volumes 44 | data/ 45 | postgres_data/ 46 | redis_data/ 47 | rabbitmq_data/ 48 | 49 | # Temporary files 50 | tmp/ 51 | temp/ -------------------------------------------------------------------------------- /templates/api/go-mod.tpl: -------------------------------------------------------------------------------- 1 | module {{.ModuleName}} 2 | 3 | go {{.GoVersion}} 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.9.1 7 | github.com/joho/godotenv v1.5.1 8 | {{if .UseZap}}go.uber.org/zap v1.27.0{{end}} 9 | {{if .UsePostgres}}github.com/lib/pq v1.10.9{{end}} 10 | {{if .UseRedis}}github.com/redis/go-redis/v9 v9.5.1{{end}} 11 | {{if .UseRabbitMQ}}github.com/rabbitmq/amqp091-go v1.9.0{{end}} 12 | ) -------------------------------------------------------------------------------- /templates/api/handlers.tpl: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // HealthHandler returns a 200 OK response if the service is healthy 11 | func HealthHandler(c *gin.Context) { 12 | c.JSON(http.StatusOK, gin.H{ 13 | "status": "ok", 14 | "service": "{{.ProjectName}}", 15 | "timestamp": time.Now().Format(time.RFC3339), 16 | }) 17 | } 18 | 19 | // PingHandler returns a simple pong response 20 | func PingHandler(c *gin.Context) { 21 | c.JSON(http.StatusOK, gin.H{ 22 | "message": "pong", 23 | }) 24 | } 25 | 26 | 27 | // NotFoundHandler handles 404 errors 28 | func NotFoundHandler(c *gin.Context) { 29 | c.JSON(http.StatusNotFound, gin.H{ 30 | "error": "Resource not found", 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /templates/api/logging.tpl: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "time" 5 | "go.uber.org/zap" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | var logger *zap.Logger 10 | 11 | func init() { 12 | var err error 13 | logger, err = zap.NewProduction() 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | func LoggingMiddleware() gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | start := time.Now() 22 | path := c.Request.URL.Path 23 | method := c.Request.Method 24 | 25 | c.Next() 26 | 27 | latency := time.Since(start) 28 | status := c.Writer.Status() 29 | 30 | logger.Info("request completed", 31 | zap.String("path", path), 32 | zap.String("method", method), 33 | zap.Int("status", status), 34 | zap.Duration("latency", latency), 35 | ) 36 | } 37 | } -------------------------------------------------------------------------------- /templates/api/main.tpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "{{.ModuleName}}/internal/server" 6 | "{{.ModuleName}}/internal/service" 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | func main() { 11 | // Load .env file 12 | if err := godotenv.Load(); err != nil { 13 | log.Printf("Warning: .env file not found") 14 | } 15 | 16 | // Initialize all services 17 | if err := service.InitServices(); err != nil { 18 | log.Fatalf("Failed to initialize services: %v", err) 19 | } 20 | defer service.CloseServices() 21 | 22 | // Create and start server 23 | srv := server.NewServer() 24 | if err := srv.Start(); err != nil { 25 | log.Fatalf("Failed to start server: %v", err) 26 | } 27 | } -------------------------------------------------------------------------------- /templates/api/middleware.tpl: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // Logger middleware logs HTTP requests 11 | func Logger() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | // Start time 14 | start := time.Now() 15 | path := c.Request.URL.Path 16 | 17 | // Process request 18 | c.Next() 19 | 20 | // End time 21 | end := time.Now() 22 | latency := end.Sub(start) 23 | 24 | // Log request 25 | fmt.Printf("[%s] %s %s %d %s\n", 26 | end.Format("2006-01-02 15:04:05"), 27 | c.Request.Method, 28 | path, 29 | c.Writer.Status(), 30 | latency, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/api/postgres.tpl: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | _ "github.com/lib/pq" 7 | ) 8 | 9 | var DB *sql.DB 10 | 11 | func InitPostgres() error { 12 | var err error 13 | DB, err = sql.Open("postgres", os.Getenv("DATABASE_URL")) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | if err := DB.Ping(); err != nil { 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | 25 | func ClosePostgres() { 26 | if DB != nil { 27 | DB.Close() 28 | } 29 | } -------------------------------------------------------------------------------- /templates/api/rabbitmq.tpl: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | amqp "github.com/rabbitmq/amqp091-go" 6 | ) 7 | 8 | var RabbitMQ *amqp.Connection 9 | 10 | func InitRabbitMQ() error { 11 | var err error 12 | RabbitMQ, err = amqp.Dial(os.Getenv("RABBITMQ_URL")) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | 20 | func CloseRabbitMQ() { 21 | if RabbitMQ != nil { 22 | RabbitMQ.Close() 23 | } 24 | } -------------------------------------------------------------------------------- /templates/api/redis.tpl: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/redis/go-redis/v9" 5 | "os" 6 | ) 7 | 8 | var RedisClient *redis.Client 9 | 10 | func InitRedis() error { 11 | RedisClient = redis.NewClient(&redis.Options{ 12 | Addr: os.Getenv("REDIS_URL"), 13 | }) 14 | 15 | return nil 16 | } 17 | 18 | func CloseRedis() { 19 | if RedisClient != nil { 20 | RedisClient.Close() 21 | } 22 | } -------------------------------------------------------------------------------- /templates/api/routes.tpl: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "{{.ModuleName}}/internal/handlers" 6 | {{if .UseZap}}"{{.ModuleName}}/internal/middleware"{{end}} 7 | ) 8 | 9 | // SetupRoutes configures all the routes for the application 10 | func SetupRoutes(router *gin.Engine) { 11 | {{if .UseZap}}// Add logging middleware 12 | router.Use(middleware.LoggingMiddleware()) 13 | {{end}} 14 | 15 | // API routes 16 | api := router.Group("/api") 17 | { 18 | api.GET("/ping", handlers.PingHandler) 19 | api.GET("/health", handlers.HealthHandler) 20 | } 21 | } -------------------------------------------------------------------------------- /templates/api/server.tpl: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "{{.ModuleName}}/internal/routes" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type Server struct { 11 | router *gin.Engine 12 | } 13 | 14 | func NewServer() *Server { 15 | return &Server{ 16 | router: gin.Default(), 17 | } 18 | } 19 | 20 | func (s *Server) Start() error { 21 | // Setup routes 22 | routes.SetupRoutes(s.router) 23 | 24 | // Get port from environment 25 | port := os.Getenv("PORT") 26 | if port == "" { 27 | port = "8080" 28 | } 29 | 30 | // Start server 31 | return s.router.Run(fmt.Sprintf(":%s", port)) 32 | } -------------------------------------------------------------------------------- /templates/api/service-init.tpl: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | func InitServices() error { 4 | {{if .UsePostgres}}// Initialize PostgreSQL 5 | if err := InitPostgres(); err != nil { 6 | return err 7 | } 8 | {{end}} 9 | 10 | {{if .UseRedis}}// Initialize Redis 11 | if err := InitRedis(); err != nil { 12 | return err 13 | } 14 | {{end}} 15 | 16 | {{if .UseRabbitMQ}}// Initialize RabbitMQ 17 | if err := InitRabbitMQ(); err != nil { 18 | return err 19 | } 20 | {{end}} 21 | 22 | return nil 23 | } 24 | 25 | func CloseServices() { 26 | {{if .UsePostgres}}ClosePostgres(){{end}} 27 | {{if .UseRedis}}CloseRedis(){{end}} 28 | {{if .UseRabbitMQ}}CloseRabbitMQ(){{end}} 29 | } -------------------------------------------------------------------------------- /templates/cli/command.tpl: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // {{.CommandName}}Cmd represents the {{.CommandName}} command 10 | var {{.CommandName}}Cmd = &cobra.Command{ 11 | Use: "{{.CommandName}}", 12 | Short: "A brief description of your command", 13 | Long: `A longer description that spans multiple lines and likely contains examples 14 | and usage of using your command. For example: 15 | 16 | Cobra is a CLI library for Go that empowers applications. 17 | This application is a tool to generate the needed files 18 | to quickly create a Cobra application.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Println("{{.CommandName}} called") 21 | }, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand({{.CommandName}}Cmd) 26 | 27 | // Here you will define your flags and configuration settings. 28 | 29 | // Cobra supports Persistent Flags which will work for this command 30 | // and all subcommands, e.g.: 31 | // {{.CommandName}}Cmd.PersistentFlags().String("foo", "", "A help for foo") 32 | 33 | // Cobra supports local flags which will only run when this command 34 | // is called directly, e.g.: 35 | // {{.CommandName}}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 36 | } -------------------------------------------------------------------------------- /templates/cli/commands.tpl: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // ExecuteCommand executes a command with the given arguments 11 | func ExecuteCommand(name string, args ...string) error { 12 | fmt.Printf("Executing command: %s %s\n", name, strings.Join(args, " ")) 13 | // In a real application, you would execute the command here 14 | time.Sleep(500 * time.Millisecond) // Simulate execution 15 | return nil 16 | } 17 | 18 | // PrintCommandError prints an error message for a command 19 | func PrintCommandError(command string, err error) { 20 | fmt.Fprintf(os.Stderr, "Error executing %s: %v\n", command, err) 21 | } 22 | 23 | // PrintCommandOutput prints the output of a command 24 | func PrintCommandOutput(command string, output string) { 25 | fmt.Printf("Output of %s:\n%s\n", command, output) 26 | } 27 | 28 | // ConfirmAction asks the user to confirm an action 29 | func ConfirmAction(action string) bool { 30 | fmt.Printf("Are you sure you want to %s? [y/N] ", action) 31 | var response string 32 | fmt.Scanln(&response) 33 | response = strings.ToLower(strings.TrimSpace(response)) 34 | return response == "y" || response == "yes" 35 | } -------------------------------------------------------------------------------- /templates/cli/config.tpl: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // Config holds the application configuration 12 | type Config struct { 13 | // General settings 14 | AppName string 15 | Version string 16 | LogLevel string 17 | ConfigFile string 18 | LastUpdateCheck time.Time 19 | 20 | // User settings 21 | Username string 22 | Email string 23 | 24 | // Path settings 25 | DataDir string 26 | LogDir string 27 | TemplateDir string 28 | } 29 | 30 | // NewConfig creates a new configuration instance with default values 31 | func NewConfig() *Config { 32 | return &Config{ 33 | AppName: "{{.ProjectName}}", 34 | Version: "0.1.0", 35 | LogLevel: "info", 36 | } 37 | } 38 | 39 | // LoadConfig loads the configuration from disk 40 | func LoadConfig(configFile string) (*Config, error) { 41 | config := NewConfig() 42 | 43 | if configFile != "" { 44 | viper.SetConfigFile(configFile) 45 | } else { 46 | // Set default config locations 47 | homeDir, err := os.UserHomeDir() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | viper.SetConfigName(".{{.ProjectName}}") 53 | viper.SetConfigType("yaml") 54 | viper.AddConfigPath(homeDir) 55 | viper.AddConfigPath(".") 56 | } 57 | 58 | // Set default values 59 | viper.SetDefault("AppName", config.AppName) 60 | viper.SetDefault("Version", config.Version) 61 | viper.SetDefault("LogLevel", config.LogLevel) 62 | 63 | // Try to read config file 64 | if err := viper.ReadInConfig(); err != nil { 65 | // It's okay if we don't find a config file 66 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 67 | return nil, err 68 | } 69 | } 70 | 71 | // Parse config into struct 72 | if err := viper.Unmarshal(config); err != nil { 73 | return nil, err 74 | } 75 | 76 | // Set some computed defaults if not specified 77 | if config.DataDir == "" { 78 | homeDir, err := os.UserHomeDir() 79 | if err != nil { 80 | return nil, err 81 | } 82 | config.DataDir = filepath.Join(homeDir, ".{{.ProjectName}}", "data") 83 | } 84 | 85 | if config.LogDir == "" { 86 | homeDir, err := os.UserHomeDir() 87 | if err != nil { 88 | return nil, err 89 | } 90 | config.LogDir = filepath.Join(homeDir, ".{{.ProjectName}}", "logs") 91 | } 92 | 93 | return config, nil 94 | } 95 | 96 | // SaveConfig saves the current configuration to disk 97 | func (c *Config) SaveConfig() error { 98 | viper.Set("AppName", c.AppName) 99 | viper.Set("Version", c.Version) 100 | viper.Set("LogLevel", c.LogLevel) 101 | viper.Set("LastUpdateCheck", c.LastUpdateCheck) 102 | viper.Set("Username", c.Username) 103 | viper.Set("Email", c.Email) 104 | viper.Set("DataDir", c.DataDir) 105 | viper.Set("LogDir", c.LogDir) 106 | viper.Set("TemplateDir", c.TemplateDir) 107 | 108 | return viper.WriteConfig() 109 | } -------------------------------------------------------------------------------- /templates/cli/gitignore.tpl: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | {{.ProjectName}} 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | 21 | # IDE specific files 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.swo 26 | 27 | # OS specific files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | # Logs 37 | *.log 38 | logs/ 39 | 40 | # Config files 41 | config.yaml 42 | config.yml 43 | .env 44 | 45 | # Build directory 46 | build/ 47 | dist/ 48 | 49 | # Temporary files 50 | tmp/ 51 | temp/ -------------------------------------------------------------------------------- /templates/cli/go-mod.tpl: -------------------------------------------------------------------------------- 1 | module {{.ModuleName}} 2 | 3 | go {{.GoVersion}} 4 | 5 | require ( 6 | github.com/spf13/cobra v1.8.0 7 | github.com/spf13/viper v1.18.1 8 | ) -------------------------------------------------------------------------------- /templates/cli/main.tpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "{{.ModuleName}}/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } -------------------------------------------------------------------------------- /templates/cli/readme.tpl: -------------------------------------------------------------------------------- 1 | # {{.ProjectName}} 2 | 3 | {{.ProjectDescription}} 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get -u github.com/yourusername/{{.ProjectName}} 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```bash 14 | {{.ProjectName}} [command] 15 | ``` 16 | 17 | ### Available Commands: 18 | 19 | * command1: Description of command1 20 | * command2: Description of command2 21 | * help: Help about any command 22 | 23 | ### Flags: 24 | 25 | * --config string: config file (default is $HOME/.{{.ProjectName}}.yaml) 26 | * -h, --help: help for {{.ProjectName}} 27 | * -t, --toggle: Help message for toggle 28 | 29 | Use "{{.ProjectName}} [command] --help" for more information about a command. 30 | 31 | ## Development 32 | 33 | 1. Clone the repository 34 | 2. Install dependencies with `go mod tidy` 35 | 3. Run with `go run main.go` 36 | 37 | ## Building 38 | 39 | Build a binary with: 40 | 41 | ```bash 42 | go build -o {{.ProjectName}} 43 | ``` 44 | 45 | ## License 46 | 47 | This project is licensed under the {{.License}} License - see the LICENSE file for details. -------------------------------------------------------------------------------- /templates/cli/root.tpl: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var cfgFile string 12 | 13 | // rootCmd represents the base command when called without any subcommands 14 | var rootCmd = &cobra.Command{ 15 | Use: "{{.ProjectName}}", 16 | Short: "A brief description of your application", 17 | Long: `A longer description that spans multiple lines and likely contains 18 | examples and usage of using your application. For example: 19 | 20 | Cobra is a CLI library for Go that empowers applications. 21 | This application is a tool to generate the needed files 22 | to quickly create a Cobra application.`, 23 | // Uncomment the following line if your bare application 24 | // has an action associated with it: 25 | // Run: func(cmd *cobra.Command, args []string) { }, 26 | } 27 | 28 | // Execute adds all child commands to the root command and sets flags appropriately. 29 | // This is called by main.main(). It only needs to happen once to the rootCmd. 30 | func Execute() { 31 | if err := rootCmd.Execute(); err != nil { 32 | fmt.Println(err) 33 | os.Exit(1) 34 | } 35 | } 36 | 37 | func init() { 38 | cobra.OnInitialize(initConfig) 39 | 40 | // Here you will define your flags and configuration settings. 41 | // Cobra supports persistent flags, which, if defined here, 42 | // will be global for your application. 43 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{.ProjectName}}.yaml)") 44 | 45 | // Cobra also supports local flags, which will only run 46 | // when this action is called directly. 47 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 48 | } 49 | 50 | // initConfig reads in config file and ENV variables if set. 51 | func initConfig() { 52 | if cfgFile != "" { 53 | // Use config file from the flag. 54 | viper.SetConfigFile(cfgFile) 55 | } else { 56 | // Find home directory. 57 | home, err := os.UserHomeDir() 58 | if err != nil { 59 | fmt.Println(err) 60 | os.Exit(1) 61 | } 62 | 63 | // Search config in home directory with name ".{{.ProjectName}}" (without extension). 64 | viper.AddConfigPath(home) 65 | viper.SetConfigName(".{{.ProjectName}}") 66 | } 67 | 68 | viper.AutomaticEnv() // read in environment variables that match 69 | 70 | // If a config file is found, read it in. 71 | if err := viper.ReadInConfig(); err == nil { 72 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 73 | } 74 | } -------------------------------------------------------------------------------- /templates/cli/utils.tpl: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Constants for terminal colors 9 | const ( 10 | ColorReset = "\033[0m" 11 | ColorRed = "\033[31m" 12 | ColorGreen = "\033[32m" 13 | ColorYellow = "\033[33m" 14 | ColorBlue = "\033[34m" 15 | ColorPurple = "\033[35m" 16 | ColorCyan = "\033[36m" 17 | ) 18 | 19 | // PrintInfo prints an info message to the console 20 | func PrintInfo(format string, a ...interface{}) { 21 | fmt.Printf(ColorBlue+"INFO: "+format+ColorReset+"\n", a...) 22 | } 23 | 24 | // PrintSuccess prints a success message to the console 25 | func PrintSuccess(format string, a ...interface{}) { 26 | fmt.Printf(ColorGreen+"SUCCESS: "+format+ColorReset+"\n", a...) 27 | } 28 | 29 | // PrintWarning prints a warning message to the console 30 | func PrintWarning(format string, a ...interface{}) { 31 | fmt.Printf(ColorYellow+"WARNING: "+format+ColorReset+"\n", a...) 32 | } 33 | 34 | // PrintError prints an error message to the console 35 | func PrintError(format string, a ...interface{}) { 36 | fmt.Printf(ColorRed+"ERROR: "+format+ColorReset+"\n", a...) 37 | } 38 | 39 | // StringInSlice checks if a string is in a slice 40 | func StringInSlice(a string, list []string) bool { 41 | for _, b := range list { 42 | if b == a { 43 | return true 44 | } 45 | } 46 | return false 47 | } -------------------------------------------------------------------------------- /templates/cli/version.tpl: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | // Version is the version of the application 11 | Version = "0.1.0" 12 | // BuildDate is the date when the application was built 13 | BuildDate = "unknown" 14 | // GitCommit is the git commit hash 15 | GitCommit = "unknown" 16 | ) 17 | 18 | // versionCmd represents the version command 19 | var versionCmd = &cobra.Command{ 20 | Use: "version", 21 | Short: "Print the version information", 22 | Long: `Print the version, build date, and git commit hash of the application.`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | fmt.Printf("{{.ProjectName}} version %s\n", Version) 25 | fmt.Printf("Built on %s\n", BuildDate) 26 | fmt.Printf("Git commit: %s\n", GitCommit) 27 | }, 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(versionCmd) 32 | } -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "text/template" 10 | 11 | "github.com/go-sova/sova-cli/pkg/utils" 12 | ) 13 | 14 | //go:embed cli/* api/* 15 | var TemplateFS embed.FS 16 | 17 | // TemplateLoader handles loading templates from the embedded filesystem 18 | type TemplateLoader struct { 19 | fs fs.FS 20 | logger *utils.Logger 21 | } 22 | 23 | // NewTemplateLoader creates a new template loader 24 | func NewTemplateLoader() *TemplateLoader { 25 | return &TemplateLoader{ 26 | fs: TemplateFS, 27 | logger: utils.NewLoggerWithPrefix(utils.Info, "TemplateLoader"), 28 | } 29 | } 30 | 31 | func (l *TemplateLoader) SetLogger(logger *utils.Logger) { 32 | l.logger = logger 33 | } 34 | 35 | // LoadTemplate loads a template by name from the embedded filesystem 36 | func (l *TemplateLoader) LoadTemplate(name string) (*template.Template, error) { 37 | // If the template name already includes a category prefix (e.g. "api/env.tpl"), 38 | // try loading it directly 39 | content, err := fs.ReadFile(l.fs, name) 40 | if err == nil { 41 | tmpl, err := template.New(filepath.Base(name)).Parse(string(content)) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to parse template %s: %w", name, err) 44 | } 45 | return tmpl, nil 46 | } 47 | 48 | // If direct loading fails, try each category as a fallback 49 | categories := []string{"cli", "api"} 50 | for _, category := range categories { 51 | if tmpl, err := l.LoadTemplateFromCategory(category, name); err == nil { 52 | return tmpl, nil 53 | } 54 | } 55 | 56 | return nil, fmt.Errorf("template not found: %s", name) 57 | } 58 | 59 | // LoadTemplateFromCategory loads a template from a specific category 60 | func (l *TemplateLoader) LoadTemplateFromCategory(category, name string) (*template.Template, error) { 61 | templatePath := filepath.Join(category, name) 62 | content, err := fs.ReadFile(l.fs, templatePath) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to read template %s: %w", templatePath, err) 65 | } 66 | 67 | tmpl, err := template.New(filepath.Base(name)).Parse(string(content)) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to parse template %s: %w", templatePath, err) 70 | } 71 | 72 | return tmpl, nil 73 | } 74 | 75 | // FileGenerator handles generating files from templates 76 | type FileGenerator struct { 77 | loader *TemplateLoader 78 | logger *utils.Logger 79 | } 80 | 81 | // NewFileGenerator creates a new file generator 82 | func NewFileGenerator(loader *TemplateLoader) *FileGenerator { 83 | return &FileGenerator{ 84 | loader: loader, 85 | logger: utils.NewLoggerWithPrefix(utils.Info, "FileGenerator"), 86 | } 87 | } 88 | 89 | func (g *FileGenerator) SetLogger(logger *utils.Logger) { 90 | g.logger = logger 91 | } 92 | 93 | // GenerateFile generates a file from a template 94 | func (g *FileGenerator) GenerateFile(templateName, outputPath string, data interface{}) error { 95 | g.logger.Debug("Generating file %s from template %s", outputPath, templateName) 96 | 97 | // Create the directory if it doesn't exist 98 | dir := filepath.Dir(outputPath) 99 | if err := os.MkdirAll(dir, 0755); err != nil { 100 | return fmt.Errorf("failed to create directory %s: %w", dir, err) 101 | } 102 | 103 | // Load the template 104 | tmpl, err := g.loader.LoadTemplate(templateName) 105 | if err != nil { 106 | return fmt.Errorf("failed to load template %s: %w", templateName, err) 107 | } 108 | 109 | // Create the output file 110 | file, err := os.Create(outputPath) 111 | if err != nil { 112 | return fmt.Errorf("failed to create file %s: %w", outputPath, err) 113 | } 114 | defer file.Close() 115 | 116 | // Execute the template 117 | if err := tmpl.Execute(file, data); err != nil { 118 | return fmt.Errorf("failed to execute template %s: %w", templateName, err) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // GetTemplateFS returns the embedded filesystem containing all templates 125 | func GetTemplateFS() fs.FS { 126 | return TemplateFS 127 | } 128 | 129 | // GetTemplatePath returns the path to a specific template within the embedded filesystem 130 | func GetTemplatePath(category, name string) string { 131 | return filepath.Join(category, name) 132 | } -------------------------------------------------------------------------------- /tests/commands_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestCLICommands(t *testing.T) { 12 | tempDir, err := os.MkdirTemp("", "sova-cli-test-*") 13 | if err != nil { 14 | t.Fatalf("Failed to create temp directory: %v", err) 15 | } 16 | defer os.RemoveAll(tempDir) 17 | 18 | testCases := []struct { 19 | name string 20 | args []string 21 | expectedOut string 22 | expectedError bool 23 | }{ 24 | { 25 | name: "Version command", 26 | args: []string{"version"}, 27 | expectedOut: "Sova CLI version", 28 | expectedError: false, 29 | }, 30 | { 31 | name: "Help command", 32 | args: []string{"help"}, 33 | expectedOut: "Available Commands:", 34 | expectedError: false, 35 | }, 36 | { 37 | name: "Init command with project name", 38 | args: []string{"init", "test-project"}, 39 | expectedOut: "Project initialized successfully", 40 | expectedError: false, 41 | }, 42 | { 43 | name: "Invalid command", 44 | args: []string{"invalid-command"}, 45 | expectedOut: "", 46 | expectedError: true, 47 | }, 48 | } 49 | 50 | for _, tc := range testCases { 51 | t.Run(tc.name, func(t *testing.T) { 52 | cmd := exec.Command("go", append([]string{"run", "../main.go"}, tc.args...)...) 53 | cmd.Dir = tempDir 54 | 55 | var stdout, stderr bytes.Buffer 56 | cmd.Stdout = &stdout 57 | cmd.Stderr = &stderr 58 | 59 | err := cmd.Run() 60 | output := stdout.String() + stderr.String() 61 | 62 | if tc.expectedError && err == nil { 63 | t.Errorf("Expected error but got none") 64 | } 65 | if !tc.expectedError && err != nil { 66 | t.Errorf("Unexpected error: %v", err) 67 | } 68 | if !strings.Contains(output, tc.expectedOut) { 69 | t.Errorf("Expected output containing %q, got %q", tc.expectedOut, output) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestCLIFlags(t *testing.T) { 76 | tempDir, err := os.MkdirTemp("", "sova-cli-flags-test-*") 77 | if err != nil { 78 | t.Fatalf("Failed to create temp directory: %v", err) 79 | } 80 | defer os.RemoveAll(tempDir) 81 | 82 | testCases := []struct { 83 | name string 84 | args []string 85 | flags []string 86 | expectedOut string 87 | expectedError bool 88 | }{ 89 | { 90 | name: "Init with template flag", 91 | args: []string{"init", "test-project"}, 92 | flags: []string{"--template", "default"}, 93 | expectedOut: "Project initialized successfully", 94 | expectedError: false, 95 | }, 96 | { 97 | name: "Init with invalid template", 98 | args: []string{"init", "test-project"}, 99 | flags: []string{"--template", "nonexistent"}, 100 | expectedOut: "", 101 | expectedError: true, 102 | }, 103 | { 104 | name: "Version with json flag", 105 | args: []string{"version"}, 106 | flags: []string{"--json"}, 107 | expectedOut: "{", 108 | expectedError: false, 109 | }, 110 | } 111 | 112 | for _, tc := range testCases { 113 | t.Run(tc.name, func(t *testing.T) { 114 | cmdArgs := append([]string{"run", "../main.go"}, tc.args...) 115 | cmdArgs = append(cmdArgs, tc.flags...) 116 | cmd := exec.Command("go", cmdArgs...) 117 | cmd.Dir = tempDir 118 | 119 | var stdout, stderr bytes.Buffer 120 | cmd.Stdout = &stdout 121 | cmd.Stderr = &stderr 122 | 123 | err := cmd.Run() 124 | output := stdout.String() + stderr.String() 125 | 126 | if tc.expectedError && err == nil { 127 | t.Errorf("Expected error but got none") 128 | } 129 | if !tc.expectedError && err != nil { 130 | t.Errorf("Unexpected error: %v", err) 131 | } 132 | if !strings.Contains(output, tc.expectedOut) { 133 | t.Errorf("Expected output containing %q, got %q", tc.expectedOut, output) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/init_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestProjectInitialization(t *testing.T) { 11 | tempDir, err := os.MkdirTemp("", "sova-test-*") 12 | if err != nil { 13 | t.Fatalf("Failed to create temp directory: %v", err) 14 | } 15 | defer os.RemoveAll(tempDir) 16 | 17 | testCases := []struct { 18 | name string 19 | projectName string 20 | template string 21 | wantDirs []string 22 | wantFiles []string 23 | wantErr bool 24 | }{ 25 | { 26 | name: "Basic project creation", 27 | projectName: "test-project", 28 | template: "default", 29 | wantDirs: []string{ 30 | "cmd", 31 | "internal", 32 | "pkg", 33 | "api", 34 | "docs", 35 | "scripts", 36 | "test", 37 | }, 38 | wantFiles: []string{ 39 | "main.go", 40 | "go.mod", 41 | "README.md", 42 | }, 43 | wantErr: false, 44 | }, 45 | { 46 | name: "Web project creation", 47 | projectName: "web-project", 48 | template: "go-web", 49 | wantDirs: []string{ 50 | "cmd", 51 | "internal", 52 | "pkg", 53 | "api", 54 | "docs", 55 | "scripts", 56 | "test", 57 | "web", 58 | "templates", 59 | "static", 60 | }, 61 | wantFiles: []string{ 62 | "main.go", 63 | "go.mod", 64 | "README.md", 65 | "web/handlers.go", 66 | "web/middleware.go", 67 | "web/routes.go", 68 | }, 69 | wantErr: false, 70 | }, 71 | { 72 | name: "CLI project creation", 73 | projectName: "cli-project", 74 | template: "cli", 75 | wantDirs: []string{ 76 | "cmd", 77 | "internal", 78 | "pkg", 79 | "docs", 80 | }, 81 | wantFiles: []string{ 82 | "main.go", 83 | "go.mod", 84 | "README.md", 85 | "cmd/root.go", 86 | "cmd/version.go", 87 | }, 88 | wantErr: false, 89 | }, 90 | { 91 | name: "Invalid template", 92 | projectName: "invalid-project", 93 | template: "nonexistent", 94 | wantDirs: []string{}, 95 | wantFiles: []string{}, 96 | wantErr: true, 97 | }, 98 | } 99 | 100 | for _, tc := range testCases { 101 | t.Run(tc.name, func(t *testing.T) { 102 | projectDir := filepath.Join(tempDir, tc.projectName) 103 | cmd := exec.Command("go", "run", "../main.go", "init", tc.projectName, "--template", tc.template) 104 | cmd.Dir = tempDir 105 | output, err := cmd.CombinedOutput() 106 | 107 | if tc.wantErr { 108 | if err == nil { 109 | t.Errorf("Expected error but got none. Output: %s", string(output)) 110 | } 111 | return 112 | } else if err != nil { 113 | t.Fatalf("Failed to run command: %v\nOutput: %s", err, string(output)) 114 | } 115 | 116 | for _, dir := range tc.wantDirs { 117 | dirPath := filepath.Join(projectDir, dir) 118 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 119 | t.Errorf("Expected directory %s does not exist", dir) 120 | } 121 | } 122 | 123 | for _, file := range tc.wantFiles { 124 | filePath := filepath.Join(projectDir, file) 125 | info, err := os.Stat(filePath) 126 | if os.IsNotExist(err) { 127 | t.Errorf("Expected file %s does not exist", file) 128 | continue 129 | } 130 | if info.Size() == 0 { 131 | t.Errorf("File %s exists but is empty", file) 132 | } 133 | } 134 | 135 | if !tc.wantErr { 136 | buildCmd := exec.Command("go", "build", "./...") 137 | buildCmd.Dir = projectDir 138 | if output, err := buildCmd.CombinedOutput(); err != nil { 139 | t.Errorf("Project failed to build: %v\nOutput: %s", err, string(output)) 140 | } 141 | } 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/template_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestTemplateSystem(t *testing.T) { 10 | tempDir, err := os.MkdirTemp("", "sova-template-test-*") 11 | if err != nil { 12 | t.Fatalf("Failed to create temp directory: %v", err) 13 | } 14 | defer os.RemoveAll(tempDir) 15 | 16 | testCases := []struct { 17 | name string 18 | templateName string 19 | projectName string 20 | files map[string]string 21 | wantErr bool 22 | }{ 23 | { 24 | name: "Default template", 25 | templateName: "default", 26 | projectName: "test-default", 27 | files: map[string]string{ 28 | "main.go": "package main", 29 | "go.mod": "module test-default", 30 | "README.md": "# Test Default Project", 31 | "cmd/root.go": `package cmd 32 | func Execute() error { 33 | return nil 34 | }`, 35 | }, 36 | wantErr: false, 37 | }, 38 | { 39 | name: "Web template", 40 | templateName: "web", 41 | projectName: "test-web", 42 | files: map[string]string{ 43 | "main.go": "package main", 44 | "go.mod": "module test-web", 45 | "web/server.go": `package web 46 | func StartServer() error { 47 | return nil 48 | }`, 49 | "templates/index.html": "Hello", 50 | }, 51 | wantErr: false, 52 | }, 53 | { 54 | name: "Invalid template", 55 | templateName: "nonexistent", 56 | projectName: "test-invalid", 57 | files: map[string]string{}, 58 | wantErr: true, 59 | }, 60 | } 61 | 62 | for _, tc := range testCases { 63 | t.Run(tc.name, func(t *testing.T) { 64 | projectDir := filepath.Join(tempDir, tc.projectName) 65 | err := os.MkdirAll(projectDir, 0755) 66 | if err != nil { 67 | t.Fatalf("Failed to create project directory: %v", err) 68 | } 69 | 70 | for filePath, content := range tc.files { 71 | fullPath := filepath.Join(projectDir, filePath) 72 | dir := filepath.Dir(fullPath) 73 | if err := os.MkdirAll(dir, 0755); err != nil { 74 | t.Fatalf("Failed to create directory %s: %v", dir, err) 75 | } 76 | 77 | if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { 78 | t.Fatalf("Failed to write file %s: %v", filePath, err) 79 | } 80 | } 81 | 82 | if !tc.wantErr { 83 | for filePath, expectedContent := range tc.files { 84 | fullPath := filepath.Join(projectDir, filePath) 85 | content, err := os.ReadFile(fullPath) 86 | if err != nil { 87 | t.Errorf("Failed to read file %s: %v", filePath, err) 88 | continue 89 | } 90 | 91 | if string(content) != expectedContent { 92 | t.Errorf("File %s content mismatch\nwant: %s\ngot: %s", filePath, expectedContent, string(content)) 93 | } 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | 100 | func TestTemplateValidation(t *testing.T) { 101 | testCases := []struct { 102 | name string 103 | template string 104 | isValid bool 105 | }{ 106 | { 107 | name: "Valid template structure", 108 | template: "default", 109 | isValid: true, 110 | }, 111 | { 112 | name: "Invalid template name", 113 | template: "invalid-template-name", 114 | isValid: false, 115 | }, 116 | { 117 | name: "Empty template name", 118 | template: "", 119 | isValid: false, 120 | }, 121 | } 122 | 123 | for _, tc := range testCases { 124 | t.Run(tc.name, func(t *testing.T) { 125 | err := validateTemplate(tc.template) 126 | if tc.isValid && err != nil { 127 | t.Errorf("Expected valid template but got error: %v", err) 128 | } 129 | if !tc.isValid && err == nil { 130 | t.Error("Expected invalid template but got no error") 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func validateTemplate(name string) error { 137 | if name == "" { 138 | return os.ErrInvalid 139 | } 140 | if name == "default" { 141 | return nil 142 | } 143 | return os.ErrNotExist 144 | } 145 | -------------------------------------------------------------------------------- /tests/utils_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestFileOperations(t *testing.T) { 10 | tempDir, err := os.MkdirTemp("", "sova-utils-test-*") 11 | if err != nil { 12 | t.Fatalf("Failed to create temp directory: %v", err) 13 | } 14 | defer os.RemoveAll(tempDir) 15 | 16 | t.Run("Directory operations", func(t *testing.T) { 17 | dirPath := filepath.Join(tempDir, "test-dir") 18 | nestedDirPath := filepath.Join(dirPath, "nested") 19 | 20 | err := os.MkdirAll(nestedDirPath, 0755) 21 | if err != nil { 22 | t.Fatalf("Failed to create nested directory: %v", err) 23 | } 24 | 25 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 26 | t.Error("Directory was not created") 27 | } 28 | 29 | if _, err := os.Stat(nestedDirPath); os.IsNotExist(err) { 30 | t.Error("Nested directory was not created") 31 | } 32 | }) 33 | 34 | t.Run("File operations", func(t *testing.T) { 35 | filePath := filepath.Join(tempDir, "test.txt") 36 | content := []byte("Hello, World!") 37 | 38 | err := os.WriteFile(filePath, content, 0644) 39 | if err != nil { 40 | t.Fatalf("Failed to write file: %v", err) 41 | } 42 | 43 | readContent, err := os.ReadFile(filePath) 44 | if err != nil { 45 | t.Fatalf("Failed to read file: %v", err) 46 | } 47 | 48 | if string(readContent) != string(content) { 49 | t.Errorf("File content mismatch. Want %q, got %q", content, readContent) 50 | } 51 | }) 52 | 53 | t.Run("File permissions", func(t *testing.T) { 54 | filePath := filepath.Join(tempDir, "permissions.txt") 55 | content := []byte("Test permissions") 56 | 57 | err := os.WriteFile(filePath, content, 0600) 58 | if err != nil { 59 | t.Fatalf("Failed to create file with permissions: %v", err) 60 | } 61 | 62 | info, err := os.Stat(filePath) 63 | if err != nil { 64 | t.Fatalf("Failed to get file info: %v", err) 65 | } 66 | 67 | if info.Mode().Perm() != 0600 { 68 | t.Errorf("File permissions mismatch. Want %o, got %o", 0600, info.Mode().Perm()) 69 | } 70 | }) 71 | 72 | t.Run("File deletion", func(t *testing.T) { 73 | filePath := filepath.Join(tempDir, "to-delete.txt") 74 | content := []byte("Delete me") 75 | 76 | err := os.WriteFile(filePath, content, 0644) 77 | if err != nil { 78 | t.Fatalf("Failed to create file: %v", err) 79 | } 80 | 81 | err = os.Remove(filePath) 82 | if err != nil { 83 | t.Fatalf("Failed to delete file: %v", err) 84 | } 85 | 86 | if _, err := os.Stat(filePath); !os.IsNotExist(err) { 87 | t.Error("File was not deleted") 88 | } 89 | }) 90 | } 91 | 92 | func TestPathOperations(t *testing.T) { 93 | testCases := []struct { 94 | name string 95 | path string 96 | wantDir string 97 | wantBase string 98 | wantExtension string 99 | }{ 100 | { 101 | name: "Simple file", 102 | path: "file.txt", 103 | wantDir: ".", 104 | wantBase: "file", 105 | wantExtension: ".txt", 106 | }, 107 | { 108 | name: "Nested file", 109 | path: "dir/subdir/file.go", 110 | wantDir: "dir/subdir", 111 | wantBase: "file", 112 | wantExtension: ".go", 113 | }, 114 | { 115 | name: "Hidden file", 116 | path: ".config", 117 | wantDir: ".", 118 | wantBase: ".config", 119 | wantExtension: "", 120 | }, 121 | { 122 | name: "Multiple extensions", 123 | path: "archive.tar.gz", 124 | wantDir: ".", 125 | wantBase: "archive.tar", 126 | wantExtension: ".gz", 127 | }, 128 | } 129 | 130 | for _, tc := range testCases { 131 | t.Run(tc.name, func(t *testing.T) { 132 | dir := filepath.Dir(tc.path) 133 | if dir != tc.wantDir { 134 | t.Errorf("Directory mismatch for %s. Want %s, got %s", tc.path, tc.wantDir, dir) 135 | } 136 | 137 | base := filepath.Base(tc.path) 138 | ext := filepath.Ext(tc.path) 139 | baseWithoutExt := base[:len(base)-len(ext)] 140 | 141 | if baseWithoutExt != tc.wantBase { 142 | t.Errorf("Base name mismatch for %s. Want %s, got %s", tc.path, tc.wantBase, baseWithoutExt) 143 | } 144 | 145 | if ext != tc.wantExtension { 146 | t.Errorf("Extension mismatch for %s. Want %s, got %s", tc.path, tc.wantExtension, ext) 147 | } 148 | }) 149 | } 150 | } 151 | --------------------------------------------------------------------------------