From 203c916626e350f12ddd38064faac7a1ce93fadd Mon Sep 17 00:00:00 2001
From: Michael Sverdlov <sverdlov93@gmail.com>
Date: Sat, 28 Dec 2024 11:30:56 +0200
Subject: [PATCH 1/4] Package manager login command - Docker, Podman (#1304)

---
 .../container/containermanagercommand.go      |   6 +-
 .../packagemanagerlogin.go                    | 119 ++++++++++++++----
 .../packagemanagerlogin_test.go               |  25 ++++
 .../utils/container/containermanager.go       |   8 +-
 artifactory/utils/repositoryutils.go          |   3 +-
 common/project/projectconfig.go               |  15 +++
 common/project/projectconfig_test.go          |  30 +++++
 utils/ioutils/questionnaire.go                |   4 +-
 8 files changed, 178 insertions(+), 32 deletions(-)
 create mode 100644 common/project/projectconfig_test.go

diff --git a/artifactory/commands/container/containermanagercommand.go b/artifactory/commands/container/containermanagercommand.go
index 6190a5693..eb4e74efe 100644
--- a/artifactory/commands/container/containermanagercommand.go
+++ b/artifactory/commands/container/containermanagercommand.go
@@ -41,7 +41,11 @@ func (cm *ContainerCommand) PerformLogin(serverDetails *config.ServerDetails, co
 			}
 		}
 		loginConfig := &container.ContainerManagerLoginConfig{ServerDetails: serverDetails}
-		return container.ContainerManagerLogin(cm.image, loginConfig, containerManagerType)
+		imageRegistry, err := cm.image.GetRegistry()
+		if err != nil {
+			return err
+		}
+		return container.ContainerManagerLogin(imageRegistry, loginConfig, containerManagerType)
 	}
 	return nil
 }
diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go b/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go
index 2a3958669..e462724a2 100644
--- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go
+++ b/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go
@@ -10,21 +10,50 @@ import (
 	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/repository"
 	commandsutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
 	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
+	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container"
 	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
 	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/yarn"
 	"github.com/jfrog/jfrog-cli-core/v2/common/project"
 	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
+	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
 	"github.com/jfrog/jfrog-client-go/artifactory/services"
 	"github.com/jfrog/jfrog-client-go/utils/errorutils"
 	"github.com/jfrog/jfrog-client-go/utils/log"
+	"golang.org/x/exp/maps"
+	"net/url"
+	"slices"
 )
 
+// packageManagerToRepositoryPackageType maps project types to corresponding Artifactory repository package types.
+var packageManagerToRepositoryPackageType = map[project.ProjectType]string{
+	// Npm package managers
+	project.Npm:  repository.Npm,
+	project.Yarn: repository.Npm,
+
+	// Python (pypi) package managers
+	project.Pip:    repository.Pypi,
+	project.Pipenv: repository.Pypi,
+	project.Poetry: repository.Pypi,
+
+	// Nuget package managers
+	project.Nuget:  repository.Nuget,
+	project.Dotnet: repository.Nuget,
+
+	// Docker package managers
+	project.Docker: repository.Docker,
+	project.Podman: repository.Docker,
+
+	project.Go: repository.Go,
+}
+
 // PackageManagerLoginCommand configures registries and authentication for various package manager (npm, Yarn, Pip, Pipenv, Poetry, Go)
 type PackageManagerLoginCommand struct {
 	// packageManager represents the type of package manager (e.g., NPM, Yarn).
 	packageManager project.ProjectType
 	// repoName is the name of the repository used for configuration.
 	repoName string
+	// projectKey is the JFrog Project key in JFrog Platform.
+	projectKey string
 	// serverDetails contains Artifactory server configuration.
 	serverDetails *config.ServerDetails
 	// commandName specifies the command for this instance.
@@ -40,20 +69,19 @@ func NewPackageManagerLoginCommand(packageManager project.ProjectType) *PackageM
 	}
 }
 
