Repository: codacy/codacy-cli-v2
Pull request #121: refactor: Take default versions from yaml declarations
Author: login: andrzej-janczak
Created: 2025-05-15T08:24:52Z | Merged: 2025-05-30T10:32:48Z

This pull request introduces significant changes to streamline runtime and tool management in the codebase by introducing a plugin-based architecture. The most important updates include the addition of a `PluginManager` for handling runtime and tool configurations, refactoring runtime installation logic to use this new system, and simplifying runtime and tool version management. Below is a breakdown of the key changes:

### Plugin-Based Architecture for Runtimes and Tools:
* Added a new `PluginManager` in `plugins/plugin_manager.go` to manage runtime and tool plugins. This provides methods for retrieving runtime configurations, tool configurations, default versions, and runtime dependencies.
* Updated the `processRuntime` function in `plugins/runtime-utils.go` to use the `PluginManager` for retrieving runtime configurations and mapping architecture/OS values. This simplifies runtime processing and makes it more modular. [[1]](diffhunk://#diff-8e5534f73912cbdd48e0ad69a259fbf61eea3c7db43cb6f93ef6e22d9e0c79d3L106-R93) [[2]](diffhunk://#diff-8e5534f73912cbdd48e0ad69a259fbf61eea3c7db43cb6f93ef6e22d9e0c79d3R124-R128)

### Refactoring Runtime Installation Logic:
* Replaced the `isRuntimeInstalled` function with `Config.IsRuntimeInstalled` to centralize runtime installation checks and improve testability. [[1]](diffhunk://#diff-0cd43ffde439067edb85c8830d7f733726c218645668d0eb58cdc41971559c3bL50-R51) [[2]](diffhunk://#diff-0cd43ffde439067edb85c8830d7f733726c218645668d0eb58cdc41971559c3bL68-R67) [[3]](diffhunk://#diff-0cd43ffde439067edb85c8830d7f733726c218645668d0eb58cdc41971559c3bL79-L109)
* Updated tests in `config/runtimes-installer_test.go` to reflect the new runtime installation logic. [[1]](diffhunk://#diff-a3e0e5bc0acaf1202fbaa4b88773703198bd75d7b6339fb48078bdba8797f9f3L27-R34) [[2]](diffhunk://#diff-a3e0e5bc0acaf1202fbaa4b88773703198bd75d7b6339fb48078bdba8797f9f3L48-R55)

### Simplification of Runtime and Tool Version Management:
* Introduced methods in `PluginManager` to retrieve default versions for runtimes and tools (`GetRuntimeVersions`, `GetToolVersions`) and their runtime dependencies (`GetToolRuntimeDependencies`).
* Refactored the `configFileTemplate` function in `cmd/init.go` to dynamically fetch runtime and tool versions using the `PluginManager`. This eliminates hardcoded version mappings and ensures consistency. [[1]](diffhunk://#diff-345cd3beb7d905a8bef99d83e99c62678bfd5101a26d30104ecc9320602072acR136-R194) [[2]](diffhunk://#diff-345cd3beb7d905a8bef99d83e99c62678bfd5101a26d30104ecc9320602072acL183-R277)

### Configuration File Updates:
* Updated `.codacy/codacy.yaml` to reorganize the list of runtimes and tools, ensuring a consistent order.
* Modified `.cursor/rules/cursor.mdc` to apply rules to all files (`globs: *.*`) and disable the `alwaysApply` flag.

### Code Cleanup and Minor Enhancements:
* Removed unused code in `plugins/runtime-utils.go`, such as the old `downloadConfig` and `isRuntimeInstalled` logic, to reduce redundancy. [[1]](diffhunk://#diff-8e5534f73912cbdd48e0ad69a259fbf61eea3c7db43cb6f93ef6e22d9e0c79d3L34-R34) [[2]](diffhunk://#diff-8e5534f73912cbdd48e0ad69a259fbf61eea3c7db43cb6f93ef6e22d9e0c79d3L169-R140)
* Added new imports in `cmd/init.go` (`"sort"` and `"codacy/cli-v2/plugins"`) to support sorting and plugin integration. [[1]](diffhunk://#diff-345cd3beb7d905a8bef99d83e99c62678bfd5101a26d30104ecc9320602072acR6) [[2]](diffhunk://#diff-345cd3beb7d905a8bef99d83e99c62678bfd5101a26d30104ecc9320602072acR19)- Modify cursor rule to disable alwaysApply


Diff:
diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml
index 74433438..e72abf26 100644
--- a/.codacy/codacy.yaml
+++ b/.codacy/codacy.yaml
@@ -1,14 +1,14 @@
 runtimes:
-    - node@22.2.0
-    - python@3.11.11
     - dart@3.7.2
     - java@17.0.10
+    - node@22.2.0
+    - python@3.11.11
 tools:
+    - codacy-enigma-cli@0.0.1-main.8.49310c3
+    - dartanalyzer@3.7.2
     - eslint@8.57.0
-    - trivy@0.59.1
-    - pylint@3.3.6
+    - lizard@1.17.19
     - pmd@6.55.0
+    - pylint@3.3.6
     - semgrep@1.78.0
-    - dartanalyzer@3.7.2
-    - lizard@1.17.19
-    - codacy-enigma-cli@0.0.1-main.8.49310c3
\ No newline at end of file
+    - trivy@0.59.1
diff --git a/.github/workflows/it-test.yml b/.github/workflows/it-test.yml
index 45c45b4a..c92fee52 100644
--- a/.github/workflows/it-test.yml
+++ b/.github/workflows/it-test.yml
@@ -40,12 +40,6 @@ jobs:
         with:
           fetch-depth: 0  # Needed for git history
 
-      - name: Set up Go
-        uses: actions/setup-go@v5
-        with:
-          go-version: '1.21'
-          cache: true
-
       - name: Download CLI binaries
         uses: actions/download-artifact@v4
         with:
diff --git a/cmd/init.go b/cmd/init.go
index 8bc3011f..dd7b1698 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -4,6 +4,7 @@ import (
 	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"
@@ -12,6 +13,7 @@ import (
 	"log"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -151,27 +153,40 @@ func createConfigurationFiles(tools []domain.Tool, cliLocalMode bool) error {
 	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
-	needsNode := false
-	needsPython := false
-	needsDart := false
-	needsJava := false
-
-	// Default versions
-	defaultVersions := map[string]string{
-		ESLint:       "8.57.0",
-		Trivy:        "0.59.1",
-		PyLint:       "3.3.6",
-		PMD:          "6.55.0",
-		DartAnalyzer: "3.7.2",
-		Semgrep:      "1.78.0",
-		Lizard:       "1.17.19",
-	}
+	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 {
@@ -179,18 +194,24 @@ func configFileTemplate(tools []domain.Tool) string {
 		if tool.Version != "" {
 			toolVersions[tool.Uuid] = tool.Version
 		} else {
-			toolVersions[tool.Uuid] = defaultVersions[tool.Uuid]
+			toolName := toolNameMap[tool.Uuid]
+			if defaultVersion, ok := defaultVersions[toolName]; ok {
+				toolVersions[tool.Uuid] = defaultVersion
+			}
 		}
 
-		// Check if tool needs a runtime
-		if tool.Uuid == ESLint {
-			needsNode = true
-		} else if tool.Uuid == PyLint || tool.Uuid == Lizard {
-			needsPython = true
-		} else if tool.Uuid == DartAnalyzer {
-			needsDart = true
-		} else if tool.Uuid == PMD {
-			needsJava = true
+		// 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
+				}
+			}
 		}
 	}
 
@@ -200,55 +221,101 @@ func configFileTemplate(tools []domain.Tool) string {
 
 	// Only include runtimes needed by the enabled tools
 	if len(tools) > 0 {
-		if needsNode {
-			sb.WriteString("    - node@22.2.0\n")
+		// Create a sorted slice of runtimes
+		var sortedRuntimes []string
+		for runtime := range neededRuntimes {
+			sortedRuntimes = append(sortedRuntimes, runtime)
 		}
-		if needsPython {
-			sb.WriteString("    - python@3.11.11\n")
+		sort.Strings(sortedRuntimes)
+
+		// Write sorted runtimes
+		for _, runtime := range sortedRuntimes {
+			sb.WriteString(fmt.Sprintf("    - %s@%s\n", runtime, runtimeVersions[runtime]))
 		}
-		if needsDart {
-			sb.WriteString("    - dart@3.7.2\n")
+	} 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()
 		}
-		if needsJava {
-			sb.WriteString("    - java@17.0.10\n")
+
+		// 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]))
 		}
-	} else {
-		// In local mode with no tools specified, include all runtimes
-		sb.WriteString("    - node@22.2.0\n")
-		sb.WriteString("    - python@3.11.11\n")
-		sb.WriteString("    - dart@3.7.2\n")
-		sb.WriteString("    - java@17.0.10\n")
 	}
 
 	sb.WriteString("tools:\n")
 
 	// If we have tools from the API (enabled tools), use only those
 	if len(tools) > 0 {
-		// Add only the tools that are in the API response (enabled tools)
-		uuidToName := map[string]string{
-			ESLint:       "eslint",
-			Trivy:        "trivy",
-			PyLint:       "pylint",
-			PMD:          "pmd",
-			DartAnalyzer: "dartanalyzer",
-			Semgrep:      "semgrep",
-			Lizard:       "lizard",
-		}
-
-		for uuid, name := range uuidToName {
+		// Create a sorted slice of tool names
+		var sortedTools []string
+		for uuid, name := range toolNameMap {
 			if toolsMap[uuid] {
-				sb.WriteString(fmt.Sprintf("    - %s@%s\n", name, toolVersions[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 defaults
-		sb.WriteString(fmt.Sprintf("    - eslint@%s\n", defaultVersions[ESLint]))
-		sb.WriteString(fmt.Sprintf("    - trivy@%s\n", defaultVersions[Trivy]))
-		sb.WriteString(fmt.Sprintf("    - pylint@%s\n", defaultVersions[PyLint]))
-		sb.WriteString(fmt.Sprintf("    - pmd@%s\n", defaultVersions[PMD]))
-		sb.WriteString(fmt.Sprintf("    - dartanalyzer@%s\n", defaultVersions[DartAnalyzer]))
-		sb.WriteString(fmt.Sprintf("    - semgrep@%s\n", defaultVersions[Semgrep]))
-		sb.WriteString(fmt.Sprintf("    - lizard@%s\n", defaultVersions[Lizard]))
+		// 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()
diff --git a/cmd/init_test.go b/cmd/init_test.go
index 417eb926..e950d3b1 100644
--- a/cmd/init_test.go
+++ b/cmd/init_test.go
@@ -24,7 +24,7 @@ func TestConfigFileTemplate(t *testing.T) {
 			expected: []string{
 				"node@22.2.0",
 				"python@3.11.11",
-				"eslint@8.57.0",
+				"eslint@9.3.0",
 				"trivy@0.59.1",
 				"pylint@3.3.6",
 				"pmd@6.55.0",
diff --git a/config/runtimes-installer.go b/config/runtimes-installer.go
index ef89938b..118257ba 100644
--- a/config/runtimes-installer.go
+++ b/config/runtimes-installer.go
@@ -47,9 +47,8 @@ func InstallRuntimes(config *ConfigType) error {
 
 // InstallRuntime installs a specific runtime
 func InstallRuntime(name string, runtimeInfo *plugins.RuntimeInfo) error {
-
-	// Check if the runtime is already installed
-	if isRuntimeInstalled(runtimeInfo) {
+	// Skip if already installed
+	if Config.IsRuntimeInstalled(name, runtimeInfo) {
 		logger.Info("Runtime already installed", logrus.Fields{
 			"runtime": name,
 			"version": runtimeInfo.Version,
@@ -65,7 +64,7 @@ func InstallRuntime(name string, runtimeInfo *plugins.RuntimeInfo) error {
 	}
 
 	// Verify that the runtime binaries are available
-	if !isRuntimeInstalled(runtimeInfo) {
+	if !Config.IsRuntimeInstalled(name, runtimeInfo) {
 		logger.Error("Runtime binaries not found after extraction", logrus.Fields{
 			"runtime": name,
 			"version": runtimeInfo.Version,
@@ -76,37 +75,6 @@ func InstallRuntime(name string, runtimeInfo *plugins.RuntimeInfo) error {
 	return nil
 }
 
-// isRuntimeInstalled checks if a runtime is already installed by checking for the binary
-func isRuntimeInstalled(runtimeInfo *plugins.RuntimeInfo) bool {
-	// If there are no binaries, check the install directory
-	if len(runtimeInfo.Binaries) == 0 {
-		_, err := os.Stat(runtimeInfo.InstallDir)
-		if err != nil {
-			logger.Debug("Runtime install directory not found", logrus.Fields{
-				"directory": runtimeInfo.InstallDir,
-				"error":     err,
-			})
-			return false
-		}
-		return true
-	}
-
-	// Check if at least one binary exists
-	for binaryName, binaryPath := range runtimeInfo.Binaries {
-		_, err := os.Stat(binaryPath)
-		if err != nil {
-			logger.Debug("Runtime binary not found", logrus.Fields{
-				"binary": binaryName,
-				"path":   binaryPath,
-				"error":  err,
-			})
-			return false
-		}
-	}
-
-	return true
-}
-
 // downloadAndExtractRuntime downloads and extracts a runtime
 func downloadAndExtractRuntime(runtimeInfo *plugins.RuntimeInfo) error {
 	// Create a file name for the downloaded archive
diff --git a/config/runtimes-installer_test.go b/config/runtimes-installer_test.go
index 1e2c4859..5ec001c2 100644
--- a/config/runtimes-installer_test.go
+++ b/config/runtimes-installer_test.go
@@ -24,14 +24,14 @@ func TestIsRuntimeInstalled(t *testing.T) {
 	}
 
 	// Test when the install directory doesn't exist
-	assert.False(t, isRuntimeInstalled(runtimeInfoNoBinaries))
+	assert.False(t, Config.IsRuntimeInstalled("test-runtime", runtimeInfoNoBinaries))
 
 	// Create the install directory
 	err = os.MkdirAll(runtimeInfoNoBinaries.InstallDir, utils.DefaultDirPerms)
 	assert.NoError(t, err)
 
 	// Test when the install directory exists
-	assert.True(t, isRuntimeInstalled(runtimeInfoNoBinaries))
+	assert.True(t, Config.IsRuntimeInstalled("test-runtime", runtimeInfoNoBinaries))
 
 	// Create a mock RuntimeInfo with binaries
 	binPath := filepath.Join(tempDir, "test-runtime-bin")
@@ -45,12 +45,12 @@ func TestIsRuntimeInstalled(t *testing.T) {
 	}
 
 	// Test when the binary doesn't exist
-	assert.False(t, isRuntimeInstalled(runtimeInfoWithBinaries))
+	assert.False(t, Config.IsRuntimeInstalled("test-runtime", runtimeInfoWithBinaries))
 
 	// Create a mock binary file
 	_, err = os.Create(binPath)
 	assert.NoError(t, err)
 
 	// Test when the binary exists
-	assert.True(t, isRuntimeInstalled(runtimeInfoWithBinaries))
+	assert.True(t, Config.IsRuntimeInstalled("test-runtime", runtimeInfoWithBinaries))
 }
diff --git a/config/tools-installer.go b/config/tools-installer.go
index 75466fd7..923ea7b7 100644
--- a/config/tools-installer.go
+++ b/config/tools-installer.go
@@ -58,7 +58,7 @@ func InstallTools(config *ConfigType, registry string) error {
 // InstallTool installs a specific tool
 func InstallTool(name string, toolInfo *plugins.ToolInfo, registry string) error {
 	// Check if the tool is already installed
-	if isToolInstalled(toolInfo) {
+	if Config.IsToolInstalled(name, toolInfo) {
 		logger.Info("Tool already installed", logrus.Fields{
 			"tool":    name,
 			"version": toolInfo.Version,
diff --git a/integration-tests/init-with-token/expected/codacy.yaml b/integration-tests/init-with-token/expected/codacy.yaml
index 1992d0b5..79d60ac8 100644
--- a/integration-tests/init-with-token/expected/codacy.yaml
+++ b/integration-tests/init-with-token/expected/codacy.yaml
@@ -1,11 +1,11 @@
 runtimes:
+    - java@17.0.10
     - node@22.2.0
     - python@3.11.11
-    - java@17.0.10
 tools:
     - eslint@8.57.0
-    - trivy@0.59.1
-    - pylint@3.3.6
+    - lizard@1.17.19
     - pmd@6.55.0
+    - pylint@3.3.7
     - semgrep@1.78.0
-    - lizard@1.17.19
\ No newline at end of file
+    - trivy@0.59.1
diff --git a/integration-tests/init-without-token/expected/codacy.yaml b/integration-tests/init-without-token/expected/codacy.yaml
index 5b53c4fa..88396369 100644
--- a/integration-tests/init-without-token/expected/codacy.yaml
+++ b/integration-tests/init-without-token/expected/codacy.yaml
@@ -1,13 +1,13 @@
 runtimes:
-    - node@22.2.0
-    - python@3.11.11
     - dart@3.7.2
     - java@17.0.10
+    - node@22.2.0
+    - python@3.11.11
 tools:
-    - eslint@8.57.0
-    - trivy@0.59.1
-    - pylint@3.3.6
-    - pmd@6.55.0
     - dartanalyzer@3.7.2
-    - semgrep@1.78.0
+    - eslint@9.3.0
     - lizard@1.17.19
+    - pmd@6.55.0
+    - pylint@3.3.6
+    - semgrep@1.78.0
+    - trivy@0.59.1
diff --git a/integration-tests/run.ps1 b/integration-tests/run.ps1
index 78051181..8444e827 100644
--- a/integration-tests/run.ps1
+++ b/integration-tests/run.ps1
@@ -5,8 +5,13 @@ $ErrorActionPreference = "Stop"
 $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
 $CLI_PATH = Join-Path (Get-Location) "cli-v2.exe"
 
+Write-Host "=== Environment Information ==="
 Write-Host "Script directory: $SCRIPT_DIR"
 Write-Host "Current working directory: $(Get-Location)"
+Write-Host "CLI Path: $CLI_PATH"
+Write-Host "OS: $([System.Environment]::OSVersion.Platform)"
+Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
+Write-Host "==============================`n"
 
 # Check if API token is provided for token-based test
 if (-not $env:CODACY_API_TOKEN) {
@@ -17,6 +22,7 @@ if (-not $env:CODACY_API_TOKEN) {
 function Normalize-Config {
     param ([string]$file)
     
+    Write-Host "Normalizing config file: $file"
     $ext = [System.IO.Path]::GetExtension($file).TrimStart('.')
     
     switch ($ext) {
@@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
+        Write-Host "Expected file: $($_.FullName)"
+        Write-Host "Actual file: $actualFile"
         
         if (-not (Test-Path $actualFile)) {
-            Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ $label/$relativePath does not exist in actual output"
             Write-Host "Expected: $($_.FullName)"
             Write-Host "Actual should be: $actualFile"
+            Write-Host "Current directory structure:"
+            Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+                Write-Host "  $($_.FullName)"
+            }
             exit 1
         }
         
+        Write-Host "Comparing file contents..."
         $expectedContent = Normalize-Config $_.FullName
         $actualContent = Normalize-Config $actualFile
         
         # Compare line by line
         $diff = Compare-Object $expectedContent $actualContent -PassThru
         if ($diff) {
-            Write-Host "❌ $label/$($_.Name) does not match expected"
+            Write-Host "❌ $label/$relativePath does not match expected"
             Write-Host "=== Expected (normalized) ==="
             $expectedContent
             Write-Host "=== Actual (normalized) ==="
@@ -107,21 +146,31 @@ function Compare-Files {
             Write-Host "==================="
             exit 1
         }
-        Write-Host "✅ $label/$($_.Name) matches expected"
+        Write-Host "✅ $label/$relativePath matches expected"
     }
     
     # Compare subdirectories
     Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object {
-        $actualSubDir = Join-Path $actualDir $_.Name
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualSubDir = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking subdirectory: $relativePath"
+        Write-Host "Expected dir: $($_.FullName)"
+        Write-Host "Actual dir: $actualSubDir"
         
         if (-not (Test-Path $actualSubDir)) {
-            Write-Host "❌ Directory $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ Directory $label/$relativePath does not exist in actual output"
             Write-Host "Expected: $($_.FullName)"
             Write-Host "Actual should be: $actualSubDir"
+            Write-Host "Current directory structure:"
+            Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+                Write-Host "  $($_.FullName)"
+            }
             exit 1
         }
-        Compare-Files $_.FullName $actualSubDir "$label/$($_.Name)"
+        Compare-Files $_.FullName $actualSubDir "$label/$relativePath"
     }
+    
+    Write-Host "`n=== Directory Comparison Complete ==="
 }
 
 function Run-InitTest {
@@ -131,7 +180,10 @@ function Run-InitTest {
         [bool]$useToken
     )
     
-    Write-Host "Running test: $testName"
+    Write-Host "`n=== Running Test: $testName ==="
+    Write-Host "Test directory: $testDir"
+    Write-Host "Using token: $useToken"
+    
     if (-not (Test-Path $testDir)) {
         Write-Host "❌ Test directory does not exist: $testDir"
         exit 1
@@ -139,33 +191,53 @@ function Run-InitTest {
     
     $originalLocation = Get-Location
     try {
+        Write-Host "Changing to test directory: $testDir"
         Set-Location $testDir
-        if (Test-Path ".codacy") { Remove-Item -Recurse -Force ".codacy" }
+        Write-Host "Current location: $(Get-Location)"
+        
+        if (Test-Path ".codacy") {
+            Write-Host "Removing existing .codacy directory"
+            Remove-Item -Recurse -Force ".codacy"
+        }
         
         if ($useToken) {
             if (-not $env:CODACY_API_TOKEN) {
                 Write-Host "❌ Skipping token-based test: CODACY_API_TOKEN not set"
                 return
             }
+            Write-Host "Running CLI with token..."
             & $CLI_PATH init --api-token $env:CODACY_API_TOKEN --organization troubleshoot-codacy-dev --provider gh --repository codacy-cli-test
         } else {
+            Write-Host "Running CLI without token..."
             & $CLI_PATH init
         }
         
+        Write-Host "`nVerifying directory structure after CLI execution:"
+        Get-ChildItem -Recurse | ForEach-Object {
+            Write-Host "  $($_.FullName)"
+        }
+        
         Compare-Files "expected" ".codacy" "Test $testName"
         Write-Host "✅ Test $testName completed successfully"
         Write-Host "----------------------------------------"
     }
+    catch {
+        Write-Host "❌ Error during test execution:"
+        Write-Host $_.Exception.Message
+        Write-Host $_.ScriptStackTrace
+        exit 1
+    }
     finally {
+        Write-Host "Returning to original location: $originalLocation"
         Set-Location $originalLocation
     }
 }
 
 # Run tests
-Write-Host "Starting integration tests..."
+Write-Host "`n=== Starting Integration Tests ==="
 Write-Host "----------------------------------------"
 
 Run-InitTest (Join-Path $SCRIPT_DIR "init-without-token") "init-without-token" $false
 Run-InitTest (Join-Path $SCRIPT_DIR "init-with-token") "init-with-token" $true
 
-Write-Host "All tests completed successfully! 🎉" 
\ No newline at end of file
+Write-Host "`nAll tests completed successfully! 🎉" 
\ No newline at end of file
diff --git a/plugins/plugin_manager.go b/plugins/plugin_manager.go
new file mode 100644
index 00000000..76040b94
--- /dev/null
+++ b/plugins/plugin_manager.go
@@ -0,0 +1,148 @@
+package plugins
+
+import (
+	"fmt"
+
+	"gopkg.in/yaml.v3"
+)
+
+// runtimePlugin represents a runtime plugin with methods to interact with it
+type runtimePlugin struct {
+	Config     PluginConfig
+	ConfigPath string
+}
+
+// PluginManager manages runtime and tool plugins
+type PluginManager struct {
+	runtimePlugins map[string]*runtimePlugin
+}
+
+var pluginManager *PluginManager
+
+// GetPluginManager returns the singleton instance of PluginManager
+func GetPluginManager() *PluginManager {
+	if pluginManager == nil {
+		pluginManager = &PluginManager{
+			runtimePlugins: make(map[string]*runtimePlugin),
+		}
+	}
+	return pluginManager
+}
+
+// GetRuntimeConfig returns the plugin configuration for a runtime
+func (pm *PluginManager) GetRuntimeConfig(name string) (PluginConfig, error) {
+	plugin, err := loadPlugin(name)
+	if err != nil {
+		return PluginConfig{}, err
+	}
+	return plugin.Config, nil
+}
+
+// GetToolConfig returns the plugin configuration for a tool
+func (pm *PluginManager) GetToolConfig(name string) (ToolPluginConfig, error) {
+	// Always use forward slashes for embedded filesystem paths (for windows support)
+	pluginPath := fmt.Sprintf(toolPluginYamlPathTemplate, name)
+
+	// Read from embedded filesystem
+	data, err := toolsFS.ReadFile(pluginPath)
+	if err != nil {
+		return ToolPluginConfig{}, fmt.Errorf("error reading plugin.yaml for %s: %w", name, err)
+	}
+
+	var config ToolPluginConfig
+	err = yaml.Unmarshal(data, &config)
+	if err != nil {
+		return ToolPluginConfig{}, fmt.Errorf("error parsing plugin.yaml for %s: %w", name, err)
+	}
+
+	return config, nil
+}
+
+// GetRuntimeVersions returns a map of runtime names to their default versions
+func (pm *PluginManager) GetRuntimeVersions() map[string]string {
+	versions := make(map[string]string)
+	entries, err := pluginsFS.ReadDir("runtimes")
+	if err != nil {
+		return versions
+	}
+
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			continue
+		}
+
+		name := entry.Name()
+		plugin, err := loadPlugin(name)
+		if err != nil {
+			continue
+		}
+
+		versions[name] = plugin.Config.DefaultVersion
+	}
+
+	return versions
+}
+
+// GetToolVersions returns a map of tool names to their default versions
+func (pm *PluginManager) GetToolVersions() map[string]string {
+	versions := make(map[string]string)
+	entries, err := toolsFS.ReadDir("tools")
+	if err != nil {
+		return versions
+	}
+
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			continue
+		}
+
+		name := entry.Name()
+		pluginPath := fmt.Sprintf(toolPluginYamlPathTemplate, name)
+		data, err := toolsFS.ReadFile(pluginPath)
+		if err != nil {
+			continue
+		}
+
+		var config ToolPluginConfig
+		if err := yaml.Unmarshal(data, &config); err != nil {
+			continue
+		}
+
+		versions[name] = config.DefaultVersion
+	}
+
+	return versions
+}
+
+// GetToolRuntimeDependencies returns a map of tool names to their runtime dependencies
+func (pm *PluginManager) GetToolRuntimeDependencies() map[string]string {
+	dependencies := make(map[string]string)
+	entries, err := toolsFS.ReadDir("tools")
+	if err != nil {
+		return dependencies
+	}
+
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			continue
+		}
+
+		name := entry.Name()
+		pluginPath := fmt.Sprintf(toolPluginYamlPathTemplate, name)
+		data, err := toolsFS.ReadFile(pluginPath)
+		if err != nil {
+			continue
+		}
+
+		var config ToolPluginConfig
+		if err := yaml.Unmarshal(data, &config); err != nil {
+			continue
+		}
+
+		if config.Runtime != "" {
+			dependencies[name] = config.Runtime
+		}
+	}
+
+	return dependencies
+}
diff --git a/plugins/runtime-utils.go b/plugins/runtime-utils.go
index 2e56353e..048ea4d2 100644
--- a/plugins/runtime-utils.go
+++ b/plugins/runtime-utils.go
@@ -1,13 +1,11 @@
 package plugins
 
 import (
-	"bytes"
 	"embed"
 	"fmt"
 	"path"
 	"runtime"
 	"strings"
-	"text/template"
 
 	"gopkg.in/yaml.v3"
 )
@@ -29,60 +27,11 @@ type binaryPath struct {
 
 // pluginConfig holds the structure of the plugin.yaml file
 type pluginConfig struct {
-	Name        string         `yaml:"name"`
-	Description string         `yaml:"description"`
-	Download    downloadConfig `yaml:"download"`
-	Binaries    []binary       `yaml:"binaries"`
-}
-
-// downloadConfig holds the download configuration from the plugin.yaml
-type downloadConfig struct {
-	URLTemplate      string            `yaml:"url_template"`
-	FileNameTemplate string            `yaml:"file_name_template"`
-	Extension        extensionConfig   `yaml:"extension"`
-	ArchMapping      map[string]string `yaml:"arch_mapping"`
-	OSMapping        map[string]string `yaml:"os_mapping"`
-	ReleaseVersion   string            `yaml:"release_version"`
-}
-
-// extensionConfig defines the file extension based on OS
-type extensionConfig struct {
-	Windows string `yaml:"windows"`
-	Default string `yaml:"default"`
-}
-
-// templateData holds the data to be used in template substitution
-type templateData struct {
-	Version        string
-	MajorVersion   string
-	FileName       string
-	OS             string
-	Arch           string
-	Extension      string
-	ReleaseVersion string
-}
-
-// runtimePlugin represents a runtime plugin with methods to interact with it
-type runtimePlugin struct {
-	Config     pluginConfig
-	ConfigPath string
-}
-
-// RuntimeConfig represents configuration for a runtime
-type RuntimeConfig struct {
-	Name    string
-	Version string
-}
-
-// RuntimeInfo contains all processed information about a runtime
-type RuntimeInfo struct {
-	Name        string
-	Version     string
-	InstallDir  string
-	DownloadURL string
-	FileName    string
-	Extension   string
-	Binaries    map[string]string // Map of binary name to full path
+	Name           string         `yaml:"name"`
+	Description    string         `yaml:"description"`
+	Download       DownloadConfig `yaml:"download"`
+	Binaries       []binary       `yaml:"binaries"`
+	DefaultVersion string         `yaml:"default_version"`
 }
 
 // ProcessRuntimes processes a list of runtime configurations and returns a map of runtime information
@@ -103,28 +52,45 @@ func ProcessRuntimes(configs []RuntimeConfig, runtimesDir string) (map[string]*R
 
 // ProcessRuntime processes a single runtime configuration and returns detailed runtime info
 func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, error) {
-	plugin, err := loadPlugin(config.Name)
+	pluginConfig, err := GetPluginManager().GetRuntimeConfig(config.Name)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load plugin for runtime %s: %w", config.Name, err)
 	}
 
-	fileName := plugin.getFileName(config.Version)
-	extension := plugin.getExtension(runtime.GOOS)
-	installDir := plugin.getInstallationDirectoryPath(runtimesDir, config.Version)
+	// Map Go architecture and OS to runtime-specific values
+	mappedArch := GetMappedArch(pluginConfig.Download.ArchMapping, runtime.GOARCH)
+	mappedOS := GetMappedOS(pluginConfig.Download.OSMapping, runtime.GOOS)
+
+	// Get the appropriate extension
+	extension := GetExtension(pluginConfig.Download.Extension, runtime.GOOS)
+
+	// Get the filename using the template
+	fileName := GetFileName(pluginConfig.Download.FileNameTemplate, config.Version, mappedArch, runtime.GOOS)
+
+	// Get the download URL using the template
+	downloadURL := GetDownloadURL(pluginConfig.Download.URLTemplate, fileName, config.Version, mappedArch, mappedOS, extension, pluginConfig.Download.ReleaseVersion)
+
+	// For Python, we want to use a simpler directory structure
+	var installDir string
+	if config.Name == "python" {
+		installDir = path.Join(runtimesDir, "python")
+	} else {
+		installDir = path.Join(runtimesDir, fileName)
+	}
 
 	// Create RuntimeInfo with essential information
 	info := &RuntimeInfo{
 		Name:        config.Name,
 		Version:     config.Version,
 		InstallDir:  installDir,
-		DownloadURL: plugin.getDownloadURL(config.Version),
+		DownloadURL: downloadURL,
 		FileName:    fileName,
 		Extension:   extension,
 		Binaries:    make(map[string]string),
 	}
 
 	// Process binary paths
-	for _, binary := range plugin.Config.Binaries {
+	for _, binary := range pluginConfig.Binaries {
 		var binaryPath string
 
 		switch path := binary.Path.(type) {
@@ -155,6 +121,11 @@ func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, err
 	return info, nil
 }
 
+// GetRuntimeVersions returns a map of runtime names to their default versions
+func GetRuntimeVersions() map[string]string {
+	return GetPluginManager().GetRuntimeVersions()
+}
+
 // LoadPlugin loads a plugin configuration from the specified plugin directory
 func loadPlugin(runtimeName string) (*runtimePlugin, error) {
 	// Always use forward slashes for embedded filesystem paths (for windows support)
@@ -166,7 +137,7 @@ func loadPlugin(runtimeName string) (*runtimePlugin, error) {
 		return nil, fmt.Errorf("error reading plugin.yaml: %w", err)
 	}
 
-	var config pluginConfig
+	var config PluginConfig
 	err = yaml.Unmarshal(data, &config)
 	if err != nil {
 		return nil, fmt.Errorf("error parsing plugin.yaml: %w", err)
@@ -177,132 +148,3 @@ func loadPlugin(runtimeName string) (*runtimePlugin, error) {
 		ConfigPath: pluginPath,
 	}, nil
 }
-
-// GetMappedArch returns the architecture mapping for the current system
-func (p *runtimePlugin) getMappedArch(goarch string) string {
-	// Check if there's a mapping for this architecture
-	if mappedArch, ok := p.Config.Download.ArchMapping[goarch]; ok {
-		return mappedArch
-	}
-	// Return the original architecture if no mapping exists
-	return goarch
-}
-
-// GetExtension returns the appropriate file extension based on the OS
-func (p *runtimePlugin) getExtension(goos string) string {
-	if goos == "windows" {
-		return p.Config.Download.Extension.Windows
-	}
-	return p.Config.Download.Extension.Default
-}
-
-// getMajorVersion extracts the major version from a version string (e.g. "17.0.10" -> "17")
-func (p *runtimePlugin) getMajorVersion(version string) string {
-	if idx := strings.Index(version, "."); idx != -1 {
-		return version[:idx]
-	}
-	return version
-}
-
-// GetFileName generates the filename based on the template in plugin.yaml
-func (p *runtimePlugin) getFileName(version string) string {
-	goos := runtime.GOOS
-	goarch := runtime.GOARCH
-
-	// Map Go architecture and OS to runtime-specific values
-	mappedArch := p.getMappedArch(goarch)
-	mappedOS := p.getMappedOS(goos)
-	releaseVersion := p.getReleaseVersion()
-
-	// Prepare template data
-	data := templateData{
-		Version:        version,
-		MajorVersion:   p.getMajorVersion(version),
-		OS:             mappedOS,
-		Arch:           mappedArch,
-		ReleaseVersion: releaseVersion,
-	}
-
-	// Execute template substitution for filename
-	tmpl, err := template.New("filename").Parse(p.Config.Download.FileNameTemplate)
-	if err != nil {
-		return ""
-	}
-
-	var buf bytes.Buffer
-	err = tmpl.Execute(&buf, data)
-	if err != nil {
-		return ""
-	}
-
-	return buf.String()
-}
-
-// GetReleaseVersion returns the release version from the plugin configuration
-func (p *runtimePlugin) getReleaseVersion() string {
-	return p.Config.Download.ReleaseVersion
-}
-
-// GetDownloadURL generates the download URL based on the template in plugin.yaml
-func (p *runtimePlugin) getDownloadURL(version string) string {
-	goos := runtime.GOOS
-	goarch := runtime.GOARCH
-
-	// Map Go architecture and OS to runtime-specific values
-	mappedArch := p.getMappedArch(goarch)
-	mappedOS := p.getMappedOS(goos)
-
-	// Get the appropriate extension
-	extension := p.getExtension(goos)
-
-	// Get the filename
-	fileName := p.getFileName(version)
-
-	releaseVersion := p.getReleaseVersion()
-
-	// Prepare template data
-	data := templateData{
-		Version:        version,
-		MajorVersion:   p.getMajorVersion(version),
-		FileName:       fileName,
-		OS:             mappedOS,
-		Arch:           mappedArch,
-		Extension:      extension,
-		ReleaseVersion: releaseVersion,
-	}
-
-	// Execute template substitution for URL
-	tmpl, err := template.New("url").Parse(p.Config.Download.URLTemplate)
-	if err != nil {
-		return ""
-	}
-
-	var buf bytes.Buffer
-	err = tmpl.Execute(&buf, data)
-	if err != nil {
-		return ""
-	}
-
-	return buf.String()
-}
-
-// GetInstallationDirectoryPath returns the installation directory path for the runtime
-func (p *runtimePlugin) getInstallationDirectoryPath(runtimesDir string, version string) string {
-	// For Python, we want to use a simpler directory structure
-	if p.Config.Name == "python" {
-		return path.Join(runtimesDir, "python")
-	}
-	// For other runtimes, keep using the filename-based directory
-	fileName := p.getFileName(version)
-	return path.Join(runtimesDir, fileName)
-}
-
-// GetMappedOS returns the OS mapping for the current system
-func (p *runtimePlugin) getMappedOS(goos string) string {
-	// Check if there's a mapping for this OS
-	if mappedOS, ok := p.Config.Download.OSMapping[goos]; ok {
-		return mappedOS
-	}
-	// Return the original OS if no mapping exists
-	return goos
-}
diff --git a/plugins/runtimes/dart/plugin.yaml b/plugins/runtimes/dart/plugin.yaml
index b040667a..192cb96c 100644
--- a/plugins/runtimes/dart/plugin.yaml
+++ b/plugins/runtimes/dart/plugin.yaml
@@ -1,5 +1,6 @@
 name: dart
 description: Dart runtime
+default_version: "3.7.2"
 download:
   url_template: "https://storage.googleapis.com/dart-archive/channels/stable/release/{{.Version}}/sdk/dartsdk-{{.OS}}-{{.Arch}}-release.{{.Extension}}"
   file_name_template: "dart-sdk"
diff --git a/plugins/runtimes/flutter/plugin.yaml b/plugins/runtimes/flutter/plugin.yaml
index 0d0f0bc9..8146bea3 100644
--- a/plugins/runtimes/flutter/plugin.yaml
+++ b/plugins/runtimes/flutter/plugin.yaml
@@ -1,5 +1,6 @@
 name: flutter
 description: Dart Flutterruntime
+default_version: "3.7.2"
 download:
   url_template: "https://storage.googleapis.com/flutter_infra_release/releases/stable/{{.OS}}/flutter_{{.OS}}_{{.Arch}}_{{.Version}}-stable.zip"
   file_name_template: "flutter"
diff --git a/plugins/runtimes/java/plugin.yaml b/plugins/runtimes/java/plugin.yaml
index e1d7350b..311c7018 100644
--- a/plugins/runtimes/java/plugin.yaml
+++ b/plugins/runtimes/java/plugin.yaml
@@ -1,5 +1,6 @@
 name: java
 description: Java Runtime Environment
+default_version: "17.0.10"
 download:
   url_template: "https://github.com/adoptium/temurin{{.MajorVersion}}-binaries/releases/download/jdk-{{.Version}}%2B7/OpenJDK{{.MajorVersion}}U-jdk_{{.Arch}}_{{.OS}}_hotspot_{{.Version}}_7.{{.Extension}}"
   file_name_template: "jdk-{{.Version}}+7"
diff --git a/plugins/runtimes/node/plugin.yaml b/plugins/runtimes/node/plugin.yaml
index 8573b835..26201cb8 100644
--- a/plugins/runtimes/node/plugin.yaml
+++ b/plugins/runtimes/node/plugin.yaml
@@ -1,5 +1,6 @@
 name: node
 description: Node.js JavaScript runtime
+default_version: "22.2.0"
 download:
   url_template: "https://nodejs.org/dist/v{{.Version}}/{{.FileName}}.{{.Extension}}"
   file_name_template: "node-v{{.Version}}-{{.OS}}-{{.Arch}}"
diff --git a/plugins/runtimes/python/plugin.yaml b/plugins/runtimes/python/plugin.yaml
index 1d495858..8cb5c93c 100644
--- a/plugins/runtimes/python/plugin.yaml
+++ b/plugins/runtimes/python/plugin.yaml
@@ -1,5 +1,6 @@
 name: python
 description: Python runtime
+default_version: "3.11.11"
 download:
   url_template: "https://github.com/astral-sh/python-build-standalone/releases/download/{{.ReleaseVersion}}/cpython-{{.Version}}+{{.ReleaseVersion}}-{{.Arch}}-{{.OS}}-install_only.{{.Extension}}"
   file_name_template: "cpython-{{.Version}}+{{.ReleaseVersion}}-{{.Arch}}-{{.OS}}"
diff --git a/plugins/shared.go b/plugins/shared.go
new file mode 100644
index 00000000..1e418bde
--- /dev/null
+++ b/plugins/shared.go
@@ -0,0 +1,156 @@
+package plugins
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+)
+
+// ExtensionConfig defines the file extension based on OS
+type ExtensionConfig struct {
+	Windows string `yaml:"windows"`
+	Default string `yaml:"default"`
+}
+
+// DownloadConfig holds the download configuration from the plugin.yaml
+type DownloadConfig struct {
+	URLTemplate      string            `yaml:"url_template"`
+	FileNameTemplate string            `yaml:"file_name_template"`
+	Extension        ExtensionConfig   `yaml:"extension"`
+	ArchMapping      map[string]string `yaml:"arch_mapping"`
+	OSMapping        map[string]string `yaml:"os_mapping"`
+	ReleaseVersion   string            `yaml:"release_version,omitempty"`
+}
+
+// Binary represents a binary executable provided by the runtime or tool
+type Binary struct {
+	Name string      `yaml:"name"`
+	Path interface{} `yaml:"path"` // Can be either string or map[string]string
+}
+
+// PluginConfig holds the structure of the plugin.yaml file
+type PluginConfig struct {
+	Name           string         `yaml:"name"`
+	Description    string         `yaml:"description"`
+	Download       DownloadConfig `yaml:"download"`
+	Binaries       []Binary       `yaml:"binaries"`
+	DefaultVersion string         `yaml:"default_version"`
+}
+
+// RuntimeConfig represents configuration for a runtime
+type RuntimeConfig struct {
+	Name    string
+	Version string
+}
+
+// RuntimeInfo contains all processed information about a runtime
+type RuntimeInfo struct {
+	Name        string
+	Version     string
+	InstallDir  string
+	DownloadURL string
+	FileName    string
+	Extension   string
+	Binaries    map[string]string // Map of binary name to full path
+}
+
+// templateData holds the data to be used in template substitution
+type templateData struct {
+	Version        string
+	MajorVersion   string
+	FileName       string
+	OS             string
+	Arch           string
+	Extension      string
+	ReleaseVersion string
+}
+
+// GetMappedArch returns the architecture mapping for the current system
+func GetMappedArch(archMapping map[string]string, goarch string) string {
+	// Check if there's a mapping for this architecture
+	if mappedArch, ok := archMapping[goarch]; ok {
+		return mappedArch
+	}
+	// Return the original architecture if no mapping exists
+	return goarch
+}
+
+// GetExtension returns the appropriate file extension based on the OS
+func GetExtension(extension ExtensionConfig, goos string) string {
+	if goos == "windows" {
+		return extension.Windows
+	}
+	return extension.Default
+}
+
+// GetMajorVersion extracts the major version from a version string (e.g. "17.0.10" -> "17")
+func GetMajorVersion(version string) string {
+	if idx := strings.Index(version, "."); idx != -1 {
+		return version[:idx]
+	}
+	return version
+}
+
+// GetFileName generates the filename based on the template
+func GetFileName(fileNameTemplate string, version string, mappedArch string, goos string) string {
+	// Prepare template data
+	data := templateData{
+		Version:      version,
+		MajorVersion: GetMajorVersion(version),
+		OS:           goos,
+		Arch:         mappedArch,
+	}
+
+	// Execute template substitution for filename
+	tmpl, err := template.New("filename").Parse(fileNameTemplate)
+	if err != nil {
+		return ""
+	}
+
+	var buf bytes.Buffer
+	err = tmpl.Execute(&buf, data)
+	if err != nil {
+		return ""
+	}
+
+	return buf.String()
+}
+
+// GetDownloadURL generates the download URL based on the template
+func GetDownloadURL(urlTemplate string, fileName string, version string, mappedArch string, mappedOS string, extension string, releaseVersion string) string {
+	// Prepare template data
+	data := templateData{
+		Version:        version,
+		MajorVersion:   GetMajorVersion(version),
+		FileName:       fileName,
+		OS:             mappedOS,
+		Arch:           mappedArch,
+		Extension:      extension,
+		ReleaseVersion: releaseVersion,
+	}
+
+	// Execute template substitution for URL
+	tmpl, err := template.New("url").Parse(urlTemplate)
+	if err != nil {
+		return ""
+	}
+
+	var buf bytes.Buffer
+	err = tmpl.Execute(&buf, data)
+	if err != nil {
+		return ""
+	}
+
+	url := buf.String()
+	return url
+}
+
+// GetMappedOS returns the OS mapping for the current system
+func GetMappedOS(osMapping map[string]string, goos string) string {
+	// Check if there's a mapping for this OS
+	if mappedOS, ok := osMapping[goos]; ok {
+		return mappedOS
+	}
+	// Return the original OS if no mapping exists
+	return goos
+}
diff --git a/plugins/tool-utils.go b/plugins/tool-utils.go
index f1f8cd3b..580158c5 100644
--- a/plugins/tool-utils.go
+++ b/plugins/tool-utils.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"os"
 	"path"
-	"path/filepath"
 	"runtime"
 	"strings"
 	"text/template"
@@ -14,6 +13,8 @@ import (
 	"gopkg.in/yaml.v3"
 )
 
+const toolPluginYamlPathTemplate = "tools/%s/plugin.yaml"
+
 //go:embed tools/*/plugin.yaml
 var toolsFS embed.FS
 
@@ -52,24 +53,11 @@ type RuntimeBinaries struct {
 	Execution      string `yaml:"execution"`
 }
 
-// ExtensionConfig defines the file extension
-type ExtensionConfig struct {
-	Default string `yaml:"default"`
-}
-
-// DownloadConfig holds the download configuration for directly downloading tools
-type DownloadConfig struct {
-	URLTemplate      string            `yaml:"url_template"`
-	FileNameTemplate string            `yaml:"file_name_template"`
-	Extension        ExtensionConfig   `yaml:"extension"`
-	ArchMapping      map[string]string `yaml:"arch_mapping"`
-	OSMapping        map[string]string `yaml:"os_mapping"`
-}
-
 // ToolPluginConfig holds the structure of the tool plugin.yaml file
 type ToolPluginConfig struct {
 	Name            string             `yaml:"name"`
 	Description     string             `yaml:"description"`
+	DefaultVersion  string             `yaml:"default_version"`
 	Runtime         string             `yaml:"runtime"`
 	RuntimeBinaries RuntimeBinaries    `yaml:"runtime_binaries"`
 	Installation    InstallationConfig `yaml:"installation"`
@@ -119,7 +107,7 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 
 	for _, config := range configs {
 		// Load the tool plugin - always use forward slashes for embedded filesystem paths (for windows support)
-		pluginPath := fmt.Sprintf("tools/%s/plugin.yaml", config.Name)
+		pluginPath := fmt.Sprintf(toolPluginYamlPathTemplate, config.Name)
 
 		// Read from embedded filesystem
 		data, err := toolsFS.ReadFile(pluginPath)
@@ -169,26 +157,28 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 		// Handle download configuration for directly downloaded tools
 		if pluginConfig.Download.URLTemplate != "" {
 			// Get the mapped architecture
-			mappedArch := getMappedArch(pluginConfig.Download.ArchMapping, runtime.GOARCH)
+			mappedArch := GetMappedArch(pluginConfig.Download.ArchMapping, runtime.GOARCH)
 
 			// Get the mapped OS
-			mappedOS := getMappedOS(pluginConfig.Download.OSMapping, runtime.GOOS)
+			mappedOS := GetMappedOS(pluginConfig.Download.OSMapping, runtime.GOOS)
 
 			// Get the appropriate extension
-			extension := getExtension(pluginConfig.Download.Extension, runtime.GOOS)
+			extension := GetExtension(pluginConfig.Download.Extension, runtime.GOOS)
 			info.Extension = extension
 
 			// Get the filename using the template
-			fileName := getFileName(pluginConfig.Download.FileNameTemplate, config.Version, mappedArch, runtime.GOOS)
+			fileName := GetFileName(pluginConfig.Download.FileNameTemplate, config.Version, mappedArch, runtime.GOOS)
 			info.FileName = fileName
 
 			// Get the download URL using the template
-			downloadURL := getDownloadURL(pluginConfig.Download.URLTemplate, fileName, config.Version, mappedArch, mappedOS, extension)
+			downloadURL := GetDownloadURL(pluginConfig.Download.URLTemplate, fileName, config.Version, mappedArch, mappedOS, extension, pluginConfig.Download.ReleaseVersion)
 			info.DownloadURL = downloadURL
 		}
 
 		// Process binary paths
 		for _, binary := range pluginConfig.Binaries {
+			var binaryPath string
+
 			// Process template variables in binary path
 			tmpl, err := template.New("binary_path").Parse(binary.Path)
 			if err != nil {
@@ -197,15 +187,28 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 
 			var buf bytes.Buffer
 			err = tmpl.Execute(&buf, struct {
-				Version string
+				Version    string
+				InstallDir string
 			}{
-				Version: config.Version,
+				Version:    config.Version,
+				InstallDir: installDir,
 			})
 			if err != nil {
 				return nil, fmt.Errorf("error executing binary path template for %s: %w", config.Name, err)
 			}
 
-			binaryPath := filepath.Join(installDir, buf.String())
+			binaryPath = buf.String()
+
+			// If the binary path is relative, join it with the install directory
+			if !path.IsAbs(binaryPath) {
+				binaryPath = path.Join(installDir, binaryPath)
+			}
+
+			// Add file extension for Windows executables
+			if runtime.GOOS == "windows" && !strings.HasSuffix(binaryPath, ".exe") {
+				binaryPath += ".exe"
+			}
+
 			info.Binaries[binary.Name] = binaryPath
 		}
 
@@ -215,36 +218,30 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 		}
 
 		// Process environment variables
-		if len(pluginConfig.Environment) > 0 {
-			// Get runtime install directory if needed
-			var runtimeInstallDir string
-			if info.Runtime != "" {
-				if runtime, ok := runtimes[info.Runtime]; ok {
-					runtimeInstallDir = runtime.InstallDir
-				}
+		for key, value := range pluginConfig.Environment {
+			// Process template variables in environment value
+			tmpl, err := template.New("env_value").Parse(value)
+			if err != nil {
+				return nil, fmt.Errorf("error parsing environment value template for %s: %w", config.Name, err)
 			}
 
-			// Process each environment variable
-			for key, tmplStr := range pluginConfig.Environment {
-				tmpl, err := template.New("env").Parse(tmplStr)
-				if err != nil {
-					return nil, fmt.Errorf("error parsing environment template for %s: %w", config.Name, err)
-				}
-
-				var buf bytes.Buffer
-				err = tmpl.Execute(&buf, struct {
-					RuntimeInstallDir string
-					Path              string
-				}{
-					RuntimeInstallDir: runtimeInstallDir,
-					Path:              os.Getenv("PATH"),
-				})
-				if err != nil {
-					return nil, fmt.Errorf("error executing environment template for %s: %w", config.Name, err)
-				}
-
-				info.Environment[key] = buf.String()
+			var buf bytes.Buffer
+			err = tmpl.Execute(&buf, struct {
+				Version           string
+				InstallDir        string
+				RuntimeInstallDir string
+				Path              string
+			}{
+				Version:           config.Version,
+				InstallDir:        installDir,
+				RuntimeInstallDir: runtimes[toolRuntime].InstallDir,
+				Path:              os.Getenv("PATH"),
+			})
+			if err != nil {
+				return nil, fmt.Errorf("error executing environment value template for %s: %w", config.Name, err)
 			}
+
+			info.Environment[key] = buf.String()
 		}
 
 		result[config.Name] = info
@@ -253,129 +250,29 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 	return result, nil
 }
 
-// Helper functions for processing download configuration
-
-// getMappedArch returns the architecture mapping for the current system
-func getMappedArch(archMapping map[string]string, goarch string) string {
-	// Check if there's a mapping for this architecture
-	if mappedArch, ok := archMapping[goarch]; ok {
-		return mappedArch
-	}
-	// Return the original architecture if no mapping exists
-	return goarch
-}
-
-// getMappedOS returns the OS mapping for the current system
-func getMappedOS(osMapping map[string]string, goos string) string {
-	// Check if there's a mapping for this OS
-	if mappedOS, ok := osMapping[goos]; ok {
-		return mappedOS
-	}
-	// Return the original OS if no mapping exists
-	return goos
-}
-
-// getExtension returns the appropriate file extension based on the OS
-func getExtension(extensionConfig ExtensionConfig, goos string) string {
-	return extensionConfig.Default
-}
-
-// getFileName generates the filename based on the template
-func getFileName(fileNameTemplate string, version string, mappedArch string, goos string) string {
-	// Prepare template data
-	data := struct {
-		Version string
-		OS      string
-		Arch    string
-	}{
-		Version: version,
-		OS:      goos,
-		Arch:    mappedArch,
-	}
-
-	// Execute template substitution for filename
-	tmpl, err := template.New("filename").Parse(fileNameTemplate)
-	if err != nil {
-		return ""
-	}
-
-	var buf bytes.Buffer
-	err = tmpl.Execute(&buf, data)
-	if err != nil {
-		return ""
-	}
-
-	return buf.String()
-}
-
-// getDownloadURL generates the download URL based on the template
-func getDownloadURL(urlTemplate string, fileName string, version string, mappedArch string, mappedOS string, extension string) string {
-	// Extract major version from version string (e.g. "17.0.10" -> "17")
-	majorVersion := version
-	if idx := strings.Index(version, "."); idx != -1 {
-		majorVersion = version[:idx]
-	}
-
-	// Prepare template data
-	data := struct {
-		Version      string
-		MajorVersion string
-		FileName     string
-		OS           string
-		Arch         string
-		Extension    string
-	}{
-		Version:      version,
-		MajorVersion: majorVersion,
-		FileName:     fileName,
-		OS:           mappedOS,
-		Arch:         mappedArch,
-		Extension:    extension,
-	}
-
-	// Execute template substitution for URL
-	tmpl, err := template.New("url").Parse(urlTemplate)
-	if err != nil {
-		return ""
-	}
-
-	var buf bytes.Buffer
-	err = tmpl.Execute(&buf, data)
-	if err != nil {
-		return ""
-	}
-
-	return buf.String()
-}
-
-// GetSupportedTools returns a map of supported tool names based on the tools folder
+// GetSupportedTools returns a map of supported tool names
 func GetSupportedTools() (map[string]struct{}, error) {
-	supportedTools := make(map[string]struct{})
-
-	// Read all directories in the tools folder
 	entries, err := toolsFS.ReadDir("tools")
 	if err != nil {
-		return nil, fmt.Errorf("failed to read tools directory: %w", err)
+		return nil, fmt.Errorf("error reading tools directory: %w", err)
 	}
 
-	// For each directory, check if it has a plugin.yaml file
+	tools := make(map[string]struct{})
 	for _, entry := range entries {
-		if !entry.IsDir() {
-			continue
+		if entry.IsDir() {
+			tools[entry.Name()] = struct{}{}
 		}
+	}
 
-		toolName := entry.Name()
-		// Always use forward slashes for embedded filesystem paths
-		pluginPath := fmt.Sprintf("tools/%s/plugin.yaml", toolName)
-
-		// Check if plugin.yaml exists
-		_, err := toolsFS.ReadFile(pluginPath)
-		if err != nil {
-			continue // Skip if no plugin.yaml
-		}
+	return tools, nil
+}
 
-		supportedTools[toolName] = struct{}{}
-	}
+// GetToolVersions returns a map of tool names to their default versions
+func GetToolVersions() map[string]string {
+	return GetPluginManager().GetToolVersions()
+}
 
-	return supportedTools, nil
+// GetToolRuntimeDependencies returns a map of tool names to their runtime dependencies
+func GetToolRuntimeDependencies() map[string]string {
+	return GetPluginManager().GetToolRuntimeDependencies()
 }
diff --git a/plugins/tools/dartanalyzer/plugin.yaml b/plugins/tools/dartanalyzer/plugin.yaml
index 31ecde9a..eb40640c 100644
--- a/plugins/tools/dartanalyzer/plugin.yaml
+++ b/plugins/tools/dartanalyzer/plugin.yaml
@@ -1,5 +1,6 @@
 name: dartanalyzer
-description: Dart Analyzer
+description: The Dart analyzer enforces the Dart style guide and other conventions. It's a static analysis tool that looks for potential errors and enforces style guidelines.
+default_version: 3.7.2
 runtime: "{{.Runtime}}"
 runtime_binaries:
   package_manager: dart
diff --git a/plugins/tools/eslint/plugin.yaml b/plugins/tools/eslint/plugin.yaml
index b049aebf..89e9b398 100644
--- a/plugins/tools/eslint/plugin.yaml
+++ b/plugins/tools/eslint/plugin.yaml
@@ -1,5 +1,6 @@
 name: eslint
-description: ESLint JavaScript linter
+description: ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code.
+default_version: 9.3.0
 runtime: node
 runtime_binaries:
   package_manager: npm
diff --git a/plugins/tools/lizard/plugin.yaml b/plugins/tools/lizard/plugin.yaml
index 9d3e19ff..373c91d6 100644
--- a/plugins/tools/lizard/plugin.yaml
+++ b/plugins/tools/lizard/plugin.yaml
@@ -1,5 +1,6 @@
 name: lizard
-description: Lizard code complexity analyzer
+description: Lizard is an extensible Cyclomatic Complexity Analyzer for many programming languages.
+default_version: 1.17.19
 runtime: python
 runtime_binaries:
   package_manager: python3
diff --git a/plugins/tools/pmd/plugin.yaml b/plugins/tools/pmd/plugin.yaml
index 35b19794..57ed9bdd 100644
--- a/plugins/tools/pmd/plugin.yaml
+++ b/plugins/tools/pmd/plugin.yaml
@@ -1,5 +1,6 @@
 name: pmd
-description: PMD - An extensible cross-language static code analyzer
+description: PMD is a source code analyzer that finds common programming flaws like unused variables, empty catch blocks, unnecessary object creation, and so forth.
+default_version: 6.55.0
 runtime: java
 runtime_binaries:
   execution: java
diff --git a/plugins/tools/pylint/plugin.yaml b/plugins/tools/pylint/plugin.yaml
index ff3e979c..85336f2f 100644
--- a/plugins/tools/pylint/plugin.yaml
+++ b/plugins/tools/pylint/plugin.yaml
@@ -1,5 +1,6 @@
 name: pylint
-description: Python linter
+description: Pylint is a Python static code analysis tool which looks for programming errors, helps enforcing a coding standard, sniffs for code smells and offers simple refactoring suggestions.
+default_version: 3.3.6
 runtime: python
 runtime_binaries:
   package_manager: python3
diff --git a/plugins/tools/semgrep/plugin.yaml b/plugins/tools/semgrep/plugin.yaml
index f9bd522f..58b773b0 100644
--- a/plugins/tools/semgrep/plugin.yaml
+++ b/plugins/tools/semgrep/plugin.yaml
@@ -1,5 +1,6 @@
 name: semgrep
-description: Static Analysis Security Testing (SAST) tool
+description: Semgrep is a fast, open-source, static analysis tool for finding bugs and enforcing code standards at editor, commit, and CI time.
+default_version: 1.78.0
 runtime: python
 runtime_binaries:
   package_manager: python3
diff --git a/plugins/tools/trivy/plugin.yaml b/plugins/tools/trivy/plugin.yaml
index d5e522d9..c4a1c169 100644
--- a/plugins/tools/trivy/plugin.yaml
+++ b/plugins/tools/trivy/plugin.yaml
@@ -1,5 +1,6 @@
 name: trivy
-description: Trivy vulnerability scanner
+description: Trivy is a comprehensive security scanner for containers and other artifacts.
+default_version: 0.59.1
 download:
   url_template: "https://github.com/aquasecurity/trivy/releases/download/v{{.Version}}/trivy_{{.Version}}_{{.OS}}-{{.Arch}}.{{.Extension}}"
   file_name_template: "trivy_{{.Version}}_{{.OS}}_{{.Arch}}"
diff --git a/tools/getTools.go b/tools/getTools.go
index 5884f1c4..fafd019a 100644
--- a/tools/getTools.go
+++ b/tools/getTools.go
@@ -4,8 +4,11 @@ import (
 	codacyClient "codacy/cli-v2/codacy-client"
 	"codacy/cli-v2/domain"
 	"codacy/cli-v2/plugins"
+	"codacy/cli-v2/utils/logger"
 	"fmt"
 	"strings"
+
+	"github.com/sirupsen/logrus"
 )
 
 func enrichToolsWithVersion(tools []domain.Tool) ([]domain.Tool, error) {
@@ -41,16 +44,33 @@ func GetRepositoryTools(initFlags domain.InitFlags) ([]domain.Tool, error) {
 		return nil, err
 	}
 
+	// Print supported tools for debugging
+	logger.Info("Supported tools:", logrus.Fields{
+		"supported_tools": supportedTools,
+	})
+
 	// Filter enabled tools
 	var enabledTools []domain.Tool
+	var unsupportedTools []string
+
 	for _, tool := range tools {
 		if tool.Settings.Enabled {
-			if _, exists := supportedTools[strings.ToLower(tool.Name)]; exists {
+			toolName := strings.ToLower(tool.Name)
+			if _, exists := supportedTools[toolName]; exists {
 				enabledTools = append(enabledTools, tool)
+			} else {
+				unsupportedTools = append(unsupportedTools, tool.Name)
 			}
 		}
 	}
 
+	// Log unsupported tools once
+	if len(unsupportedTools) > 0 {
+		logger.Warn("Some tools are not supported", logrus.Fields{
+			"unsupported_tools": strings.Join(unsupportedTools, ", "),
+		})
+	}
+
 	return enrichToolsWithVersion(enabledTools)
 }
 


Reviews:
user: login: machadoit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T09:26:43Z
user: login: andrzej-janczak
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T12:03:10Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:08Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:09Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:10Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:11Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:11Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:12Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:13Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:14Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:14Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:15Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:16Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:16Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:17Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:18Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:18Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:19Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:20Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:20Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:21Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:22Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:22Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:23Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:24Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:24Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-10-31T09:29:25Z

Review comments:
diff_hunk: @@ -306,3 +307,39 @@ func (p *runtimePlugin) getMappedOS(goos string) string {
 	// Return the original OS if no mapping exists
 	return goos
 }
+
+// GetRuntimeVersions returns a map of runtime names to their default versions
+func GetRuntimeVersions() map[string]string {
path: plugins/runtime-utils.go
user: login: machadoit
body: Seems strange that we added 'DefaultVersion string         `yaml:"default_version"`' to the pluginConfig and it is not about 'loading the full plugin configuration' or something. And there is this method exclusively about the runtimes.

I guess it does not hurt, but would expect, that if we are making this changes, the init to be the spot that you would load the plugins configurations and use them. Why have an hardcoded list on the init, and then load them just for the default_version? Might as well, load them, and use them.

Checking if some tools like 'enigma' we can add an optional 'enableByDefault' or something that would be off for the ones we don't want to add automatically
created_at: 2025-05-15T09:26:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -306,3 +307,39 @@ func (p *runtimePlugin) getMappedOS(goos string) string {
 	// Return the original OS if no mapping exists
 	return goos
 }
+
+// GetRuntimeVersions returns a map of runtime names to their default versions
+func GetRuntimeVersions() map[string]string {
path: plugins/runtime-utils.go
user: login: andrzej-janczak
body: First two points are addressed 

3rd one looks like candidate for another PR, we can discuss it 👍 
created_at: 2025-05-15T12:03:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+package plugins
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
path: plugins/shared.go
user: login: codacy-production[bot]
body: :no_entry_sign: **Codacy** found a **high Security** issue: [When working with web applications that involve rendering user-generated  content, it's important to properly escape any HTML content to prevent  Cross-Site Scripting (XSS) attacks.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the Semgrep linter is related to the potential for Cross-Site Scripting (XSS) attacks when rendering user-generated content with the `text/template` package in Go. If user input is not properly escaped, it could lead to the injection of malicious scripts that execute in the user's browser.

To mitigate this risk, you should use the `html/template` package instead of the `text/template` package. The `html/template` package automatically escapes HTML content, providing an additional layer of security against XSS attacks.

Here is the code suggestion to fix the issue:

```suggestion
	"html/template"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:09Z
author_association: NONE
diff_hunk: @@ -103,28 +52,45 @@ func ProcessRuntimes(configs []RuntimeConfig, runtimesDir string) (map[string]*R
 
 // ProcessRuntime processes a single runtime configuration and returns detailed runtime info
 func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, error) {
-	plugin, err := loadPlugin(config.Name)
+	pluginConfig, err := GetPluginManager().GetRuntimeConfig(config.Name)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load plugin for runtime %s: %w", config.Name, err)
 	}
 
-	fileName := plugin.getFileName(config.Version)
-	extension := plugin.getExtension(runtime.GOOS)
-	installDir := plugin.getInstallationDirectoryPath(runtimesDir, config.Version)
+	// Map Go architecture and OS to runtime-specific values
+	mappedArch := GetMappedArch(pluginConfig.Download.ArchMapping, runtime.GOARCH)
+	mappedOS := GetMappedOS(pluginConfig.Download.OSMapping, runtime.GOOS)
+
+	// Get the appropriate extension
+	extension := GetExtension(pluginConfig.Download.Extension, runtime.GOOS)
+
+	// Get the filename using the template
+	fileName := GetFileName(pluginConfig.Download.FileNameTemplate, config.Version, mappedArch, runtime.GOOS)
+
+	// Get the download URL using the template
+	downloadURL := GetDownloadURL(pluginConfig.Download.URLTemplate, fileName, config.Version, mappedArch, mappedOS, extension, pluginConfig.Download.ReleaseVersion)
+
+	// For Python, we want to use a simpler directory structure
+	var installDir string
+	if config.Name == "python" {
+		installDir = path.Join(runtimesDir, "python")
+	} else {
+		installDir = path.Join(runtimesDir, fileName)
path: plugins/runtime-utils.go
user: login: codacy-production[bot]
body: :no_entry_sign: **Codacy** found a **high ErrorProne** issue: [`path.Join(...)` always joins using a forward slash.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the Semgrep linter is that the `path.Join(...)` function in Go always uses forward slashes (`/`) as path separators, regardless of the operating system. This can lead to problems on Windows systems, where the standard path separator is a backslash (`\`). Therefore, using `path.Join` with a filename that may contain backslashes can result in incorrect paths.

To fix this issue, we can use `filepath.Join` instead of `path.Join`, as `filepath.Join` will use the correct path separator for the current operating system.

Here's the code suggestion to fix the issue:

```suggestion
		installDir = filepath.Join(runtimesDir, fileName)
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:09Z
author_association: NONE
diff_hunk: @@ -197,15 +187,28 @@ func ProcessTools(configs []ToolConfig, toolDir string, runtimes map[string]*Run
 
 			var buf bytes.Buffer
 			err = tmpl.Execute(&buf, struct {
-				Version string
+				Version    string
+				InstallDir string
 			}{
-				Version: config.Version,
+				Version:    config.Version,
+				InstallDir: installDir,
 			})
 			if err != nil {
 				return nil, fmt.Errorf("error executing binary path template for %s: %w", config.Name, err)
 			}
 
-			binaryPath := filepath.Join(installDir, buf.String())
+			binaryPath = buf.String()
+
+			// If the binary path is relative, join it with the install directory
+			if !path.IsAbs(binaryPath) {
+				binaryPath = path.Join(installDir, binaryPath)
path: plugins/tool-utils.go
user: login: codacy-production[bot]
body: :no_entry_sign: **Codacy** found a **high ErrorProne** issue: [`path.Join(...)` always joins using a forward slash.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the Semgrep linter is that `path.Join(...)` uses forward slashes (`/`) as the path separator, which can lead to incorrect paths on Windows systems where the expected separator is a backslash (`\`). This can cause problems when the resulting path is used to access files on the filesystem.

To fix this issue, you can use `filepath.Join(...)` instead of `path.Join(...)`. The `filepath` package automatically uses the correct path separator for the operating system on which the code is running.

Here’s the suggested code change:

```suggestion
				binaryPath = filepath.Join(installDir, binaryPath)
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:10Z
author_association: NONE
diff_hunk: @@ -103,28 +52,45 @@ func ProcessRuntimes(configs []RuntimeConfig, runtimesDir string) (map[string]*R
 
 // ProcessRuntime processes a single runtime configuration and returns detailed runtime info
 func processRuntime(config RuntimeConfig, runtimesDir string) (*RuntimeInfo, error) {
-	plugin, err := loadPlugin(config.Name)
+	pluginConfig, err := GetPluginManager().GetRuntimeConfig(config.Name)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load plugin for runtime %s: %w", config.Name, err)
 	}
 
-	fileName := plugin.getFileName(config.Version)
-	extension := plugin.getExtension(runtime.GOOS)
-	installDir := plugin.getInstallationDirectoryPath(runtimesDir, config.Version)
+	// Map Go architecture and OS to runtime-specific values
+	mappedArch := GetMappedArch(pluginConfig.Download.ArchMapping, runtime.GOARCH)
+	mappedOS := GetMappedOS(pluginConfig.Download.OSMapping, runtime.GOOS)
+
+	// Get the appropriate extension
+	extension := GetExtension(pluginConfig.Download.Extension, runtime.GOOS)
+
+	// Get the filename using the template
+	fileName := GetFileName(pluginConfig.Download.FileNameTemplate, config.Version, mappedArch, runtime.GOOS)
+
+	// Get the download URL using the template
+	downloadURL := GetDownloadURL(pluginConfig.Download.URLTemplate, fileName, config.Version, mappedArch, mappedOS, extension, pluginConfig.Download.ReleaseVersion)
+
+	// For Python, we want to use a simpler directory structure
+	var installDir string
+	if config.Name == "python" {
+		installDir = path.Join(runtimesDir, "python")
path: plugins/runtime-utils.go
user: login: codacy-production[bot]
body: :no_entry_sign: **Codacy** found a **high ErrorProne** issue: [`path.Join(...)` always joins using a forward slash.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the Semgrep linter indicates that `path.Join(...)` is being used to concatenate paths, but it is being done with a hardcoded string "python" that may not be compatible with the operating system's path separator. In Go, `path.Join` is intended for use with paths that should be treated as POSIX-style paths (using forward slashes), while `filepath.Join` should be used for file paths that are OS-aware and will use the correct separator for the current operating system.

To fix the issue, you should replace `path.Join` with `filepath.Join`, which will ensure that the correct path separator is used based on the operating system.

Here's the suggested code change:

```suggestion
		installDir = filepath.Join(runtimesDir, "python")
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,156 @@
+package plugins
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+)
+
+// ExtensionConfig defines the file extension based on OS
+type ExtensionConfig struct {
+	Windows string `yaml:"windows"`
+	Default string `yaml:"default"`
+}
+
+// DownloadConfig holds the download configuration from the plugin.yaml
+type DownloadConfig struct {
+	URLTemplate      string            `yaml:"url_template"`
+	FileNameTemplate string            `yaml:"file_name_template"`
+	Extension        ExtensionConfig   `yaml:"extension"`
+	ArchMapping      map[string]string `yaml:"arch_mapping"`
+	OSMapping        map[string]string `yaml:"os_mapping"`
+	ReleaseVersion   string            `yaml:"release_version,omitempty"`
+}
+
+// Binary represents a binary executable provided by the runtime or tool
+type Binary struct {
+	Name string      `yaml:"name"`
+	Path interface{} `yaml:"path"` // Can be either string or map[string]string
+}
+
+// PluginConfig holds the structure of the plugin.yaml file
+type PluginConfig struct {
+	Name           string         `yaml:"name"`
+	Description    string         `yaml:"description"`
+	Download       DownloadConfig `yaml:"download"`
+	Binaries       []Binary       `yaml:"binaries"`
+	DefaultVersion string         `yaml:"default_version"`
+}
+
+// RuntimeConfig represents configuration for a runtime
+type RuntimeConfig struct {
+	Name    string
+	Version string
+}
+
+// RuntimeInfo contains all processed information about a runtime
+type RuntimeInfo struct {
+	Name        string
+	Version     string
+	InstallDir  string
+	DownloadURL string
+	FileName    string
+	Extension   string
+	Binaries    map[string]string // Map of binary name to full path
+}
+
+// templateData holds the data to be used in template substitution
+type templateData struct {
+	Version        string
+	MajorVersion   string
+	FileName       string
+	OS             string
+	Arch           string
+	Extension      string
+	ReleaseVersion string
+}
+
+// GetMappedArch returns the architecture mapping for the current system
+func GetMappedArch(archMapping map[string]string, goarch string) string {
+	// Check if there's a mapping for this architecture
+	if mappedArch, ok := archMapping[goarch]; ok {
+		return mappedArch
+	}
+	// Return the original architecture if no mapping exists
+	return goarch
+}
+
+// GetExtension returns the appropriate file extension based on the OS
+func GetExtension(extension ExtensionConfig, goos string) string {
+	if goos == "windows" {
+		return extension.Windows
+	}
+	return extension.Default
+}
+
+// GetMajorVersion extracts the major version from a version string (e.g. "17.0.10" -> "17")
+func GetMajorVersion(version string) string {
+	if idx := strings.Index(version, "."); idx != -1 {
+		return version[:idx]
+	}
+	return version
+}
+
+// GetFileName generates the filename based on the template
+func GetFileName(fileNameTemplate string, version string, mappedArch string, goos string) string {
+	// Prepare template data
+	data := templateData{
+		Version:      version,
+		MajorVersion: GetMajorVersion(version),
+		OS:           goos,
+		Arch:         mappedArch,
+	}
+
+	// Execute template substitution for filename
+	tmpl, err := template.New("filename").Parse(fileNameTemplate)
+	if err != nil {
+		return ""
+	}
+
+	var buf bytes.Buffer
+	err = tmpl.Execute(&buf, data)
+	if err != nil {
+		return ""
+	}
+
+	return buf.String()
+}
+
+// GetDownloadURL generates the download URL based on the template
+func GetDownloadURL(urlTemplate string, fileName string, version string, mappedArch string, mappedOS string, extension string, releaseVersion string) string {
path: plugins/shared.go
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium Complexity** issue: [Method GetDownloadURL has 7 parameters (limit is 5)](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the Lizard linter is related to the number of parameters in the `GetDownloadURL` function. Having too many parameters can make the function harder to use and understand, as well as increase the cognitive load for anyone maintaining the code. The limit set by the linter suggests that functions should ideally have no more than 5 parameters.

To address this issue, we can encapsulate the parameters into a single struct, which will reduce the number of parameters in the function signature. Here's a code suggestion that implements this change:

```suggestion
func GetDownloadURL(urlTemplate string, data templateData) string {
```

In this suggestion, we assume that `templateData` is a struct that contains all the necessary fields (like `FileName`, `Version`, `MappedArch`, etc.) that were previously passed as separate parameters. You would also need to create an instance of `templateData` before calling `GetDownloadURL`.

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:11Z
author_association: NONE
diff_hunk: @@ -107,21 +146,31 @@ function Compare-Files {
             Write-Host "==================="
             exit 1
         }
-        Write-Host "✅ $label/$($_.Name) matches expected"
+        Write-Host "✅ $label/$relativePath matches expected"
     }
     
     # Compare subdirectories
     Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object {
-        $actualSubDir = Join-Path $actualDir $_.Name
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualSubDir = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking subdirectory: $relativePath"
+        Write-Host "Expected dir: $($_.FullName)"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used to output information. While `Write-Host` works for displaying messages to the console, it is not recommended because it doesn't allow for capturing or redirecting output, and may not function correctly in all environments or hosts. Instead, it's better to use other cmdlets like `Write-Output`, `Write-Verbose`, or `Write-Information`, which provide more flexibility and are more suitable for scripting.

To address this issue, you can replace the `Write-Host` command with `Write-Output` for the specific line flagged by the linter.

Here's the suggested code change:

```suggestion
        Write-Output "Expected dir: $($_.FullName)"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:12Z
author_association: NONE
diff_hunk: @@ -17,6 +22,7 @@ if (-not $env:CODACY_API_TOKEN) {
 function Normalize-Config {
     param ([string]$file)
     
+    Write-Host "Normalizing config file: $file"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is used to output messages. While `Write-Host` can display messages to the console, it has limitations: it does not allow for capturing or redirecting output, and it may not function correctly in all environments or hosts. Instead, it's recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better flexibility and compatibility.

To address this issue, you can replace `Write-Host` with `Write-Information`, which is more suitable for logging informational messages that can be captured or redirected.

Here's the suggested code change:

```suggestion
    Write-Information "Normalizing config file: $file"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:13Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is not recommended for use because it directly writes to the console and cannot be captured or redirected. This can lead to problems in scripts where output needs to be processed or logged. Instead, it is better to use `Write-Output`, `Write-Verbose`, or `Write-Information` which provide more flexibility in handling output.

To fix the issue, you can replace the `Write-Host` command with `Write-Output`. Here’s the suggested change:

```suggestion
    Write-Output "`nActual directory contents:"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:14Z
author_association: NONE
diff_hunk: @@ -107,21 +146,31 @@ function Compare-Files {
             Write-Host "==================="
             exit 1
         }
-        Write-Host "✅ $label/$($_.Name) matches expected"
+        Write-Host "✅ $label/$relativePath matches expected"
     }
     
     # Compare subdirectories
     Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object {
-        $actualSubDir = Join-Path $actualDir $_.Name
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualSubDir = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking subdirectory: $relativePath"
+        Write-Host "Expected dir: $($_.FullName)"
+        Write-Host "Actual dir: $actualSubDir"
         
         if (-not (Test-Path $actualSubDir)) {
-            Write-Host "❌ Directory $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ Directory $label/$relativePath does not exist in actual output"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is being used to output messages. While `Write-Host` directly writes to the console, it has limitations: it does not allow for output to be captured, redirected, or suppressed, which can lead to issues in automated scripts or environments without a host. The recommended practice is to use `Write-Output`, `Write-Verbose`, or `Write-Information` instead, which provide more flexibility in handling output.

To fix the issue, we can replace `Write-Host` with `Write-Output` in the specified line.

Here’s the code suggestion:

```suggestion
            Write-Output "❌ Directory $label/$relativePath does not exist in actual output"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:14Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
+        Write-Host "Expected file: $($_.FullName)"
+        Write-Host "Actual file: $actualFile"
         
         if (-not (Test-Path $actualFile)) {
-            Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ $label/$relativePath does not exist in actual output"
             Write-Host "Expected: $($_.FullName)"
             Write-Host "Actual should be: $actualFile"
+            Write-Host "Current directory structure:"
+            Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+                Write-Host "  $($_.FullName)"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is generally discouraged because it outputs directly to the console and cannot be captured or redirected. This can lead to problems in environments where the output needs to be processed or logged. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which provide better flexibility and usability in scripts.

To address this issue, you can replace `Write-Host` with `Write-Output`. Here's the code suggestion:

```suggestion
                Write-Output "  $($_.FullName)"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:15Z
author_association: NONE
diff_hunk: @@ -107,21 +146,31 @@ function Compare-Files {
             Write-Host "==================="
             exit 1
         }
-        Write-Host "✅ $label/$($_.Name) matches expected"
+        Write-Host "✅ $label/$relativePath matches expected"
     }
     
     # Compare subdirectories
     Get-ChildItem -Path $expectedDir -Directory | Where-Object { $_.Name -ne "logs" } | ForEach-Object {
-        $actualSubDir = Join-Path $actualDir $_.Name
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualSubDir = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking subdirectory: $relativePath"
+        Write-Host "Expected dir: $($_.FullName)"
+        Write-Host "Actual dir: $actualSubDir"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that the `Write-Host` cmdlet is being used to output information to the console. While `Write-Host` does display messages, it has limitations: it does not work in all PowerShell hosts, cannot be captured or redirected, and is not suitable for logging or outputting information that may need to be processed further. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better compatibility and flexibility.

To resolve this issue, you can replace `Write-Host` with `Write-Output` for the specific line mentioned. Here’s the code suggestion:

```suggestion
        Write-Output "Actual dir: $actualSubDir"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:16Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used to output information to the console. While `Write-Host` can display text to the user, it has several limitations: it does not allow for redirection, capturing, or suppression of output, which can lead to issues in scripts that need to log information or run in different environments. Instead, it's recommended to use other cmdlets like `Write-Output`, `Write-Verbose`, or `Write-Information` for better flexibility and compatibility.

To resolve this issue, you can replace `Write-Host` with `Write-Output`, which allows the output to be captured or redirected if needed.

Here is the suggested code change:

```suggestion
        Write-Output "  $($_.FullName.Replace($expectedDir, ''))"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:17Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by PSScriptAnalyzer is that `Write-Host` is not recommended because it directly writes to the console and does not allow for output redirection, capturing, or suppression. This can lead to problems in scripts that are intended to be more flexible or reusable in different contexts, such as when they are run in environments without a console or when the output needs to be processed further.

To address this, you can replace `Write-Host` with `Write-Output`, which is more versatile and allows the output to be captured or redirected.

Here's the suggested code change:

```suggestion
    Write-Output "Label: $label"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:17Z
author_association: NONE
diff_hunk: @@ -5,8 +5,13 @@ $ErrorActionPreference = "Stop"
 $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
 $CLI_PATH = Join-Path (Get-Location) "cli-v2.exe"
 
+Write-Host "=== Environment Information ==="
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used to output information to the console. While `Write-Host` does display text directly to the host, it has limitations such as not being able to be captured or redirected, which can lead to issues in scripts that may be run in different environments or contexts. The linter suggests using `Write-Output`, `Write-Verbose`, or `Write-Information` instead, as these cmdlets provide more flexibility and are better suited for scripting.

To resolve this issue, you can replace `Write-Host` with `Write-Output`, which allows the output to be captured or redirected if necessary.

Here is the suggested single line change:

```suggestion
Write-Output "=== Environment Information ==="
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:18Z
author_association: NONE
diff_hunk: @@ -107,21 +146,31 @@ function Compare-Files {
             Write-Host "==================="
             exit 1
         }
-        Write-Host "✅ $label/$($_.Name) matches expected"
+        Write-Host "✅ $label/$relativePath matches expected"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is not recommended for outputting messages in PowerShell scripts because it writes directly to the console and cannot be redirected or captured. This can lead to problems in environments where the script's output needs to be processed further or logged. Instead, it is advisable to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which provide more flexibility and are better suited for scripting.

To resolve this issue, you can replace `Write-Host` with `Write-Information`, which is a more appropriate choice for logging informational messages.

Here's the code suggestion to fix the issue:

```suggestion
        Write-Information "✅ $label/$relativePath matches expected"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:18Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
+        Write-Host "Expected file: $($_.FullName)"
+        Write-Host "Actual file: $actualFile"
         
         if (-not (Test-Path $actualFile)) {
-            Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ $label/$relativePath does not exist in actual output"
             Write-Host "Expected: $($_.FullName)"
             Write-Host "Actual should be: $actualFile"
+            Write-Host "Current directory structure:"
+            Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+                Write-Host "  $($_.FullName)"
+            }
             exit 1
         }
         
+        Write-Host "Comparing file contents..."
         $expectedContent = Normalize-Config $_.FullName
         $actualContent = Normalize-Config $actualFile
         
         # Compare line by line
         $diff = Compare-Object $expectedContent $actualContent -PassThru
         if ($diff) {
-            Write-Host "❌ $label/$($_.Name) does not match expected"
+            Write-Host "❌ $label/$relativePath does not match expected"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is being used to output messages to the console. While `Write-Host` is suitable for displaying information directly to the user, it has limitations such as not being able to redirect output or capture it in a pipeline. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which are more versatile and provide better control over output.

To fix the issue, we can replace `Write-Host` with `Write-Output`, which will allow the message to be captured or redirected if needed.

Here's the code suggestion:

```suggestion
            Write-Output "❌ $label/$relativePath does not match expected"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:19Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used to output information to the console. While `Write-Host` does display messages directly to the host, it has limitations. Specifically, it may not work in all environments, cannot be captured or redirected, and prior to PowerShell 5.0, it could not be suppressed. The recommendation is to use `Write-Output`, `Write-Verbose`, or `Write-Information` instead, which are more versatile and suitable for scripting.

To address this specific line of code, we can replace `Write-Host` with `Write-Output`. Here's the single line change:

```suggestion
        Write-Output "`nChecking file: $relativePath"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:20Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
+        Write-Host "Expected file: $($_.FullName)"
+        Write-Host "Actual file: $actualFile"
         
         if (-not (Test-Path $actualFile)) {
-            Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ $label/$relativePath does not exist in actual output"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is being used to output messages to the console. While `Write-Host` can display information to the user, it is not the best practice for logging or outputting information in PowerShell scripts because it does not allow for the output to be captured, redirected, or suppressed. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better flexibility and compatibility.

To address this issue, we can replace `Write-Host` with `Write-Information`, which is a more suitable alternative for logging informational messages. Here is the single line change to fix the issue:

```suggestion
            Write-Information "❌ $label/$relativePath does not exist in actual output"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:20Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
+    Get-ChildItem -Path $expectedDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($expectedDir, ''))"
+    }
+    
+    Write-Host "`nActual directory contents:"
+    Get-ChildItem -Path $actualDir -Recurse | ForEach-Object {
+        Write-Host "  $($_.FullName.Replace($actualDir, ''))"
+    }
+    
     # Compare files
     Get-ChildItem -Path $expectedDir -File | ForEach-Object {
-        $actualFile = Join-Path $actualDir $_.Name
-
+        $relativePath = $_.FullName.Replace($expectedDir, '').TrimStart('\')
+        $actualFile = Join-Path $actualDir $relativePath
+        Write-Host "`nChecking file: $relativePath"
+        Write-Host "Expected file: $($_.FullName)"
+        Write-Host "Actual file: $actualFile"
         
         if (-not (Test-Path $actualFile)) {
-            Write-Host "❌ $label/$($_.Name) does not exist in actual output"
+            Write-Host "❌ $label/$relativePath does not exist in actual output"
             Write-Host "Expected: $($_.FullName)"
             Write-Host "Actual should be: $actualFile"
+            Write-Host "Current directory structure:"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used for outputting messages. `Write-Host` directly writes to the console and is not suitable for scenarios where output needs to be captured, redirected, or suppressed. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better flexibility and compatibility.

To fix the issue, you can replace `Write-Host` with `Write-Output` in the line where the current directory structure is being printed. This change will allow the output to be captured or redirected if needed.

Here's the suggested code change:

```suggestion
            Write-Output "Current directory structure:"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:21Z
author_association: NONE
diff_hunk: @@ -5,8 +5,13 @@ $ErrorActionPreference = "Stop"
 $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
 $CLI_PATH = Join-Path (Get-Location) "cli-v2.exe"
 
+Write-Host "=== Environment Information ==="
 Write-Host "Script directory: $SCRIPT_DIR"
 Write-Host "Current working directory: $(Get-Location)"
+Write-Host "CLI Path: $CLI_PATH"
+Write-Host "OS: $([System.Environment]::OSVersion.Platform)"
+Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is being used to output information to the console. While `Write-Host` works for displaying messages directly to the host, it has limitations, such as not being able to redirect output or capture it in a pipeline. This can lead to problems in scenarios where you want to log information or use the output programmatically. Instead, it's recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which are more flexible and can be used in a variety of contexts.

To resolve this issue, you can replace `Write-Host` with `Write-Output` for the line that outputs the PowerShell version. This will allow the information to be captured or redirected if necessary.

Here’s the suggested code change:

```suggestion
Write-Output "PowerShell Version: $($PSVersionTable.PSVersion)"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:22Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is generally discouraged because it writes directly to the console and does not allow for output redirection or capturing. This can lead to problems in different PowerShell hosts or when running scripts in environments where console output is not available. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better compatibility and flexibility.

To fix the issue, you can replace `Write-Host` with `Write-Output`, which allows the output to be captured or redirected. Here’s the suggested change:

```suggestion
    Write-Output "Expected dir: $expectedDir"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:22Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by PSScriptAnalyzer is that `Write-Host` is being used for output. While `Write-Host` displays information directly to the console, it has limitations such as not being able to redirect output or work in all environments. Instead, it is recommended to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which provide more flexibility and are better practices for script output.

To resolve this issue, you can replace the `Write-Host` command with `Write-Output` for the line that produces the "Resolved paths:" output. This change allows the output to be captured or redirected if needed.

Here's the suggested code change:

```suggestion
    Write-Output "Resolved paths:"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:23Z
author_association: NONE
diff_hunk: @@ -5,8 +5,13 @@ $ErrorActionPreference = "Stop"
 $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
 $CLI_PATH = Join-Path (Get-Location) "cli-v2.exe"
 
+Write-Host "=== Environment Information ==="
 Write-Host "Script directory: $SCRIPT_DIR"
 Write-Host "Current working directory: $(Get-Location)"
+Write-Host "CLI Path: $CLI_PATH"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by PSScriptAnalyzer is that `Write-Host` is not recommended for outputting information in PowerShell scripts because it writes directly to the host's console, making it difficult to capture or redirect the output. This can lead to issues in environments where the script might be run in different contexts (e.g., automated scripts, logging, etc.). Instead, it's better to use `Write-Output`, `Write-Verbose`, or `Write-Information` for more flexible output handling.

To fix the issue in the provided code, you can replace `Write-Host` with `Write-Output`. Here’s the suggested change:

```suggestion
Write-Output "CLI Path: $CLI_PATH"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:24Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
+    Write-Host "Label: $label"
+    
+    # Convert to absolute paths and normalize separators
+    $expectedDir = (Resolve-Path $expectedDir).Path
+    $actualDir = (Resolve-Path $actualDir).Path
+    
+    Write-Host "Resolved paths:"
+    Write-Host "Expected dir (resolved): $expectedDir"
+    Write-Host "Actual dir (resolved): $actualDir"
+    
+    # List directory contents before comparison
+    Write-Host "`nExpected directory contents:"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer linter is that `Write-Host` is not recommended for outputting information because it is not suitable for all PowerShell hosts and cannot be redirected or captured. Instead, it's better to use `Write-Output`, `Write-Verbose`, or `Write-Information` for better compatibility and flexibility in handling output.

To address this issue, you can replace `Write-Host` with `Write-Output`, which allows the output to be captured or redirected if needed.

Here's the code suggestion to fix the issue:

```suggestion
    Write-Output "`nExpected directory contents:"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:24Z
author_association: NONE
diff_hunk: @@ -79,25 +85,58 @@ function Compare-Files {
         [string]$label
     )
     
+    Write-Host "`n=== Starting Directory Comparison ==="
+    Write-Host "Comparing directories:"
+    Write-Host "Expected dir: $expectedDir"
+    Write-Host "Actual dir: $actualDir"
path: integration-tests/run.ps1
user: login: codacy-production[bot]
body: :warning: **Codacy** found a **medium BestPractice** issue: [File 'run.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121)

The issue identified by the PSScriptAnalyzer is that `Write-Host` is not recommended for outputting information in PowerShell scripts because it writes directly to the host and cannot be captured, redirected, or suppressed. Instead, it's better to use `Write-Output`, `Write-Verbose`, or `Write-Information`, which are more flexible and compatible with various environments.

To fix the issue, you can replace `Write-Host` with `Write-Output`. Here's the single line change:

```suggestion
    Write-Output "Actual dir: $actualDir"
```

---

 *This comment was generated by an experimental AI tool.*
created_at: 2025-10-31T09:29:25Z
author_association: NONE

PR comments:
user: login: codacy-production[bot]
created_at: 2025-05-15T08:26:53Z
body: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121/coverage?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **-1.32%** | :white_check_mark: **57.10%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (c29ef51d212ac559cb175ca0037e16da838a9720) | 4494 | 1376 | 30.62% | |
| Head commit (586bb1cec9930fba19f8007cb3162c4fb8ac34e1) | 4516 (+22) | 1323 (-53) | 29.30% (**-1.32%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#121) | 338 | 193 | **57.10%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
author_association: NONE
user: login: machadoit
created_at: 2025-05-15T08:56:29Z
body: I think we should discuss this and align with the needs for https://github.com/codacy/codacy-cli-v2/pull/119. If we are going to fetch configurations without token from the API, maybe we can also get the tools versions from the public API instead of having them on the yamls
author_association: CONTRIBUTOR
user: login: andrzej-janczak
created_at: 2025-05-15T09:11:39Z
body: Ok, so yaml could be always just a fallback just in case ... :) 
author_association: CONTRIBUTOR
user: login: machadoit
created_at: 2025-05-15T09:13:04Z
body: > Ok, so yaml could be always just a fallback just in case ... :)

Alright, that is fair 👌 
author_association: CONTRIBUTOR
user: login: franciscoovazevedo
created_at: 2025-05-15T12:16:41Z
body: > I think we should discuss this and align with the needs for #119. If we are going to fetch configurations without token from the API, maybe we can also get the tools versions from the public API instead of having them on the yamls

```scala 
func enrichToolsWithVersion(tools []Tool) ([]Tool, error) {
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	// Create a new GET request
	req, err := http.NewRequest("GET", "https://api.codacy.com/api/v3/tools", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	// Send the request
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("failed to get tools from Codacy API: status code %d", resp.StatusCode)
	}

	// Read the response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	var response ToolsResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	// Create a map of tool UUIDs to versions
	versionMap := make(map[string]string)
	for _, tool := range response.Data {
		versionMap[tool.Uuid] = tool.Version
	}

	// Enrich the input tools with versions
	for i, tool := range tools {
		if version, exists := versionMap[tool.Uuid]; exists {
			tools[i].Version = version
		}
	}

	return tools, nil
}
```

I think we already fetch versions from API. 
author_association: CONTRIBUTOR
user: login: andrzej-janczak
created_at: 2025-05-16T07:42:15Z
body: Sorry It got so big, I did not expect that 😓 
I understand we should discuss it first, this refactor was a small one in my head when I started this 😄

There are some good parts like removing some common code 😉 
https://github.com/codacy/codacy-cli-v2/pull/121/files?diff=split&w=0#diff-8e5534f73912cbdd48e0ad69a259fbf61eea3c7db43cb6f93ef6e22d9e0c79d3L32-L85 
author_association: CONTRIBUTOR
user: login: codacy-production[bot]
created_at: 2025-05-26T14:02:55Z
body: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121/coverage?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **-1.49%** | :white_check_mark: **56.97%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (f894e3d530fa0910988f4f0d4f6a1e129239ff9f) | 4578 | 1430 | 31.24% | |
| Head commit (74fbb75880290fa1b99cede8debc35d18a2636ef) | 4599 (+21) | 1368 (-62) | 29.75% (**-1.49%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#121) | 337 | 192 | **56.97%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
author_association: NONE
user: login: codacy-production[bot]
created_at: 2025-10-31T09:27:43Z
body: Codacy detected **25 issues** in this pull request (**1** are potential false positives)
[Review issues in Codacy →](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/121/issues?utm_source=github.com&utm_medium=pullRequestSummary&utm_campaign=pullRequestSummaryPullRequest)
<!-- 082f941e-099e-494a-83c7-b808dccfa8f3 -->
author_association: NONE