diff --git a/.gitignore b/.gitignore index 9e3852bc..70226312 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ codacy-cli #Macos .DS_Store + + +#Ignore vscode AI rules +.github/copilot-instructions.md diff --git a/cmd/analyze.go b/cmd/analyze.go index 7a5d611d..716e438f 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -15,6 +15,7 @@ import ( "path/filepath" "strings" + "codacy/cli-v2/cmd/configsetup" "codacy/cli-v2/utils" "github.com/spf13/cobra" @@ -264,10 +265,6 @@ func loadsToolAndPatterns(toolName string) (Tool, []Pattern) { break } } - // TO DO - PANIC - //if tool == nil { - // return nil, nil - //} var patterns []Pattern var hasNext bool = true cursor := "" @@ -390,10 +387,6 @@ func runLizardAnalysis(workDirectory string, pathsToCheck []string, outputFile s var patterns []domain.PatternDefinition var err error - //this logic is here because I want to pass the config to the runner which is good for: - //Separation of concerns, runner will simply run the tool now - //Easier testing, since config is now passed - //Avoiding fetching the default patterns in tests (unless we want to maintain codacy directory with the configs) if exists { // Configuration exists, read from file patterns, err = lizard.ReadConfig(configFile) @@ -402,7 +395,7 @@ func runLizardAnalysis(workDirectory string, pathsToCheck []string, outputFile s } } else { fmt.Println("No configuration file found for Lizard, using default patterns, run init with repository token to get a custom configuration") - patterns, err = tools.FetchDefaultEnabledPatterns(Lizard) + patterns, err = tools.FetchDefaultEnabledPatterns(configsetup.Lizard) if err != nil { return fmt.Errorf("failed to fetch default patterns: %v", err) } diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 00000000..58dfa199 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,155 @@ +package cmd + +import ( + "fmt" + "log" + "os" + + "codacy/cli-v2/cmd/configsetup" + "codacy/cli-v2/config" + "codacy/cli-v2/domain" + "codacy/cli-v2/utils" + + "github.com/spf13/cobra" + // Added import for YAML parsing +) + +// configResetInitFlags holds the flags for the config reset command. +var configResetInitFlags domain.InitFlags + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Manage Codacy configuration", +} + +// cliConfigYaml defines the structure for parsing .codacy/cli-config.yaml +type cliConfigYaml struct { + Mode string `yaml:"mode"` +} + +var configResetCmd = &cobra.Command{ + Use: "reset", + Short: "Reset Codacy configuration to default or repository-specific settings", + Long: "Resets the Codacy configuration files and tool-specific configurations. " + + "This command will overwrite an existing configuration with local default configurations " + + "if no API token is provided (and current mode is not 'remote'). If an API token is provided, it will fetch and apply " + + "repository-specific configurations from the Codacy API, effectively resetting to those.", + Run: func(cmd *cobra.Command, args []string) { + // Get current CLI mode from config + currentCliMode, err := config.Config.GetCliMode() + if err != nil { + // Log the error for debugging purposes + log.Printf("Warning: Could not determine CLI mode from cli-config.yaml: %v. Defaulting to 'local' mode.", err) + // Show a user-facing warning on stdout + fmt.Println("⚠️ Warning: Could not read or parse .codacy/cli-config.yaml. Defaulting to 'local' CLI mode.") + fmt.Println(" You might want to run 'codacy-cli init' or 'codacy-cli config reset --api-token ...' to correctly set up your configuration.") + fmt.Println() + currentCliMode = "local" // Default to local as per existing logic + } + + apiTokenFlagProvided := len(configResetInitFlags.ApiToken) > 0 + + // If current mode is 'remote', prevent resetting to local without explicit API token for a remote reset. + if currentCliMode == "remote" && !apiTokenFlagProvided { + fmt.Println("Error: Your Codacy CLI is currently configured in 'remote' (cloud) mode.") + fmt.Println("To reset your configuration using remote settings, you must provide the --api-token, --provider, --organization, and --repository flags.") + fmt.Println("Running 'config reset' without these flags is not permitted while configured for 'remote' mode.") + fmt.Println("This prevents an accidental switch to a local default configuration.") + fmt.Println() + if errHelp := cmd.Help(); errHelp != nil { + log.Printf("Warning: Failed to display command help: %v\n", errHelp) + } + os.Exit(1) + } + + // Validate flags: if API token is provided, other related flags must also be provided. + if apiTokenFlagProvided { + if configResetInitFlags.Provider == "" || configResetInitFlags.Organization == "" || configResetInitFlags.Repository == "" { + fmt.Println("Error: When using --api-token, you must also provide --provider, --organization, and --repository flags.") + fmt.Println("Please provide all required flags and try again.") + fmt.Println() + if errHelp := cmd.Help(); errHelp != nil { + log.Fatalf("Failed to display command help: %v", errHelp) + } + os.Exit(1) + } + } + + codacyConfigFile := config.Config.ProjectConfigFile() + // Check if the main configuration file exists + if _, err := os.Stat(codacyConfigFile); os.IsNotExist(err) { + fmt.Println("Configuration file (.codacy/codacy.yaml) not found, running initialization logic...") + runConfigResetLogic(cmd, args, configResetInitFlags) + } else { + fmt.Println("Resetting existing Codacy configuration...") + runConfigResetLogic(cmd, args, configResetInitFlags) + } + }, +} + +// runConfigResetLogic contains the core logic for resetting or initializing the configuration. +// It mirrors the behavior of the original init command but uses shared functions from the configsetup package. +func runConfigResetLogic(cmd *cobra.Command, args []string, flags domain.InitFlags) { + // Create local .codacy directory first + if err := config.Config.CreateLocalCodacyDir(); err != nil { + log.Fatalf("Failed to create local codacy directory: %v", err) + } + + // Create .codacy/tools-configs directory + toolsConfigDir := config.Config.ToolsConfigDirectory() + if err := os.MkdirAll(toolsConfigDir, utils.DefaultDirPerms); err != nil { + log.Fatalf("Failed to create tools-configs directory: %v", err) + } + + // Determine if running in local mode (no API token) + cliLocalMode := len(flags.ApiToken) == 0 + + if cliLocalMode { + fmt.Println() + fmt.Println("ℹ️ Resetting to local default configurations.") + noTools := []domain.Tool{} // Empty slice for tools as we are in local mode without specific toolset from API initially + if err := configsetup.CreateConfigurationFiles(noTools, cliLocalMode); err != nil { + log.Fatalf("Failed to create base configuration files: %v", err) + } + // Create default configuration files for tools + if err := configsetup.BuildDefaultConfigurationFiles(toolsConfigDir, flags); err != nil { + log.Fatalf("Failed to build default tool configuration files: %v", err) + } + // Create the languages configuration file for local mode + if err := configsetup.CreateLanguagesConfigFileLocal(toolsConfigDir); err != nil { + log.Fatalf("Failed to create local languages configuration file: %v", err) + } + } else { + // API token provided, fetch configuration from Codacy + fmt.Println("API token specified. Fetching and applying repository-specific configurations from Codacy...") + if err := configsetup.BuildRepositoryConfigurationFiles(flags); err != nil { + log.Fatalf("Failed to build repository-specific configuration files: %v", err) + } + } + + // Create or update .gitignore file in .codacy directory + if err := configsetup.CreateGitIgnoreFile(); err != nil { + log.Printf("Warning: Failed to create or update .codacy/.gitignore: %v", err) // Log as warning, not fatal + } + + fmt.Println() + fmt.Println("✅ Successfully reset Codacy configuration!") + fmt.Println() + fmt.Println("🔧 Next steps:") + fmt.Println(" 1. Run 'codacy-cli install' to install all dependencies based on the new/updated configuration.") + fmt.Println(" 2. Run 'codacy-cli analyze' to start analyzing your code.") + fmt.Println() +} + +func init() { + // Define flags for the config reset command. These are the same flags used by the init command. + configResetCmd.Flags().StringVar(&configResetInitFlags.ApiToken, "api-token", "", "Optional Codacy API token. If defined, configurations will be fetched from Codacy.") + configResetCmd.Flags().StringVar(&configResetInitFlags.Provider, "provider", "", "Provider (e.g., gh, bb, gl) to fetch configurations from Codacy. Required when api-token is provided.") + configResetCmd.Flags().StringVar(&configResetInitFlags.Organization, "organization", "", "Remote organization name to fetch configurations from Codacy. Required when api-token is provided.") + configResetCmd.Flags().StringVar(&configResetInitFlags.Repository, "repository", "", "Remote repository name to fetch configurations from Codacy. Required when api-token is provided.") + + // Add the reset subcommand to the config command + configCmd.AddCommand(configResetCmd) + // Add the config command to the root command + rootCmd.AddCommand(configCmd) +} diff --git a/cmd/configsetup/setup.go b/cmd/configsetup/setup.go new file mode 100644 index 00000000..a915dfbc --- /dev/null +++ b/cmd/configsetup/setup.go @@ -0,0 +1,441 @@ +package configsetup + +import ( + "fmt" + "log" + "os" + "path/filepath" + "sort" + "strings" + + codacyclient "codacy/cli-v2/codacy-client" + "codacy/cli-v2/config" + "codacy/cli-v2/domain" + "codacy/cli-v2/plugins" + "codacy/cli-v2/tools" + "codacy/cli-v2/tools/lizard" + "codacy/cli-v2/tools/pylint" + "codacy/cli-v2/utils" +) + +// Tool UUID constants +const ( + ESLint string = "f8b29663-2cb2-498d-b923-a10c6a8c05cd" + Trivy string = "2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb" + PMD string = "9ed24812-b6ee-4a58-9004-0ed183c45b8f" + PyLint string = "31677b6d-4ae0-4f56-8041-606a8d7a8e61" + DartAnalyzer string = "d203d615-6cf1-41f9-be5f-e2f660f7850f" + Semgrep string = "6792c561-236d-41b7-ba5e-9d6bee0d548b" + Lizard string = "76348462-84b3-409a-90d3-955e90abfb87" +) + +// AvailableTools lists all tool UUIDs supported by Codacy CLI. +var AvailableTools = []string{ + ESLint, + Trivy, + PMD, + PyLint, + DartAnalyzer, + Semgrep, + Lizard, +} + +// Map tool UUIDs to their names +var toolNameMap = map[string]string{ + ESLint: "eslint", + Trivy: "trivy", + PyLint: "pylint", + PMD: "pmd", + DartAnalyzer: "dartanalyzer", + Semgrep: "semgrep", + Lizard: "lizard", +} + +func CreateLanguagesConfigFileLocal(toolsConfigDir string) error { + content := `tools: + - name: pylint + languages: [Python] + extensions: [.py] + - name: eslint + languages: [JavaScript, TypeScript, JSX, TSX] + extensions: [.js, .jsx, .ts, .tsx] + - name: pmd + languages: [Java, JavaScript, JSP, Velocity, XML, Apex, Scala, Ruby, VisualForce] + extensions: [.java, .js, .jsp, .vm, .xml, .cls, .trigger, .scala, .rb, .page, .component] + - name: trivy + languages: [Multiple] + extensions: [] + - name: dartanalyzer + languages: [Dart] + extensions: [.dart] + - name: lizard + languages: [C, CPP, Java, "C#", JavaScript, TypeScript, VueJS, "Objective-C", Swift, Python, Ruby, "TTCN-3", PHP, Scala, GDScript, Golang, Lua, Rust, Fortran, Kotlin, Solidity, Erlang, Zig, Perl] + extensions: [.c, .cpp, .cc, .h, .hpp, .java, .cs, .js, .jsx, .ts, .tsx, .vue, .m, .swift, .py, .rb, .ttcn, .php, .scala, .gd, .go, .lua, .rs, .f, .f90, .kt, .sol, .erl, .zig, .pl] + - name: semgrep + languages: [C, CPP, "C#", Generic, Go, Java, JavaScript, JSON, Kotlin, Python, TypeScript, Ruby, Rust, JSX, PHP, Scala, Swift, Terraform] + extensions: [.c, .cpp, .h, .hpp, .cs, .go, .java, .js, .json, .kt, .py, .ts, .rb, .rs, .jsx, .php, .scala, .swift, .tf, .tfvars] + - name: codacy-enigma-cli + languages: [Multiple] + extensions: []` + + return os.WriteFile(filepath.Join(toolsConfigDir, "languages-config.yaml"), []byte(content), utils.DefaultFilePerms) +} + +func CreateGitIgnoreFile() error { + gitIgnorePath := filepath.Join(config.Config.LocalCodacyDirectory(), ".gitignore") + gitIgnoreFile, err := os.Create(gitIgnorePath) + if err != nil { + return fmt.Errorf("failed to create .gitignore file: %w", err) + } + defer gitIgnoreFile.Close() + + content := "# Codacy CLI\ntools-configs/\n.gitignore\ncli-config.yaml\nlogs/\n" + if _, err := gitIgnoreFile.WriteString(content); err != nil { + return fmt.Errorf("failed to write to .gitignore file: %w", err) + } + + return nil +} + +func CreateConfigurationFiles(toolsToUse []domain.Tool, cliLocalMode bool) error { + configFile, err := os.Create(config.Config.ProjectConfigFile()) + if err != nil { + return fmt.Errorf("failed to create project config file: %w", err) + } + defer configFile.Close() + + configContent := ConfigFileTemplate(toolsToUse) + _, err = configFile.WriteString(configContent) + if err != nil { + return fmt.Errorf("failed to write project config file: %w", err) + } + + cliConfigFile, err := os.Create(config.Config.CliConfigFile()) + if err != nil { + return fmt.Errorf("failed to create CLI config file: %w", err) + } + defer cliConfigFile.Close() + + cliConfigContent := CliConfigFileTemplate(cliLocalMode) + _, err = cliConfigFile.WriteString(cliConfigContent) + if err != nil { + return fmt.Errorf("failed to write CLI config file: %w", err) + } + + return nil +} + +func ConfigFileTemplate(toolsToUse []domain.Tool) string { + toolsMap := make(map[string]bool) + toolVersions := make(map[string]string) + neededRuntimes := make(map[string]bool) + defaultVersions := plugins.GetToolVersions() + runtimeVersions := plugins.GetRuntimeVersions() + runtimeDependencies := plugins.GetToolRuntimeDependencies() + + for _, tool := range toolsToUse { + toolsMap[tool.Uuid] = true + if tool.Version != "" { + toolVersions[tool.Uuid] = tool.Version + } else { + toolName := toolNameMap[tool.Uuid] + if defaultVersion, ok := defaultVersions[toolName]; ok { + toolVersions[tool.Uuid] = defaultVersion + } + } + + toolName := toolNameMap[tool.Uuid] + if toolName != "" { + if runtime, ok := runtimeDependencies[toolName]; ok { + if toolName == "dartanalyzer" { + neededRuntimes["dart"] = true + } else { + neededRuntimes[runtime] = true + } + } + } + } + + var sb strings.Builder + sb.WriteString("runtimes:\n") + + if len(toolsToUse) > 0 { + var sortedRuntimes []string + for runtime := range neededRuntimes { + sortedRuntimes = append(sortedRuntimes, runtime) + } + sort.Strings(sortedRuntimes) + for _, runtime := range sortedRuntimes { + sb.WriteString(fmt.Sprintf(" - %s@%s\n", runtime, runtimeVersions[runtime])) + } + } else { + supportedTools, err := plugins.GetSupportedTools() + if err != nil { + log.Printf("Warning: failed to get supported tools: %v", err) + return sb.String() + } + for toolName := range supportedTools { + if runtime, ok := runtimeDependencies[toolName]; ok { + if toolName == "dartanalyzer" { + neededRuntimes["dart"] = true + } else { + neededRuntimes[runtime] = true + } + } + } + var sortedRuntimes []string + for runtime := range neededRuntimes { + sortedRuntimes = append(sortedRuntimes, runtime) + } + sort.Strings(sortedRuntimes) + for _, runtime := range sortedRuntimes { + sb.WriteString(fmt.Sprintf(" - %s@%s\n", runtime, runtimeVersions[runtime])) + } + } + + sb.WriteString("tools:\n") + if len(toolsToUse) > 0 { + var sortedTools []string + for uuid, name := range toolNameMap { + if toolsMap[uuid] { + sortedTools = append(sortedTools, name) + } + } + sort.Strings(sortedTools) + for _, name := range sortedTools { + for uuid, toolNameLookup := range toolNameMap { + if toolNameLookup == name && toolsMap[uuid] { + version := toolVersions[uuid] + sb.WriteString(fmt.Sprintf(" - %s@%s\n", name, version)) + break + } + } + } + } else { + var sortedTools []string + supportedTools, err := plugins.GetSupportedTools() + if err != nil { + log.Printf("Warning: failed to get supported tools: %v", err) + return sb.String() + } + for toolName := range supportedTools { + if version, ok := defaultVersions[toolName]; ok { + if version != "" { + sortedTools = append(sortedTools, toolName) + } + } + } + sort.Strings(sortedTools) + for _, toolName := range sortedTools { + if version, ok := defaultVersions[toolName]; ok { + sb.WriteString(fmt.Sprintf(" - %s@%s\n", toolName, version)) + } + } + } + return sb.String() +} + +func CliConfigFileTemplate(cliLocalMode bool) string { + var cliModeString string + if cliLocalMode { + cliModeString = "local" + } else { + cliModeString = "remote" + } + return fmt.Sprintf(`mode: %s`, cliModeString) +} + +func BuildRepositoryConfigurationFiles(initFlags domain.InitFlags) error { + fmt.Println("Fetching repository configuration from codacy ...") + toolsConfigDir := config.Config.ToolsConfigDirectory() + if err := os.MkdirAll(toolsConfigDir, utils.DefaultDirPerms); err != nil { + return fmt.Errorf("failed to create tools-configs directory: %w", err) + } + if err := CleanConfigDirectory(toolsConfigDir); err != nil { + return fmt.Errorf("failed to clean configuration directory: %w", err) + } + + apiTools, err := tools.GetRepositoryTools(initFlags) + if err != nil { + return err + } + + uuidToName := map[string]string{ + ESLint: "eslint", + Trivy: "trivy", + PyLint: "pylint", + PMD: "pmd", + DartAnalyzer: "dartanalyzer", + Lizard: "lizard", + Semgrep: "semgrep", + } + + if err := tools.CreateLanguagesConfigFile(apiTools, toolsConfigDir, uuidToName, initFlags); err != nil { + return fmt.Errorf("failed to create languages configuration file: %w", err) + } + + configuredToolsWithUI := tools.FilterToolsByConfigUsage(apiTools) + err = CreateConfigurationFiles(apiTools, false) + if err != nil { + log.Fatal(err) + } + + for _, tool := range configuredToolsWithUI { + apiToolConfigurations, err := codacyclient.GetRepositoryToolPatterns(initFlags, tool.Uuid) + if err != nil { + fmt.Println("Error unmarshaling tool configurations:", err) + return err + } + CreateToolFileConfigurations(tool, apiToolConfigurations, initFlags) + } + return nil +} + +func CreateToolFileConfigurations(tool domain.Tool, patternConfiguration []domain.PatternConfiguration, initFlags domain.InitFlags) error { + toolsConfigDir := config.Config.ToolsConfigDirectory() + switch tool.Uuid { + case ESLint: + err := tools.CreateEslintConfig(toolsConfigDir, patternConfiguration) + if err != nil { + return fmt.Errorf("failed to write eslint config: %v", err) + } + fmt.Println("ESLint configuration created based on Codacy settings. Ignoring plugin rules. ESLint plugins are not supported yet.") + case Trivy: + err := CreateTrivyConfigFile(patternConfiguration, toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to create Trivy config: %v", err) + } + fmt.Println("Trivy configuration created based on Codacy settings") + case PMD: + err := CreatePMDConfigFile(patternConfiguration, toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to create PMD config: %v", err) + } + fmt.Println("PMD configuration created based on Codacy settings") + case PyLint: + err := CreatePylintConfigFile(patternConfiguration, toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to create Pylint config: %v", err) + } + fmt.Println("Pylint configuration created based on Codacy settings") + case DartAnalyzer: + err := CreateDartAnalyzerConfigFile(patternConfiguration, toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to create Dart Analyzer config: %v", err) + } + fmt.Println("Dart configuration created based on Codacy settings") + case Semgrep: + err := CreateSemgrepConfigFile(patternConfiguration, toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to create Semgrep config: %v", err) + } + fmt.Println("Semgrep configuration created based on Codacy settings") + case Lizard: + err := CreateLizardConfigFile(toolsConfigDir, patternConfiguration) + if err != nil { + return fmt.Errorf("failed to create Lizard config: %v", err) + } + fmt.Println("Lizard configuration created based on Codacy settings") + } + return nil +} + +func CreatePMDConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { + pmdConfigurationString := tools.CreatePmdConfig(config) + return os.WriteFile(filepath.Join(toolsConfigDir, "ruleset.xml"), []byte(pmdConfigurationString), utils.DefaultFilePerms) +} + +func CreatePylintConfigFile(configPatterns []domain.PatternConfiguration, toolsConfigDir string) error { + pylintConfigurationString := pylint.GeneratePylintRC(configPatterns) + return os.WriteFile(filepath.Join(toolsConfigDir, "pylint.rc"), []byte(pylintConfigurationString), utils.DefaultFilePerms) +} + +func CreateTrivyConfigFile(configPatterns []domain.PatternConfiguration, toolsConfigDir string) error { + trivyConfigurationString := tools.CreateTrivyConfig(configPatterns) + return os.WriteFile(filepath.Join(toolsConfigDir, "trivy.yaml"), []byte(trivyConfigurationString), utils.DefaultFilePerms) +} + +func CreateDartAnalyzerConfigFile(configPatterns []domain.PatternConfiguration, toolsConfigDir string) error { + dartAnalyzerConfigurationString := tools.CreateDartAnalyzerConfig(configPatterns) + return os.WriteFile(filepath.Join(toolsConfigDir, "analysis_options.yaml"), []byte(dartAnalyzerConfigurationString), utils.DefaultFilePerms) +} + +func CreateSemgrepConfigFile(configPatterns []domain.PatternConfiguration, toolsConfigDir string) error { + configData, err := tools.GetSemgrepConfig(configPatterns) + if err != nil { + return fmt.Errorf("failed to create Semgrep config: %v", err) + } + return os.WriteFile(filepath.Join(toolsConfigDir, "semgrep.yaml"), configData, utils.DefaultFilePerms) +} + +func CleanConfigDirectory(toolsConfigDir string) error { + if _, err := os.Stat(toolsConfigDir); os.IsNotExist(err) { + return nil + } + entries, err := os.ReadDir(toolsConfigDir) + if err != nil { + return fmt.Errorf("failed to read config directory: %w", err) + } + for _, entry := range entries { + if !entry.IsDir() { + filePath := filepath.Join(toolsConfigDir, entry.Name()) + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed to remove file %s: %w", filePath, err) + } + } + } + fmt.Println("Cleaned previous configuration files") + return nil +} + +func CreateLizardConfigFile(toolsConfigDir string, patternConfiguration []domain.PatternConfiguration) error { + patterns := make([]domain.PatternDefinition, len(patternConfiguration)) + for i, pattern := range patternConfiguration { + patterns[i] = pattern.PatternDefinition + } + err := lizard.CreateLizardConfig(toolsConfigDir, patterns) + if err != nil { + return fmt.Errorf("failed to create Lizard configuration: %w", err) + } + return nil +} + +func BuildDefaultConfigurationFiles(toolsConfigDir string, initFlags domain.InitFlags) error { + for _, tool := range AvailableTools { + patternsConfig, err := codacyclient.GetDefaultToolPatternsConfig(initFlags, tool) + if err != nil { + return fmt.Errorf("failed to get default tool patterns config: %w", err) + } + switch tool { + case ESLint: + if err := tools.CreateEslintConfig(toolsConfigDir, patternsConfig); err != nil { + return fmt.Errorf("failed to create eslint config file: %v", err) + } + case Trivy: + if err := CreateTrivyConfigFile(patternsConfig, toolsConfigDir); err != nil { + return fmt.Errorf("failed to create default Trivy configuration: %w", err) + } + case PMD: + if err := CreatePMDConfigFile(patternsConfig, toolsConfigDir); err != nil { + return fmt.Errorf("failed to create default PMD configuration: %w", err) + } + case PyLint: + if err := CreatePylintConfigFile(patternsConfig, toolsConfigDir); err != nil { + return fmt.Errorf("failed to create default Pylint configuration: %w", err) + } + case DartAnalyzer: + if err := CreateDartAnalyzerConfigFile(patternsConfig, toolsConfigDir); err != nil { + return fmt.Errorf("failed to create default Dart Analyzer configuration: %w", err) + } + case Semgrep: + if err := CreateSemgrepConfigFile(patternsConfig, toolsConfigDir); err != nil { + return fmt.Errorf("failed to create default Semgrep configuration: %w", err) + } + case Lizard: + if err := CreateLizardConfigFile(toolsConfigDir, patternsConfig); err != nil { + return fmt.Errorf("failed to create default Lizard configuration: %w", err) + } + } + } + return nil +} diff --git a/cmd/init.go b/cmd/init.go index dd7b1698..638e0d9d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,20 +1,13 @@ package cmd import ( - codacyclient "codacy/cli-v2/codacy-client" + "codacy/cli-v2/cmd/configsetup" "codacy/cli-v2/config" "codacy/cli-v2/domain" - "codacy/cli-v2/plugins" - "codacy/cli-v2/tools" - "codacy/cli-v2/tools/lizard" - "codacy/cli-v2/tools/pylint" "codacy/cli-v2/utils" "fmt" "log" "os" - "path/filepath" - "sort" - "strings" "github.com/spf13/cobra" ) @@ -32,16 +25,16 @@ func init() { var initCmd = &cobra.Command{ Use: "init", Short: "Bootstraps project configuration", - Long: "Bootstraps project configuration, creates codacy configuration file", + Long: "Bootstraps project configuration, creates codacy configuration file and necessary tool configurations.", Run: func(cmd *cobra.Command, args []string) { - // Create local codacy directory first + // Create local .codacy directory first if err := config.Config.CreateLocalCodacyDir(); err != nil { log.Fatalf("Failed to create local codacy directory: %v", err) } - // Create tools-configs directory + // Create .codacy/tools-configs directory toolsConfigDir := config.Config.ToolsConfigDirectory() - if err := os.MkdirAll(toolsConfigDir, 0777); err != nil { + if err := os.MkdirAll(toolsConfigDir, utils.DefaultDirPerms); err != nil { log.Fatalf("Failed to create tools-configs directory: %v", err) } @@ -49,542 +42,38 @@ var initCmd = &cobra.Command{ if cliLocalMode { fmt.Println() - fmt.Println("ℹ️ No project token was specified, fetching codacy default configurations") + fmt.Println("ℹ️ No API token was specified. Proceeding with local default configurations.") noTools := []domain.Tool{} - err := createConfigurationFiles(noTools, cliLocalMode) - if err != nil { - log.Fatal(err) + if err := configsetup.CreateConfigurationFiles(noTools, cliLocalMode); err != nil { + log.Fatalf("Failed to create base configuration files: %v", err) } - // Create default configuration files - if err := buildDefaultConfigurationFiles(toolsConfigDir); err != nil { - log.Fatal(err) + // Create default configuration files for tools + if err := configsetup.BuildDefaultConfigurationFiles(toolsConfigDir, initFlags); err != nil { + log.Fatalf("Failed to build default tool configuration files: %v", err) } - if err := createLanguagesConfigFileLocal(toolsConfigDir); err != nil { - log.Fatal(err) + // Create the languages configuration file for local mode + if err := configsetup.CreateLanguagesConfigFileLocal(toolsConfigDir); err != nil { + log.Fatalf("Failed to create local languages configuration file: %v", err) } } else { - err := buildRepositoryConfigurationFiles(initFlags.ApiToken) - if err != nil { - log.Fatal(err) + // API token provided, fetch configuration from Codacy + fmt.Println("API token specified. Fetching repository-specific configurations from Codacy...") + if err := configsetup.BuildRepositoryConfigurationFiles(initFlags); err != nil { + log.Fatalf("Failed to build repository-specific configuration files: %v", err) } } - createGitIgnoreFile() + + // Create or update .gitignore file in .codacy directory + if err := configsetup.CreateGitIgnoreFile(); err != nil { + log.Printf("Warning: Failed to create or update .codacy/.gitignore: %v", err) + } + fmt.Println() fmt.Println("✅ Successfully initialized Codacy configuration!") fmt.Println() fmt.Println("🔧 Next steps:") - fmt.Println(" 1. Run 'codacy-cli install' to install all dependencies") - fmt.Println(" 2. Run 'codacy-cli analyze' to start analyzing your code") + fmt.Println(" 1. Run 'codacy-cli install' to install all dependencies.") + fmt.Println(" 2. Run 'codacy-cli analyze' to start analyzing your code.") fmt.Println() }, } - -func createLanguagesConfigFileLocal(toolsConfigDir string) error { - content := `tools: - - name: pylint - languages: [Python] - extensions: [.py] - - name: eslint - languages: [JavaScript, TypeScript, JSX, TSX] - extensions: [.js, .jsx, .ts, .tsx] - - name: pmd - languages: [Java, JavaScript, JSP, Velocity, XML, Apex, Scala, Ruby, VisualForce] - extensions: [.java, .js, .jsp, .vm, .xml, .cls, .trigger, .scala, .rb, .page, .component] - - name: trivy - languages: [Multiple] - extensions: [] - - name: dartanalyzer - languages: [Dart] - extensions: [.dart] - - name: lizard - languages: [C, CPP, Java, "C#", JavaScript, TypeScript, VueJS, "Objective-C", Swift, Python, Ruby, "TTCN-3", PHP, Scala, GDScript, Golang, Lua, Rust, Fortran, Kotlin, Solidity, Erlang, Zig, Perl] - extensions: [.c, .cpp, .cc, .h, .hpp, .java, .cs, .js, .jsx, .ts, .tsx, .vue, .m, .swift, .py, .rb, .ttcn, .php, .scala, .gd, .go, .lua, .rs, .f, .f90, .kt, .sol, .erl, .zig, .pl] - - name: semgrep - languages: [C, CPP, "C#", Generic, Go, Java, JavaScript, JSON, Kotlin, Python, TypeScript, Ruby, Rust, JSX, PHP, Scala, Swift, Terraform] - extensions: [.c, .cpp, .h, .hpp, .cs, .go, .java, .js, .json, .kt, .py, .ts, .rb, .rs, .jsx, .php, .scala, .swift, .tf, .tfvars] - - name: codacy-enigma-cli - languages: [Multiple] - extensions: []` - - return os.WriteFile(filepath.Join(toolsConfigDir, "languages-config.yaml"), []byte(content), utils.DefaultFilePerms) -} - -func createGitIgnoreFile() error { - gitIgnorePath := filepath.Join(config.Config.LocalCodacyDirectory(), ".gitignore") - gitIgnoreFile, err := os.Create(gitIgnorePath) - if err != nil { - return fmt.Errorf("failed to create .gitignore file: %w", err) - } - defer gitIgnoreFile.Close() - - content := "# Codacy CLI\ntools-configs/\n.gitignore\ncli-config.yaml\nlogs/\n" - if _, err := gitIgnoreFile.WriteString(content); err != nil { - return fmt.Errorf("failed to write to .gitignore file: %w", err) - } - - return nil -} - -func createConfigurationFiles(tools []domain.Tool, cliLocalMode bool) error { - configFile, err := os.Create(config.Config.ProjectConfigFile()) - if err != nil { - return fmt.Errorf("failed to create project config file: %w", err) - } - defer configFile.Close() - - configContent := configFileTemplate(tools) - _, err = configFile.WriteString(configContent) - if err != nil { - return fmt.Errorf("failed to write project config file: %w", err) - } - - cliConfigFile, err := os.Create(config.Config.CliConfigFile()) - if err != nil { - return fmt.Errorf("failed to create CLI config file: %w", err) - } - defer cliConfigFile.Close() - - cliConfigContent := cliConfigFileTemplate(cliLocalMode) - _, err = cliConfigFile.WriteString(cliConfigContent) - if err != nil { - return fmt.Errorf("failed to write CLI config file: %w", err) - } - - return nil -} - -// Map tool UUIDs to their names -var toolNameMap = map[string]string{ - ESLint: "eslint", - Trivy: "trivy", - PyLint: "pylint", - PMD: "pmd", - DartAnalyzer: "dartanalyzer", - Semgrep: "semgrep", - Lizard: "lizard", -} - -// RuntimePluginConfig holds the structure of the runtime plugin.yaml file -type RuntimePluginConfig struct { - Name string `yaml:"name"` - Description string `yaml:"description"` - DefaultVersion string `yaml:"default_version"` -} - -func configFileTemplate(tools []domain.Tool) string { - // Maps to track which tools are enabled - toolsMap := make(map[string]bool) - toolVersions := make(map[string]string) - - // Track needed runtimes - neededRuntimes := make(map[string]bool) - - // Get tool versions from plugin configurations - defaultVersions := plugins.GetToolVersions() - - // Get runtime versions all at once - runtimeVersions := plugins.GetRuntimeVersions() - - // Get tool runtime dependencies - runtimeDependencies := plugins.GetToolRuntimeDependencies() - - // Build map of enabled tools with their versions - for _, tool := range tools { - toolsMap[tool.Uuid] = true - if tool.Version != "" { - toolVersions[tool.Uuid] = tool.Version - } else { - toolName := toolNameMap[tool.Uuid] - if defaultVersion, ok := defaultVersions[toolName]; ok { - toolVersions[tool.Uuid] = defaultVersion - } - } - - // Get the tool's runtime dependency - toolName := toolNameMap[tool.Uuid] - if toolName != "" { - if runtime, ok := runtimeDependencies[toolName]; ok { - // Handle special case for dartanalyzer which can use either dart or flutter - if toolName == "dartanalyzer" { - // For now, default to dart runtime - neededRuntimes["dart"] = true - } else { - neededRuntimes[runtime] = true - } - } - } - } - - // Start building the YAML content - var sb strings.Builder - sb.WriteString("runtimes:\n") - - // Only include runtimes needed by the enabled tools - if len(tools) > 0 { - // Create a sorted slice of runtimes - var sortedRuntimes []string - for runtime := range neededRuntimes { - sortedRuntimes = append(sortedRuntimes, runtime) - } - sort.Strings(sortedRuntimes) - - // Write sorted runtimes - for _, runtime := range sortedRuntimes { - sb.WriteString(fmt.Sprintf(" - %s@%s\n", runtime, runtimeVersions[runtime])) - } - } else { - // In local mode with no tools specified, include only the necessary runtimes - supportedTools, err := plugins.GetSupportedTools() - if err != nil { - log.Printf("Warning: failed to get supported tools: %v", err) - return sb.String() - } - - // Get runtimes needed by supported tools - for toolName := range supportedTools { - if runtime, ok := runtimeDependencies[toolName]; ok { - if toolName == "dartanalyzer" { - neededRuntimes["dart"] = true - } else { - neededRuntimes[runtime] = true - } - } - } - - // Create a sorted slice of runtimes - var sortedRuntimes []string - for runtime := range neededRuntimes { - sortedRuntimes = append(sortedRuntimes, runtime) - } - sort.Strings(sortedRuntimes) - - // Write sorted runtimes - for _, runtime := range sortedRuntimes { - sb.WriteString(fmt.Sprintf(" - %s@%s\n", runtime, runtimeVersions[runtime])) - } - } - - sb.WriteString("tools:\n") - - // If we have tools from the API (enabled tools), use only those - if len(tools) > 0 { - // Create a sorted slice of tool names - var sortedTools []string - for uuid, name := range toolNameMap { - if toolsMap[uuid] { - sortedTools = append(sortedTools, name) - } - } - sort.Strings(sortedTools) - - // Write sorted tools - for _, name := range sortedTools { - // Find the UUID for this tool name to get its version - for uuid, toolName := range toolNameMap { - if toolName == name && toolsMap[uuid] { - version := toolVersions[uuid] - sb.WriteString(fmt.Sprintf(" - %s@%s\n", name, version)) - break - } - } - } - } else { - // If no tools were specified (local mode), include all tools in sorted order - var sortedTools []string - - // Get supported tools from plugin system - supportedTools, err := plugins.GetSupportedTools() - if err != nil { - log.Printf("Warning: failed to get supported tools: %v", err) - return sb.String() - } - - // Convert map keys to slice and sort them - for toolName := range supportedTools { - if version, ok := defaultVersions[toolName]; ok { - // Skip tools without a version - if version != "" { - sortedTools = append(sortedTools, toolName) - } - } - } - sort.Strings(sortedTools) - - // Write sorted tools - for _, toolName := range sortedTools { - if version, ok := defaultVersions[toolName]; ok { - sb.WriteString(fmt.Sprintf(" - %s@%s\n", toolName, version)) - } - } - } - - return sb.String() -} - -func cliConfigFileTemplate(cliLocalMode bool) string { - var cliModeString string - - if cliLocalMode { - cliModeString = "local" - } else { - cliModeString = "remote" - } - - return fmt.Sprintf(`mode: %s`, cliModeString) -} - -func buildRepositoryConfigurationFiles(token string) error { - fmt.Println("Fetching repository configuration from codacy ...") - - toolsConfigDir := config.Config.ToolsConfigDirectory() - - // Create tools-configs directory if it doesn't exist - if err := os.MkdirAll(toolsConfigDir, utils.DefaultDirPerms); err != nil { - return fmt.Errorf("failed to create tools-configs directory: %w", err) - } - - // Clear any previous configuration files - if err := cleanConfigDirectory(toolsConfigDir); err != nil { - return fmt.Errorf("failed to clean configuration directory: %w", err) - } - - apiTools, err := tools.GetRepositoryTools(initFlags) - if err != nil { - return err - } - - // Map UUID to tool shortname for lookup - uuidToName := map[string]string{ - ESLint: "eslint", - Trivy: "trivy", - PyLint: "pylint", - PMD: "pmd", - DartAnalyzer: "dartanalyzer", - Lizard: "lizard", - Semgrep: "semgrep", - } - - // Generate languages configuration based on API tools response - if err := tools.CreateLanguagesConfigFile(apiTools, toolsConfigDir, uuidToName, initFlags); err != nil { - return fmt.Errorf("failed to create languages configuration file: %w", err) - } - - // Filter out any tools that use configuration file - configuredToolsWithUI := tools.FilterToolsByConfigUsage(apiTools) - - // Create main config files with all enabled API tools - err = createConfigurationFiles(apiTools, false) - if err != nil { - log.Fatal(err) - } - - // Only generate config files for tools not using their own config file - for _, tool := range configuredToolsWithUI { - - apiToolConfigurations, err := codacyclient.GetRepositoryToolPatterns(initFlags, tool.Uuid) - - if err != nil { - fmt.Println("Error unmarshaling tool configurations:", err) - return err - } - - createToolFileConfigurations(tool, apiToolConfigurations) - } - - return nil -} - -// map tool uuid to tool name -func createToolFileConfigurations(tool domain.Tool, patternConfiguration []domain.PatternConfiguration) error { - toolsConfigDir := config.Config.ToolsConfigDirectory() - switch tool.Uuid { - case ESLint: - err := tools.CreateEslintConfig(toolsConfigDir, patternConfiguration) - if err != nil { - return fmt.Errorf("failed to write eslint config: %v", err) - } - fmt.Println("ESLint configuration created based on Codacy settings. Ignoring plugin rules. ESLint plugins are not supported yet.") - case Trivy: - err := createTrivyConfigFile(patternConfiguration, toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to create Trivy config: %v", err) - } - fmt.Println("Trivy configuration created based on Codacy settings") - case PMD: - err := createPMDConfigFile(patternConfiguration, toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to create PMD config: %v", err) - } - fmt.Println("PMD configuration created based on Codacy settings") - case PyLint: - err := createPylintConfigFile(patternConfiguration, toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to create Pylint config: %v", err) - } - fmt.Println("Pylint configuration created based on Codacy settings") - case DartAnalyzer: - err := createDartAnalyzerConfigFile(patternConfiguration, toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to create Dart Analyzer config: %v", err) - } - fmt.Println("Dart configuration created based on Codacy settings") - case Semgrep: - err := createSemgrepConfigFile(patternConfiguration, toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to create Semgrep config: %v", err) - } - fmt.Println("Semgrep configuration created based on Codacy settings") - case Lizard: - err := createLizardConfigFile(toolsConfigDir, patternConfiguration) - if err != nil { - return fmt.Errorf("failed to create Lizard config: %v", err) - } - fmt.Println("Lizard configuration created based on Codacy settings") - } - return nil -} - -func createPMDConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { - pmdConfigurationString := tools.CreatePmdConfig(config) - return os.WriteFile(filepath.Join(toolsConfigDir, "ruleset.xml"), []byte(pmdConfigurationString), utils.DefaultFilePerms) -} - -func createPylintConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { - pylintConfigurationString := pylint.GeneratePylintRC(config) - return os.WriteFile(filepath.Join(toolsConfigDir, "pylint.rc"), []byte(pylintConfigurationString), utils.DefaultFilePerms) -} - -// createTrivyConfigFile creates a trivy.yaml configuration file based on the API configuration -func createTrivyConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { - - trivyConfigurationString := tools.CreateTrivyConfig(config) - - // Write to file - return os.WriteFile(filepath.Join(toolsConfigDir, "trivy.yaml"), []byte(trivyConfigurationString), utils.DefaultFilePerms) -} - -func createDartAnalyzerConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { - - dartAnalyzerConfigurationString := tools.CreateDartAnalyzerConfig(config) - return os.WriteFile(filepath.Join(toolsConfigDir, "analysis_options.yaml"), []byte(dartAnalyzerConfigurationString), utils.DefaultFilePerms) -} - -// SemgrepRulesFile represents the structure of the rules.yaml file -type SemgrepRulesFile struct { - Rules []map[string]interface{} `yaml:"rules"` -} - -// createSemgrepConfigFile creates a semgrep.yaml configuration file based on the API configuration -func createSemgrepConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { - // Use the refactored function from tools package - configData, err := tools.GetSemgrepConfig(config) - - if err != nil { - return fmt.Errorf("failed to create Semgrep config: %v", err) - } - - // Write to file - return os.WriteFile(filepath.Join(toolsConfigDir, "semgrep.yaml"), configData, utils.DefaultFilePerms) -} - -// cleanConfigDirectory removes all previous configuration files in the tools-configs directory -func cleanConfigDirectory(toolsConfigDir string) error { - // Check if directory exists - if _, err := os.Stat(toolsConfigDir); os.IsNotExist(err) { - return nil // Directory doesn't exist, nothing to clean - } - - // Read directory contents - entries, err := os.ReadDir(toolsConfigDir) - if err != nil { - return fmt.Errorf("failed to read config directory: %w", err) - } - - // Remove all files - for _, entry := range entries { - if !entry.IsDir() { // Only remove files, not subdirectories - filePath := filepath.Join(toolsConfigDir, entry.Name()) - if err := os.Remove(filePath); err != nil { - return fmt.Errorf("failed to remove file %s: %w", filePath, err) - } - } - } - - fmt.Println("Cleaned previous configuration files") - return nil -} - -func createLizardConfigFile(toolsConfigDir string, patternConfiguration []domain.PatternConfiguration) error { - patterns := make([]domain.PatternDefinition, len(patternConfiguration)) - for i, pattern := range patternConfiguration { - patterns[i] = pattern.PatternDefinition - - } - err := lizard.CreateLizardConfig(toolsConfigDir, patterns) - if err != nil { - return fmt.Errorf("failed to create Lizard configuration: %w", err) - } - return nil -} - -// buildDefaultConfigurationFiles creates default configuration files for all tools -func buildDefaultConfigurationFiles(toolsConfigDir string) error { - for _, tool := range AvailableTools { - patternsConfig, err := codacyclient.GetDefaultToolPatternsConfig(initFlags, tool) - if err != nil { - return fmt.Errorf("failed to get default tool patterns config: %w", err) - } - switch tool { - case ESLint: - if err := tools.CreateEslintConfig(toolsConfigDir, patternsConfig); err != nil { - return fmt.Errorf("failed to create eslint config file: %v", err) - } - case Trivy: - if err := createTrivyConfigFile(patternsConfig, toolsConfigDir); err != nil { - return fmt.Errorf("failed to create default Trivy configuration: %w", err) - } - case PMD: - if err := createPMDConfigFile(patternsConfig, toolsConfigDir); err != nil { - return fmt.Errorf("failed to create default PMD configuration: %w", err) - } - case PyLint: - if err := createPylintConfigFile(patternsConfig, toolsConfigDir); err != nil { - return fmt.Errorf("failed to create default Pylint configuration: %w", err) - } - case DartAnalyzer: - if err := createDartAnalyzerConfigFile(patternsConfig, toolsConfigDir); err != nil { - return fmt.Errorf("failed to create default Dart Analyzer configuration: %w", err) - } - case Semgrep: - if err := createSemgrepConfigFile(patternsConfig, toolsConfigDir); err != nil { - return fmt.Errorf("failed to create default Semgrep configuration: %w", err) - } - case Lizard: - if err := createLizardConfigFile(toolsConfigDir, patternsConfig); err != nil { - return fmt.Errorf("failed to create default Lizard configuration: %w", err) - } - } - } - return nil -} - -const ( - ESLint string = "f8b29663-2cb2-498d-b923-a10c6a8c05cd" - Trivy string = "2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb" - PMD string = "9ed24812-b6ee-4a58-9004-0ed183c45b8f" - PyLint string = "31677b6d-4ae0-4f56-8041-606a8d7a8e61" - DartAnalyzer string = "d203d615-6cf1-41f9-be5f-e2f660f7850f" - Semgrep string = "6792c561-236d-41b7-ba5e-9d6bee0d548b" - Lizard string = "76348462-84b3-409a-90d3-955e90abfb87" -) - -// AvailableTools lists all tool UUIDs supported by Codacy CLI. -var AvailableTools = []string{ - ESLint, - Trivy, - PMD, - PyLint, - DartAnalyzer, - Semgrep, - Lizard, -} diff --git a/cmd/init_test.go b/cmd/init_test.go index e950d3b1..645b1de6 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -1,6 +1,7 @@ package cmd import ( + "codacy/cli-v2/cmd/configsetup" "codacy/cli-v2/config" "codacy/cli-v2/domain" "codacy/cli-v2/utils" @@ -35,7 +36,7 @@ func TestConfigFileTemplate(t *testing.T) { name: "only eslint enabled", tools: []domain.Tool{ { - Uuid: ESLint, + Uuid: configsetup.ESLint, Name: "eslint", Version: "9.4.0", }, @@ -55,7 +56,7 @@ func TestConfigFileTemplate(t *testing.T) { name: "only pylint enabled", tools: []domain.Tool{ { - Uuid: PyLint, + Uuid: configsetup.PyLint, Name: "pylint", Version: "3.4.0", }, @@ -75,12 +76,12 @@ func TestConfigFileTemplate(t *testing.T) { name: "eslint and trivy enabled", tools: []domain.Tool{ { - Uuid: ESLint, + Uuid: configsetup.ESLint, Name: "eslint", Version: "9.4.0", }, { - Uuid: Trivy, + Uuid: configsetup.Trivy, Name: "trivy", Version: "0.60.0", }, @@ -100,22 +101,22 @@ func TestConfigFileTemplate(t *testing.T) { name: "all tools enabled", tools: []domain.Tool{ { - Uuid: ESLint, + Uuid: configsetup.ESLint, Name: "eslint", Version: "9.4.0", }, { - Uuid: Trivy, + Uuid: configsetup.Trivy, Name: "trivy", Version: "0.60.0", }, { - Uuid: PyLint, + Uuid: configsetup.PyLint, Name: "pylint", Version: "3.4.0", }, { - Uuid: PMD, + Uuid: configsetup.PMD, Name: "pmd", Version: "6.56.0", }, @@ -134,7 +135,7 @@ func TestConfigFileTemplate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := configFileTemplate(tt.tools) + result := configsetup.ConfigFileTemplate(tt.tools) // Check that expected strings are present for _, exp := range tt.expected { @@ -173,7 +174,7 @@ func TestCleanConfigDirectory(t *testing.T) { assert.Equal(t, len(testFiles), len(files), "Expected %d files before cleaning", len(testFiles)) // Run the clean function - err = cleanConfigDirectory(tempDir) + err = configsetup.CleanConfigDirectory(tempDir) assert.NoError(t, err, "cleanConfigDirectory should not return an error") // Verify all files are gone @@ -189,9 +190,9 @@ func TestInitCommand_NoToken(t *testing.T) { defer os.Chdir(originalWD) // Use the real plugins/tools/semgrep/rules.yaml file - rulesPath := filepath.Join("plugins", "tools", "semgrep", "rules.yaml") + rulesPath := filepath.Join("..", "plugins", "tools", "semgrep", "rules.yaml") if _, err := os.Stat(rulesPath); os.IsNotExist(err) { - t.Skip("plugins/tools/semgrep/rules.yaml not found; skipping test") + t.Skipf("plugins/tools/semgrep/rules.yaml not found at %s; skipping test", rulesPath) } // Change to the temp directory to simulate a new project @@ -199,10 +200,12 @@ func TestInitCommand_NoToken(t *testing.T) { assert.NoError(t, err, "Failed to change working directory to tempDir") // Simulate running init with no token - initFlags.ApiToken = "" - initFlags.Provider = "" - initFlags.Organization = "" - initFlags.Repository = "" + currentInitFlags := domain.InitFlags{ + ApiToken: "", + Provider: "", + Organization: "", + Repository: "", + } // Call the Run logic from initCmd if err := config.Config.CreateLocalCodacyDir(); err != nil { @@ -210,37 +213,46 @@ func TestInitCommand_NoToken(t *testing.T) { } toolsConfigDir := config.Config.ToolsConfigDirectory() - if err := os.MkdirAll(toolsConfigDir, utils.DefaultFilePerms); err != nil { + if err := os.MkdirAll(toolsConfigDir, utils.DefaultDirPerms); err != nil { t.Fatalf("Failed to create tools-configs directory: %v", err) } - cliLocalMode := len(initFlags.ApiToken) == 0 + cliLocalMode := len(currentInitFlags.ApiToken) == 0 if cliLocalMode { noTools := []domain.Tool{} - err := createConfigurationFiles(noTools, cliLocalMode) - assert.NoError(t, err, "createConfigurationFiles should not return an error") - if err := buildDefaultConfigurationFiles(toolsConfigDir); err != nil { + err := configsetup.CreateConfigurationFiles(noTools, cliLocalMode) + assert.NoError(t, err, "CreateConfigurationFiles should not return an error") + if err := configsetup.BuildDefaultConfigurationFiles(toolsConfigDir, currentInitFlags); err != nil { t.Fatalf("Failed to build default configuration files: %v", err) } + if err := configsetup.CreateLanguagesConfigFileLocal(toolsConfigDir); err != nil { + t.Fatalf("Failed to create languages config file: %v", err) + } } // Assert that the expected config files are created codacyDir := config.Config.LocalCodacyDirectory() expectedFiles := []string{ - "tools-configs/eslint.config.mjs", - "tools-configs/trivy.yaml", - "tools-configs/ruleset.xml", - "tools-configs/pylint.rc", - "tools-configs/analysis_options.yaml", - "tools-configs/semgrep.yaml", - "tools-configs/lizard.yaml", + filepath.Join("tools-configs", "eslint.config.mjs"), + filepath.Join("tools-configs", "trivy.yaml"), + filepath.Join("tools-configs", "ruleset.xml"), + filepath.Join("tools-configs", "pylint.rc"), + filepath.Join("tools-configs", "analysis_options.yaml"), + filepath.Join("tools-configs", "semgrep.yaml"), + filepath.Join("tools-configs", "lizard.yaml"), "codacy.yaml", "cli-config.yaml", + filepath.Join("tools-configs", "languages-config.yaml"), + ".gitignore", } for _, file := range expectedFiles { filePath := filepath.Join(codacyDir, file) + if file == ".gitignore" { + filePath = filepath.Join(config.Config.LocalCodacyDirectory(), file) + } + _, err := os.Stat(filePath) - assert.NoError(t, err, "Expected config file %s to be created", file) + assert.NoError(t, err, "Expected config file %s to be created at %s", file, filePath) } } diff --git a/cmd/install.go b/cmd/install.go index aebf000f..47826d39 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -2,7 +2,6 @@ package cmd import ( "codacy/cli-v2/config" - cfg "codacy/cli-v2/config" config_file "codacy/cli-v2/config-file" "codacy/cli-v2/utils/logger" "fmt" @@ -41,7 +40,7 @@ var installCmd = &cobra.Command{ } // Load config file - if err := config_file.ReadConfigFile(cfg.Config.ProjectConfigFile()); err != nil { + if err := config_file.ReadConfigFile(config.Config.ProjectConfigFile()); err != nil { logger.Warn("Configuration file not found", logrus.Fields{ "error": err.Error(), }) @@ -54,15 +53,15 @@ var installCmd = &cobra.Command{ // Check if anything needs to be installed needsInstallation := false - for name, runtime := range cfg.Config.Runtimes() { - if !cfg.Config.IsRuntimeInstalled(name, runtime) { + for name, runtime := range config.Config.Runtimes() { + if !config.Config.IsRuntimeInstalled(name, runtime) { needsInstallation = true break } } if !needsInstallation { - for name, tool := range cfg.Config.Tools() { - if !cfg.Config.IsToolInstalled(name, tool) { + for name, tool := range config.Config.Tools() { + if !config.Config.IsToolInstalled(name, tool) { needsInstallation = true break } @@ -83,13 +82,13 @@ var installCmd = &cobra.Command{ // Calculate total items to install totalItems := 0 - for name, runtime := range cfg.Config.Runtimes() { - if !cfg.Config.IsRuntimeInstalled(name, runtime) { + for name, runtime := range config.Config.Runtimes() { + if !config.Config.IsRuntimeInstalled(name, runtime) { totalItems++ } } - for name, tool := range cfg.Config.Tools() { - if !cfg.Config.IsToolInstalled(name, tool) { + for name, tool := range config.Config.Tools() { + if !config.Config.IsToolInstalled(name, tool) { totalItems++ } } @@ -103,8 +102,8 @@ var installCmd = &cobra.Command{ // Print list of items to install fmt.Println("📦 Items to install:") - for name, runtime := range cfg.Config.Runtimes() { - if !cfg.Config.IsRuntimeInstalled(name, runtime) { + for name, runtime := range config.Config.Runtimes() { + if !config.Config.IsRuntimeInstalled(name, runtime) { logger.Info("Runtime scheduled for installation", logrus.Fields{ "runtime": name, "version": runtime.Version, @@ -112,8 +111,8 @@ var installCmd = &cobra.Command{ fmt.Printf(" • Runtime: %s v%s\n", name, runtime.Version) } } - for name, tool := range cfg.Config.Tools() { - if !cfg.Config.IsToolInstalled(name, tool) { + for name, tool := range config.Config.Tools() { + if !config.Config.IsToolInstalled(name, tool) { logger.Info("Tool scheduled for installation", logrus.Fields{ "tool": name, "version": tool.Version, @@ -152,14 +151,14 @@ var installCmd = &cobra.Command{ log.SetOutput(io.Discard) // Install runtimes first - for name, runtime := range cfg.Config.Runtimes() { - if !cfg.Config.IsRuntimeInstalled(name, runtime) { + for name, runtime := range config.Config.Runtimes() { + if !config.Config.IsRuntimeInstalled(name, runtime) { progressBar.Describe(fmt.Sprintf("Installing runtime: %s v%s...", name, runtime.Version)) logger.Info("Installing runtime", logrus.Fields{ "runtime": name, "version": runtime.Version, }) - err := cfg.InstallRuntime(name, runtime) + err := config.InstallRuntime(name, runtime) if err != nil { logger.Error("Failed to install runtime", logrus.Fields{ "runtime": name, @@ -180,14 +179,14 @@ var installCmd = &cobra.Command{ } // Install tools - for name, tool := range cfg.Config.Tools() { - if !cfg.Config.IsToolInstalled(name, tool) { + for name, tool := range config.Config.Tools() { + if !config.Config.IsToolInstalled(name, tool) { progressBar.Describe(fmt.Sprintf("Installing tool: %s v%s...", name, tool.Version)) logger.Info("Installing tool", logrus.Fields{ "tool": name, "version": tool.Version, }) - err := cfg.InstallTool(name, tool, registry) + err := config.InstallTool(name, tool, registry) if err != nil { logger.Error("Failed to install tool", logrus.Fields{ "tool": name, @@ -215,16 +214,16 @@ var installCmd = &cobra.Command{ // Print completion status with warnings for failed installations fmt.Println() var hasFailures bool - for name, runtime := range cfg.Config.Runtimes() { - if !cfg.Config.IsRuntimeInstalled(name, runtime) { + for name, runtime := range config.Config.Runtimes() { + if !config.Config.IsRuntimeInstalled(name, runtime) { color.Yellow(" ⚠️ Runtime: %s v%s (installation failed)", name, runtime.Version) hasFailures = true } else { green.Printf(" ✓ Runtime: %s v%s\n", name, runtime.Version) } } - for name, tool := range cfg.Config.Tools() { - if !cfg.Config.IsToolInstalled(name, tool) { + for name, tool := range config.Config.Tools() { + if !config.Config.IsToolInstalled(name, tool) { color.Yellow(" ⚠️ Tool: %s v%s (installation failed)", name, tool.Version) hasFailures = true } else { diff --git a/config/config.go b/config/config.go index ca2165f9..e3e2dbb3 100644 --- a/config/config.go +++ b/config/config.go @@ -8,8 +8,15 @@ import ( "codacy/cli-v2/plugins" "codacy/cli-v2/utils" + + "gopkg.in/yaml.v3" // Added import for YAML parsing ) +// CliConfigYaml defines the structure for parsing .codacy/cli-config.yaml +type CliConfigYaml struct { + Mode string `yaml:"mode"` +} + type ConfigType struct { repositoryDirectory string @@ -231,3 +238,35 @@ func (c *ConfigType) IsToolInstalled(name string, tool *plugins.ToolInfo) bool { // Global singleton config-file var Config = ConfigType{} + +// GetCliMode reads and parses the .codacy/cli-config.yaml file to determine the CLI's operational mode. +// It returns "local" by default and an error if the file doesn't exist or an error occurs during parsing. +func (c *ConfigType) GetCliMode() (string, error) { + cliConfigFilePath := c.CliConfigFile() + currentCliMode := "local" // Default to local + + content, readErr := os.ReadFile(cliConfigFilePath) + if readErr != nil { + if os.IsNotExist(readErr) { + // File does not exist. Return default mode and the error so the caller can warn. + return currentCliMode, readErr + } + // Some other error occurred during reading the file. + return currentCliMode, fmt.Errorf("failed to read %s: %w", cliConfigFilePath, readErr) + } + + // If ReadFile was successful, the file exists. Now parse it. + var parsedCliConfig CliConfigYaml + if yamlErr := yaml.Unmarshal(content, &parsedCliConfig); yamlErr != nil { + return currentCliMode, fmt.Errorf("failed to parse %s: %w", cliConfigFilePath, yamlErr) + } + + if parsedCliConfig.Mode == "remote" || parsedCliConfig.Mode == "local" { + currentCliMode = parsedCliConfig.Mode + } else { + // Invalid mode value in the config file. + return "local", fmt.Errorf("invalid mode value \"%s\" in %s", parsedCliConfig.Mode, cliConfigFilePath) + } + + return currentCliMode, nil +} diff --git a/docs/config-reset-scenarios.md b/docs/config-reset-scenarios.md new file mode 100644 index 00000000..12d7a337 --- /dev/null +++ b/docs/config-reset-scenarios.md @@ -0,0 +1,28 @@ +# Codacy CLI `config reset` Command Scenarios + +This document outlines the possible scenarios for the `codacy-cli config reset` command, considering the current CLI mode (as defined in `.codacy/cli-config.yaml`) and the flags provided during the command execution. + +| Current CLI Mode (`.codacy/cli-config.yaml`) | `config reset` Flags Provided | Behavior | +| :------------------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Local** | No flags | Resets configuration to local default settings. Creates/overwrites `.codacy/codacy.yaml` and `tools-configs/` with defaults. `cli-config.yaml` is set to `mode: local`. | +| **Local** | `--api-token `
(missing provider/org/repo) | **Error:** Command exits. Message indicates that `--provider`, `--organization`, and `--repository` are required when `--api-token` is used. | +| **Local** | `--api-token `
`--provider

`
`--organization `
`--repository ` | Fetches repository-specific configurations from Codacy API. Creates/overwrites `.codacy/codacy.yaml` and `tools-configs/` based on API response. `cli-config.yaml` is set to `mode: remote`. | +| **Remote** | No flags | **Error:** Command exits. Message indicates that the CLI is in 'remote' mode, and to reset, API flags (`--api-token`, `--provider`, etc.) must be provided. This prevents accidental reset to local defaults. | +| **Remote** | `--api-token `
(missing provider/org/repo) | **Error:** Command exits. Message indicates that `--provider`, `--organization`, and `--repository` are required when `--api-token` is used. | +| **Remote** | `--api-token `
`--provider

`
`--organization `
`--repository ` | Fetches repository-specific configurations from Codacy API. Creates/overwrites `.codacy/codacy.yaml` and `tools-configs/` based on API response. `cli-config.yaml` remains/is set to `mode: remote`. | +| File missing or unparseable (`.codacy/cli-config.yaml`) | No flags | (Defaults to Local mode) Resets configuration to local default settings. Creates/overwrites `.codacy/codacy.yaml` and `tools-configs/` with defaults. `cli-config.yaml` is set to `mode: local`. **User-facing warning printed to console and logged regarding `cli-config.yaml` issue.** | +| File missing or unparseable (`.codacy/cli-config.yaml`) | `--api-token `
(missing provider/org/repo) | (Defaults to Local mode) **Error:** Command exits. Message indicates that `--provider`, `--organization`, and `--repository` are required when `--api-token` is used. **User-facing warning printed to console and logged regarding `cli-config.yaml` issue.** | +| File missing or unparseable (`.codacy/cli-config.yaml`) | `--api-token `
`--provider

`
`--organization `
`--repository ` | (Defaults to Local mode) Fetches repository-specific configurations from Codacy API. Creates/overwrites `.codacy/codacy.yaml` and `tools-configs/` based on API response. `cli-config.yaml` is set to `mode: remote`. **User-facing warning printed to console and logged regarding `cli-config.yaml` issue.** | + +## Key Points + +* The `runConfigResetLogic` function determines whether to use local defaults or fetch from the API based purely on the presence of the `ApiToken` flag at the time of its execution. +* The `cliLocalMode` variable within `runConfigResetLogic` (which influences `CreateConfigurationFiles` and `CliConfigFileTemplate`) is set based on `len(flags.ApiToken) == 0`. + * If an API token is provided to `config reset`, the resulting `.codacy/cli-config.yaml` will always be set to `mode: remote`. + * If no token is provided, it will be set to `mode: local`. +* The validation logic in the `configResetCmd.Run` function occurs *before* `runConfigResetLogic` is called. This validation is responsible for: + * Informing the user with a console warning if `.codacy/cli-config.yaml` is missing or unparseable, then defaulting to 'local' mode for subsequent checks. + * Ensuring that if the (potentially defaulted) current CLI mode is "remote", an API token *must* be supplied to proceed with the reset. + * Ensuring that if an API token *is* supplied, then `--provider`, `--organization`, and `--repository` flags must also be supplied. + +This table should cover the main operational flows and error conditions for the `config reset` command based on the implemented logic. \ No newline at end of file