-// packageManagerToPackageType maps project types to corresponding Artifactory package types (e.g., npm, pypi).
-func packageManagerToPackageType(packageManager project.ProjectType) (string, error) {
-	switch packageManager {
-	case project.Npm, project.Yarn:
-		return repository.Npm, nil
-	case project.Pip, project.Pipenv, project.Poetry:
-		return repository.Pypi, nil
-	case project.Go:
-		return repository.Go, nil
-	case project.Nuget, project.Dotnet:
-		return repository.Nuget, nil
-	default:
-		return "", errorutils.CheckErrorf("unsupported package manager: %s", packageManager)
-	}
+// GetSupportedPackageManagersList returns a sorted list of supported package managers.
+func GetSupportedPackageManagersList() []project.ProjectType {
+	allSupportedPackageManagers := maps.Keys(packageManagerToRepositoryPackageType)
+	// Sort keys based on their natural enum order
+	slices.SortFunc(allSupportedPackageManagers, func(a, b project.ProjectType) int {
+		return int(a) - int(b)
+	})
+	return allSupportedPackageManagers
+}
+
+func IsSupportedPackageManager(packageManager project.ProjectType) bool {
+	_, exists := packageManagerToRepositoryPackageType[packageManager]
+	return exists
 }
 
 // CommandName returns the name of the login command.
@@ -72,8 +100,24 @@ func (pmlc *PackageManagerLoginCommand) ServerDetails() (*config.ServerDetails,
 	return pmlc.serverDetails, nil
 }
 
+// SetRepoName assigns the repository name to the command.
+func (pmlc *PackageManagerLoginCommand) SetRepoName(repoName string) *PackageManagerLoginCommand {
+	pmlc.repoName = repoName
+	return pmlc
+}
+
+// SetProjectKey assigns the project key to the command.
+func (pmlc *PackageManagerLoginCommand) SetProjectKey(projectKey string) *PackageManagerLoginCommand {
+	pmlc.projectKey = projectKey
+	return pmlc
+}
+
 // Run executes the configuration method corresponding to the package manager specified for the command.
 func (pmlc *PackageManagerLoginCommand) Run() (err error) {
+	if !IsSupportedPackageManager(pmlc.packageManager) {
+		return errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager)
+	}
+
 	if pmlc.repoName == "" {
 		// Prompt the user to select a virtual repository that matches the package manager.
 		if err = pmlc.promptUserToSelectRepository(); err != nil {
@@ -95,6 +139,8 @@ func (pmlc *PackageManagerLoginCommand) Run() (err error) {
 		err = pmlc.configureGo()
 	case project.Nuget, project.Dotnet:
 		err = pmlc.configureDotnetNuget()
+	case project.Docker, project.Podman:
+		err = pmlc.configureContainer()
 	default:
 		err = errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager)
 	}
@@ -102,20 +148,16 @@ func (pmlc *PackageManagerLoginCommand) Run() (err error) {
 		return fmt.Errorf("failed to configure %s: %w", pmlc.packageManager.String(), err)
 	}
 
-	log.Info(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", pmlc.packageManager.String(), pmlc.repoName))
+	log.Output(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", coreutils.PrintBoldTitle(pmlc.packageManager.String()), coreutils.PrintBoldTitle(pmlc.repoName)))
 	return nil
 }
 
 // promptUserToSelectRepository prompts the user to select a compatible virtual repository.
-func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() error {
-	// Map the package manager to its corresponding package type.
-	packageType, err := packageManagerToPackageType(pmlc.packageManager)
-	if err != nil {
-		return err
-	}
+func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() (err error) {
 	repoFilterParams := services.RepositoriesFilterParams{
 		RepoType:    utils.Virtual.String(),
-		PackageType: packageType,
+		PackageType: packageManagerToRepositoryPackageType[pmlc.packageManager],
+		ProjectKey:  pmlc.projectKey,
 	}
 
 	// Prompt for repository selection based on filter parameters.
@@ -239,3 +281,36 @@ func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error {
 	// Add the repository as a source in the NuGet configuration with credentials for authentication.
 	return dotnet.AddSourceToNugetConfig(toolchainType, sourceUrl, user, password)
 }
+
+// configureContainer configures container managers like Docker or Podman to authenticate with JFrog Artifactory.
+// It performs a login using the container manager's CLI command.
+//
+// For Docker:
+//
+//	echo <password> | docker login <artifactory-url-without-scheme> -u <username> --password-stdin
+//
+// For Podman:
+//
+//	echo <password> | podman login <artifactory-url-without-scheme> -u <username> --password-stdin
+func (pmlc *PackageManagerLoginCommand) configureContainer() error {
+	var containerManagerType container.ContainerManagerType
+	switch pmlc.packageManager {
+	case project.Docker:
+		containerManagerType = container.DockerClient
+	case project.Podman:
+		containerManagerType = container.Podman
+	default:
+		return errorutils.CheckErrorf("unsupported container manager: %s", pmlc.packageManager)
+	}
+	// Parse the URL to remove the scheme (https:// or http://)
+	parsedPlatformURL, err := url.Parse(pmlc.serverDetails.GetUrl())
+	if err != nil {
+		return err
+	}
+	urlWithoutScheme := parsedPlatformURL.Host + parsedPlatformURL.Path
+	return container.ContainerManagerLogin(
+		urlWithoutScheme,
+		&container.ContainerManagerLoginConfig{ServerDetails: pmlc.serverDetails},
+		containerManagerType,
+	)
+}
diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go b/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go
index 8bb95abde..9783e8133 100644
--- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go
+++ b/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go
@@ -13,6 +13,7 @@ import (
 	clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"golang.org/x/exp/slices"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -52,6 +53,13 @@ func createTestPackageManagerLoginCommand(packageManager project.ProjectType) *P
 	return cmd
 }
 
+func TestPackageManagerLoginCommand_NotSupported(t *testing.T) {
+	notSupportedLoginCmd := createTestPackageManagerLoginCommand(project.Cocoapods)
+	err := notSupportedLoginCmd.Run()
+	assert.Error(t, err)
+	assert.ErrorContains(t, err, "unsupported package manager")
+}
+
 func TestPackageManagerLoginCommand_Npm(t *testing.T) {
 	// Create a temporary directory to act as the environment's npmrc file location.
 	tempDir := t.TempDir()
@@ -381,3 +389,20 @@ func testBuildToolLoginCommandConfigureDotnetNuget(t *testing.T, packageManager
 		})
 	}
 }
+
+func TestGetSupportedPackageManagersList(t *testing.T) {
+	result := GetSupportedPackageManagersList()
+	// Check that Go is before Pip, and Pip is before Npm using GreaterOrEqual
+	assert.GreaterOrEqual(t, slices.Index(result, project.Pip), slices.Index(result, project.Go), "Go should come before Pip")
+	assert.GreaterOrEqual(t, slices.Index(result, project.Npm), slices.Index(result, project.Pip), "Pip should come before Npm")
+}
+
+func TestIsSupportedPackageManager(t *testing.T) {
+	// Test valid package managers
+	for pm := range packageManagerToRepositoryPackageType {
+		assert.True(t, IsSupportedPackageManager(pm), "Package manager %s should be supported", pm)
+	}
+
+	// Test unsupported package manager
+	assert.False(t, IsSupportedPackageManager(project.Cocoapods), "Package manager Cocoapods should not be supported")
+}
diff --git a/artifactory/utils/container/containermanager.go b/artifactory/utils/container/containermanager.go
index 8c16c6659..9867352bc 100644
--- a/artifactory/utils/container/containermanager.go
+++ b/artifactory/utils/container/containermanager.go
@@ -189,11 +189,7 @@ func (loginCmd *LoginCmd) RunCmd() error {
 
 // First we'll try to log in assuming a proxy-less tag (e.g. "registry-address/docker-repo/image:ver").
 // If fails, we will try assuming a reverse proxy tag (e.g. "registry-address-docker-repo/image:ver").
-func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error {
-	imageRegistry, err := image.GetRegistry()
-	if err != nil {
-		return err
-	}
+func ContainerManagerLogin(imageRegistry string, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error {
 	username := config.ServerDetails.User
 	password := config.ServerDetails.Password
 	// If access-token exists, perform login with it.
@@ -206,7 +202,7 @@ func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, co
 	}
 	// Perform login.
 	cmd := &LoginCmd{DockerRegistry: imageRegistry, Username: username, Password: password, containerManager: containerManager}
-	err = cmd.RunCmd()
+	err := cmd.RunCmd()
 	if exitCode := coreutils.GetExitCode(err, 0, 0, false); exitCode == coreutils.ExitCodeNoError {
 		// Login succeeded
 		return nil
diff --git a/artifactory/utils/repositoryutils.go b/artifactory/utils/repositoryutils.go
index 40e11b30f..1e3aa39a6 100644
--- a/artifactory/utils/repositoryutils.go
+++ b/artifactory/utils/repositoryutils.go
@@ -1,6 +1,7 @@
 package utils
 
 import (
+	"fmt"
 	"github.com/jfrog/gofrog/datastructures"
 	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
 	"github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
@@ -101,7 +102,7 @@ func SelectRepositoryInteractively(serverDetails *config.ServerDetails, repoFilt
 		return filteredRepos[0], nil
 	}
 	// Prompt the user to select a repository.
-	return ioutils.AskFromListWithMismatchConfirmation("Please select a repository to login to:", "Repository not found.", ioutils.ConvertToSuggests(filteredRepos)), nil
+	return ioutils.AskFromListWithMismatchConfirmation(fmt.Sprintf("Please select a %s %s repository to configure:", repoFilterParams.RepoType, repoFilterParams.PackageType), "Repository not found.", ioutils.ConvertToSuggests(filteredRepos)), nil
 }
 
 // GetFilteredRepositoriesWithFilterParams returns the names of local, remote, virtual, and federated repositories filtered by their names and type.
diff --git a/common/project/projectconfig.go b/common/project/projectconfig.go
index 92134ae75..646f014eb 100644
--- a/common/project/projectconfig.go
+++ b/common/project/projectconfig.go
@@ -26,6 +26,7 @@ const (
 type ProjectType int
 
 const (
+	// When adding new ProjectType here, Must also add it as a string to the ProjectTypes slice
 	Go ProjectType = iota
 	Pip
 	Pipenv
@@ -41,6 +42,8 @@ const (
 	Terraform
 	Cocoapods
 	Swift
+	Docker
+	Podman
 )
 
 type ConfigType string
@@ -66,12 +69,24 @@ var ProjectTypes = []string{
 	"terraform",
 	"cocoapods",
 	"swift",
+	"docker",
+	"podman",
 }
 
 func (projectType ProjectType) String() string {
 	return ProjectTypes[projectType]
 }
 
+// FromString converts a string to its corresponding ProjectType
+func FromString(value string) ProjectType {
+	for i, projectType := range ProjectTypes {
+		if projectType == value {
+			return ProjectType(i)
+		}
+	}
+	return -1
+}
+
 type MissingResolverErr struct {
 	message string
 }
diff --git a/common/project/projectconfig_test.go b/common/project/projectconfig_test.go
new file mode 100644
index 000000000..9a7edafc5
--- /dev/null
+++ b/common/project/projectconfig_test.go
@@ -0,0 +1,30 @@
+package project
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestFromString(t *testing.T) {
+	// Test valid conversions
+	testCases := []struct {
+		input    string
+		expected ProjectType
+	}{
+		{"go", Go},
+		{"pip", Pip},
+		{"npm", Npm},
+		{"pnpm", Pnpm},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.input, func(t *testing.T) {
+			result := FromString(testCase.input)
+			assert.Equal(t, testCase.expected, result)
+		})
+	}
+
+	// Test invalid conversion
+	result := FromString("InvalidProject")
+	assert.Equal(t, ProjectType(-1), result)
+}
diff --git a/utils/ioutils/questionnaire.go b/utils/ioutils/questionnaire.go
index 74459ac43..40ba1b965 100644
--- a/utils/ioutils/questionnaire.go
+++ b/utils/ioutils/questionnaire.go
@@ -82,7 +82,7 @@ func interruptKeyBind() prompt.Option {
 	interrupt := prompt.KeyBind{
 		Key: prompt.ControlC,
 		Fn: func(buf *prompt.Buffer) {
-			panic("Interrupted")
+			panic("Operation interrupted. Exiting...")
 		},
 	}
 	return prompt.OptionAddKeyBind(interrupt)
@@ -177,7 +177,7 @@ func validateAnswer(answer string, options []prompt.Suggest, allowVars bool) boo
 // If the provided answer does not appear in list, confirm the choice.
 func AskFromListWithMismatchConfirmation(promptPrefix, misMatchMsg string, options []prompt.Suggest) string {
 	for {
-		answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind())
+		answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind(), prompt.OptionShowCompletionAtStart(), prompt.OptionCompletionOnDown())
 		if answer == "" {
 			log.Output(EmptyValueMsg)
 		}

From b26e9a6644c66fb791b4f14816de679764233034 Mon Sep 17 00:00:00 2001
From: Assaf Attias <49212512+attiasas@users.noreply.github.com>
Date: Wed, 1 Jan 2025 13:08:57 +0200
Subject: [PATCH 2/4] Test Cli outputs of cmd with errors (#1324)

---
 utils/tests/test_cli.go | 28 ++++++++++++++++++++++------
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/utils/tests/test_cli.go b/utils/tests/test_cli.go
index 8c300f45d..8c4a1cc6c 100644
--- a/utils/tests/test_cli.go
+++ b/utils/tests/test_cli.go
@@ -52,21 +52,37 @@ func (cli *JfrogCli) RunCliCmdWithOutput(t *testing.T, args ...string) string {
 	return RunCmdWithOutput(t, func() error { return cli.Exec(args...) })
 }
 
-// Run a command, redirect the stdout and return the output
-func RunCmdWithOutput(t *testing.T, executeCmd func() error) string {
+func (cli *JfrogCli) RunCliCmdWithOutputs(t *testing.T, args ...string) (string, error) {
+	return RunCmdWithOutputs(t, func() error { return cli.Exec(args...) })
+}
+
+func RunCmdWithOutputs(t *testing.T, executeCmd func() error) (output string, err error) {
 	newStdout, stdWriter, cleanUp := redirectOutToPipe(t)
 	defer cleanUp()
 
+	errCh := make(chan error, 1)
+
 	go func() {
-		assert.NoError(t, executeCmd())
+		errCh <- executeCmd()
 		// Closing the temp stdout in order to be able to read it's content.
 		assert.NoError(t, stdWriter.Close())
 	}()
 
-	content, err := io.ReadAll(newStdout)
+	content, e := io.ReadAll(newStdout)
+	assert.NoError(t, e)
+	output = string(content)
+	log.Debug(output)
+
+	err = <-errCh
+
+	return
+}
+
+// Run a command, redirect the stdout and return the output
+func RunCmdWithOutput(t *testing.T, executeCmd func() error) string {
+	output, err := RunCmdWithOutputs(t, executeCmd)
 	assert.NoError(t, err)
-	log.Debug(string(content))
-	return string(content)
+	return output
 }
 
 func redirectOutToPipe(t *testing.T) (*os.File, *os.File, func()) {

From c90c38c1ace9ae9848bbb1d36b13c5f09817b18d Mon Sep 17 00:00:00 2001
From: Eyal Delarea <eyalde@jfrog.com>
Date: Mon, 6 Jan 2025 16:38:24 +0200
Subject: [PATCH 3/4] Command Summary - Add visibility system query params
 (#1325)

---
 artifactory/utils/commandsummary/utils.go     | 24 ++++++++++++++-----
 .../utils/commandsummary/utils_test.go        | 20 +++++++++++-----
 .../extended/build-info-table.md              |  6 ++---
 .../extended/docker-image-module.md           |  2 +-
 .../extended/generic-module.md                |  2 +-
 .../extended/maven-module.md                  |  2 +-
 .../extended/maven-nested-module.md           |  4 ++--
 .../extended/multiarch-docker-image.md        |  2 +-
 utils/usage/visibility_system_manager.go      |  2 +-
 9 files changed, 42 insertions(+), 22 deletions(-)

diff --git a/artifactory/utils/commandsummary/utils.go b/artifactory/utils/commandsummary/utils.go
index f9e51b3ec..75b9541e9 100644
--- a/artifactory/utils/commandsummary/utils.go
+++ b/artifactory/utils/commandsummary/utils.go
@@ -49,26 +49,38 @@ const (
 	buildInfoSection summarySection = "buildInfo"
 )
 
-// addGitHubTrackingToUrl adds GitHub-related query parameters to a given URL if the GITHUB_WORKFLOW environment variable is set.
+const (
+	// The source of the request
+	sourceParamKey    = "s"
+	githubSourceValue = "1"
+	// The metric to track
+	metricParamKey    = "m"
+	githubMetricValue = "3"
+
+	jobIDKey       = "gh_job_id"
+	sectionKey     = "gh_section"
+	workflowEnvKey = "GITHUB_WORKFLOW"
+)
+
 func addGitHubTrackingToUrl(urlStr string, section summarySection) (string, error) {
 	// Check if GITHUB_WORKFLOW environment variable is set
-	githubWorkflow := os.Getenv("GITHUB_WORKFLOW")
+	githubWorkflow := os.Getenv(workflowEnvKey)
 	if githubWorkflow == "" {
-		// Return the original URL if the variable is not set
 		return urlStr, nil
 	}
 
 	// Parse the input URL
 	parsedUrl, err := url.Parse(urlStr)
 	if errorutils.CheckError(err) != nil {
-		// Return an error if the URL is invalid
 		return "", err
 	}
 
 	// Get the query parameters and add the GitHub tracking parameters
 	queryParams := parsedUrl.Query()
-	queryParams.Set("gh_job_id", githubWorkflow)
-	queryParams.Set("gh_section", string(section))
+	queryParams.Set(sourceParamKey, githubSourceValue)
+	queryParams.Set(metricParamKey, githubMetricValue)
+	queryParams.Set(jobIDKey, githubWorkflow)
+	queryParams.Set(sectionKey, string(section))
 	parsedUrl.RawQuery = queryParams.Encode()
 
 	// Return the modified URL
diff --git a/artifactory/utils/commandsummary/utils_test.go b/artifactory/utils/commandsummary/utils_test.go
index 5473df4d0..62dc669ac 100644
--- a/artifactory/utils/commandsummary/utils_test.go
+++ b/artifactory/utils/commandsummary/utils_test.go
@@ -19,9 +19,9 @@ func TestGenerateArtifactUrl(t *testing.T) {
 		majorVersion int
 		expected     string
 	}{
-		{"artifactory 7 without project", "", 7, "https://myplatform.com/ui/repos/tree/General/repo/path/file?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section"},
-		{"artifactory 7 with project", "proj", 7, "https://myplatform.com/ui/repos/tree/General/repo/path/file?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section"},
-		{"artifactory 6 without project", "", 6, "https://myplatform.com/artifactory/webapp/?gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section#/artifacts/browse/tree/General/repo/path/file"},
+		{"artifactory 7 without project", "", 7, "https://myplatform.com/ui/repos/tree/General/repo/path/file?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section&m=3&s=1"},
+		{"artifactory 7 with project", "proj", 7, "https://myplatform.com/ui/repos/tree/General/repo/path/file?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section&m=3&s=1"},
+		{"artifactory 6 without project", "", 6, "https://myplatform.com/artifactory/webapp/?gh_job_id=JFrog+CLI+Core+Tests&gh_section=test-section&m=3&s=1#/artifacts/browse/tree/General/repo/path/file"},
 	}
 	StaticMarkdownConfig.setPlatformUrl(testPlatformUrl)
 	for _, testCase := range cases {
@@ -71,7 +71,7 @@ func TestAddGitHubTrackingToUrl(t *testing.T) {
 			"https://example.com/path",
 			buildInfoSection,
 			"workflow123",
-			"https://example.com/path?gh_job_id=workflow123&gh_section=buildInfo",
+			"https://example.com/path?gh_job_id=workflow123&gh_section=buildInfo&m=3&s=1",
 			false,
 		},
 		{
@@ -87,7 +87,7 @@ func TestAddGitHubTrackingToUrl(t *testing.T) {
 			"https://example.com/path?existing_param=value",
 			packagesSection,
 			"workflow123",
-			"https://example.com/path?existing_param=value&gh_job_id=workflow123&gh_section=packages",
+			"https://example.com/path?existing_param=value&gh_job_id=workflow123&gh_section=packages&m=3&s=1",
 			false,
 		},
 		{
@@ -95,7 +95,15 @@ func TestAddGitHubTrackingToUrl(t *testing.T) {
 			"https://example.com/path",
 			artifactsSection,
 			"workflow with spaces & special?characters",
-			"https://example.com/path?gh_job_id=workflow+with+spaces+%26+special%3Fcharacters&gh_section=artifacts",
+			"https://example.com/path?gh_job_id=workflow+with+spaces+%26+special%3Fcharacters&gh_section=artifacts&m=3&s=1",
+			false,
+		},
+		{
+			"URL with spaces",
+			"https://example.com/path?existing_param=value",
+			packagesSection,
+			"workflow space",
+			"https://example.com/path?existing_param=value&gh_job_id=workflow+space&gh_section=packages&m=3&s=1",
 			false,
 		},
 	}
diff --git a/artifactory/utils/testdata/command_summaries/extended/build-info-table.md b/artifactory/utils/testdata/command_summaries/extended/build-info-table.md
index 5d0bb212c..9dba95552 100644
--- a/artifactory/utils/testdata/command_summaries/extended/build-info-table.md
+++ b/artifactory/utils/testdata/command_summaries/extended/build-info-table.md
@@ -1,7 +1,7 @@
 
 
-|  Build Info|  Security Violations| Security Issues|
-|:---------|:------------|:------------|
-| [buildName 123](http://myJFrogPlatform/builds/buildName/123?gh_job_id=JFrog+CLI+Core+Tests&gh_section=buildInfo) | Not scanned| Not scanned| 
+| Build Info | Security Violations | Security Issues |
+| :--------- | :------------ | :------------ |
+| [buildName 123](http://myJFrogPlatform/builds/buildName/123?gh_job_id=JFrog+CLI+Core+Tests&gh_section=buildInfo&m=3&s=1) | Not scanned | Not scanned |
 
 
diff --git a/artifactory/utils/testdata/command_summaries/extended/docker-image-module.md b/artifactory/utils/testdata/command_summaries/extended/docker-image-module.md
index 2300c23c0..7e57d9f5e 100644
--- a/artifactory/utils/testdata/command_summaries/extended/docker-image-module.md
+++ b/artifactory/utils/testdata/command_summaries/extended/docker-image-module.md
@@ -10,4 +10,4 @@
 
 | Artifacts | Security Violations | Security Issues |
 | :------------ | :--------------------- | :------------------ |
-| <pre>📦 docker-local<br>└── 📁 image2<br>    └── 📁 sha256:552c<br>        └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/image2/sha256:552c/sha256__aae9?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">sha256__aae9</a><br><br></pre> | Not scanned | Not scanned |
+| <pre>📦 docker-local<br>└── 📁 image2<br>    └── 📁 sha256:552c<br>        └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/image2/sha256:552c/sha256__aae9?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">sha256__aae9</a><br><br></pre> | Not scanned | Not scanned |
diff --git a/artifactory/utils/testdata/command_summaries/extended/generic-module.md b/artifactory/utils/testdata/command_summaries/extended/generic-module.md
index 33e1fe0f1..c8d6e0aff 100644
--- a/artifactory/utils/testdata/command_summaries/extended/generic-module.md
+++ b/artifactory/utils/testdata/command_summaries/extended/generic-module.md
@@ -11,7 +11,7 @@
 <pre>📦 generic-local
 └── 📁 path
     └── 📁 to
-        └── <a href='https://myplatform.com/ui/repos/tree/General/generic-local/path/to/artifact2?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=artifacts' target="_blank">artifact2</a>
+        └── <a href='https://myplatform.com/ui/repos/tree/General/generic-local/path/to/artifact2?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=artifacts&m=3&s=1' target="_blank">artifact2</a>
 
 </pre>
 
diff --git a/artifactory/utils/testdata/command_summaries/extended/maven-module.md b/artifactory/utils/testdata/command_summaries/extended/maven-module.md
index c1454b39c..05eb108f6 100644
--- a/artifactory/utils/testdata/command_summaries/extended/maven-module.md
+++ b/artifactory/utils/testdata/command_summaries/extended/maven-module.md
@@ -11,7 +11,7 @@
 <pre>📦 libs-release
 └── 📁 path
     └── 📁 to
-        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact1?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">artifact1</a>
+        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact1?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">artifact1</a>
 
 </pre>
 
diff --git a/artifactory/utils/testdata/command_summaries/extended/maven-nested-module.md b/artifactory/utils/testdata/command_summaries/extended/maven-nested-module.md
index 5e6d5b90c..178250a80 100644
--- a/artifactory/utils/testdata/command_summaries/extended/maven-nested-module.md
+++ b/artifactory/utils/testdata/command_summaries/extended/maven-nested-module.md
@@ -12,7 +12,7 @@
 📦 libs-release
 └── 📁 path
     └── 📁 to
-        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact2?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">artifact2</a>
+        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact2?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">artifact2</a>
 
 </details></pre>
 
@@ -22,7 +22,7 @@
 📦 libs-release
 └── 📁 path
     └── 📁 to
-        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact3?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">artifact3</a>
+        └── <a href='https://myplatform.com/ui/repos/tree/General/libs-release/path/to/artifact3?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">artifact3</a>
 
 </details></pre>
 
diff --git a/artifactory/utils/testdata/command_summaries/extended/multiarch-docker-image.md b/artifactory/utils/testdata/command_summaries/extended/multiarch-docker-image.md
index 5dfc53802..f3df8aea3 100644
--- a/artifactory/utils/testdata/command_summaries/extended/multiarch-docker-image.md
+++ b/artifactory/utils/testdata/command_summaries/extended/multiarch-docker-image.md
@@ -10,4 +10,4 @@
 
 | Artifacts | Security Violations | Security Issues |
 | :------------ | :--------------------- | :------------------ |
-| <pre><details><summary>linux/amd64/multiarch-image:1 <a href=https://myplatform.com/ui/packages/docker:%2F%2Fmultiarch-image/sha256__sha256:552c>(🐸 View)</a></summary><br>📦 docker-local<br>└── 📁 multiarch-image<br>    ├── 📁 sha256:552c<br>    │   └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/multiarch-image/sha256:552c/sha256?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">sha256</a><br>    └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/multiarch-image/sha256?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages' target="_blank">sha256</a><br><br></details></pre> | Not scanned | Not scanned |
+| <pre><details><summary>linux/amd64/multiarch-image:1 <a href=https://myplatform.com/ui/packages/docker:%2F%2Fmultiarch-image/sha256__sha256:552c>(🐸 View)</a></summary><br>📦 docker-local<br>└── 📁 multiarch-image<br>    ├── 📁 sha256:552c<br>    │   └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/multiarch-image/sha256:552c/sha256?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">sha256</a><br>    └── <a href='https://myplatform.com/ui/repos/tree/General/docker-local/multiarch-image/sha256?clearFilter=true&gh_job_id=JFrog+CLI+Core+Tests&gh_section=packages&m=3&s=1' target="_blank">sha256</a><br><br></details></pre> | Not scanned | Not scanned |
diff --git a/utils/usage/visibility_system_manager.go b/utils/usage/visibility_system_manager.go
index ac030d5b2..6b23c9aa8 100644
--- a/utils/usage/visibility_system_manager.go
+++ b/utils/usage/visibility_system_manager.go
@@ -39,7 +39,7 @@ func (vsm *VisibilitySystemManager) createMetric(commandName string) services.Vi
 }
 
 func (vsm *VisibilitySystemManager) SendUsage(commandName string) error {
-	manager, err := utils.CreateJfConnectServiceManager(vsm.serverDetails, 1, 0)
+	manager, err := utils.CreateJfConnectServiceManager(vsm.serverDetails, 0, 0)
 	if err != nil {
 		return err
 	}

From 506d2fe363172e009019f61ba6602847c6f9f39b Mon Sep 17 00:00:00 2001
From: Eyal Delarea <eyalde@jfrog.com>
Date: Tue, 7 Jan 2025 13:39:44 +0200
Subject: [PATCH 4/4] Update dependencies (#1328)

---
 go.mod | 16 ++++++++--------
 go.sum | 32 ++++++++++++++++----------------
 2 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/go.mod b/go.mod
index e36659a68..702fa6e99 100644
--- a/go.mod
+++ b/go.mod
@@ -12,9 +12,9 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/gookit/color v1.5.4
 	github.com/jedib0t/go-pretty/v6 v6.6.5
-	github.com/jfrog/build-info-go v1.10.7
+	github.com/jfrog/build-info-go v1.10.8
 	github.com/jfrog/gofrog v1.7.6
-	github.com/jfrog/jfrog-client-go v1.48.6
+	github.com/jfrog/jfrog-client-go v1.49.0
 	github.com/magiconair/properties v1.8.9
 	github.com/manifoldco/promptui v0.9.0
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
@@ -22,10 +22,10 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/urfave/cli v1.22.16
 	github.com/vbauerster/mpb/v8 v8.8.3
-	golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
 	golang.org/x/mod v0.22.0
 	golang.org/x/sync v0.10.0
-	golang.org/x/term v0.27.0
+	golang.org/x/term v0.28.0
 	golang.org/x/text v0.21.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -88,10 +88,10 @@ require (
 	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
-	golang.org/x/crypto v0.31.0 // indirect
-	golang.org/x/net v0.33.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/tools v0.27.0 // indirect
+	golang.org/x/crypto v0.32.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/tools v0.29.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 )
diff --git a/go.sum b/go.sum
index c1956415a..ffce8e369 100644
--- a/go.sum
+++ b/go.sum
@@ -89,12 +89,12 @@ github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3N
 github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs=
 github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI=
 github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw=
-github.com/jfrog/build-info-go v1.10.7 h1:10NVHYg0193gJpQft+S4WQfvYMtj5jlwwhJRvkFJtBE=
-github.com/jfrog/build-info-go v1.10.7/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE=
+github.com/jfrog/build-info-go v1.10.8 h1:8D4wtvKzLS1hzfDWtfH4OliZLtLCgL62tXCnGWDXuac=
+github.com/jfrog/build-info-go v1.10.8/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE=
 github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s=
 github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4=
-github.com/jfrog/jfrog-client-go v1.48.6 h1:Pl9foa9XBaPbP3gz4wevDmF/mpfit94IQHKQKnfk3lA=
-github.com/jfrog/jfrog-client-go v1.48.6/go.mod h1:2ySOMva54L3EYYIlCBYBTcTgqfrrQ19gtpA/MWfA/ec=
+github.com/jfrog/jfrog-client-go v1.49.0 h1:NaTK6+LQBEJafL//6ntnS/eVx1dZMJnxydALwWHKORQ=
+github.com/jfrog/jfrog-client-go v1.49.0/go.mod h1:ohIfKpMBCQsE9kunrKQ1wvoExpqsPLaluRFO186B5EM=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@@ -222,15 +222,15 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
 go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
-golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
 golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -252,17 +252,17 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
-golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=