Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion artifactory/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const (
)

func GetCommands() []components.Command {
return []components.Command{
commands := []components.Command{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of having commands here in artifactory/cli/cli.go it is looking good to move them as separate commands for jfrog-cli similar to lifecycle commands.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something similar to jf vscode config jf jetbrains config so that the command vscode can have its own sub commands like jf vscode install etc

{
Name: "upload",
Flags: flagkit.GetCommandFlags(flagkit.Upload),
Expand Down Expand Up @@ -404,6 +404,8 @@ func GetCommands() []components.Command {
Category: replicCategory,
},
}

return commands
}

func getRetries(c *components.Context) (retries int, err error) {
Expand Down
68 changes: 68 additions & 0 deletions artifactory/cli/ide/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package ide

import (
"fmt"
"strings"

pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
)

// ValidateSingleNonEmptyArg checks that there is exactly one argument and it is not empty.
func ValidateSingleNonEmptyArg(c *components.Context, usage string) (string, error) {
if c.GetNumberOfArgs() != 1 {
return "", pluginsCommon.WrongNumberOfArgumentsHandler(c)
}
arg := c.GetArgumentAt(0)
if arg == "" {
return "", fmt.Errorf("argument cannot be empty\n\nUsage: %s", usage)
}
return arg, nil
}

// HasServerConfigFlags checks if any server configuration flags are provided
func HasServerConfigFlags(c *components.Context) bool {
return c.IsFlagSet("url") ||
c.IsFlagSet("user") ||
c.IsFlagSet("access-token") ||
c.IsFlagSet("server-id") ||
// Only consider password if other required fields are also provided
(c.IsFlagSet("password") && (c.IsFlagSet("url") || c.IsFlagSet("server-id")))
}

// ExtractRepoKeyFromURL extracts the repository key from both JetBrains and VSCode extension URLs.
// For JetBrains: https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins
// For VSCode: https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery
// Returns the repo key (e.g., "jetbrains-plugins" or "vscode-extensions")
func ExtractRepoKeyFromURL(repoURL string) (string, error) {
if repoURL == "" {
return "", fmt.Errorf("URL is empty")
}

url := strings.TrimSpace(repoURL)
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
url = strings.TrimSuffix(url, "/")

// Check for JetBrains plugins API
if idx := strings.Index(url, "/api/jetbrainsplugins/"); idx != -1 {
rest := url[idx+len("/api/jetbrainsplugins/"):]
parts := strings.SplitN(rest, "/", 2)
if len(parts) == 0 || parts[0] == "" {
return "", fmt.Errorf("repository key not found in JetBrains URL")
}
return parts[0], nil
}

// Check for VSCode extensions API
if idx := strings.Index(url, "/api/vscodeextensions/"); idx != -1 {
rest := url[idx+len("/api/vscodeextensions/"):]
parts := strings.SplitN(rest, "/", 2)
if len(parts) == 0 || parts[0] == "" {
return "", fmt.Errorf("repository key not found in VSCode URL")
}
return parts[0], nil
}

return "", fmt.Errorf("URL does not contain a supported API type (/api/jetbrainsplugins/ or /api/vscodeextensions/)")
}
42 changes: 42 additions & 0 deletions artifactory/cli/ide/descriptions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ide

const VscodeConfigDescription = `
Configure VSCode to use JFrog Artifactory for extensions.

The service URL should be in the format:
https://<artifactory-url>/artifactory/api/vscodeextensions/<repo-key>/_apis/public/gallery

Examples:
jf vscode-config https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery

This command will:
- Modify the VSCode product.json file to change the extensions gallery URL
- Create an automatic backup before making changes
- Require VSCode to be restarted to apply changes

Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id)
to enable repository validation. Without these flags, the command will only modify the local VSCode configuration.

Note: On macOS/Linux, you may need to run with sudo for system-installed VSCode.
`

const JetbrainsConfigDescription = `
Configure JetBrains IDEs to use JFrog Artifactory for plugins.

The repository URL should be in the format:
https://<artifactory-url>/artifactory/api/jetbrainsplugins/<repo-key>

Examples:
jf jetbrains-config https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins

This command will:
- Detect all installed JetBrains IDEs
- Modify each IDE's idea.properties file to add the plugins repository URL
- Create automatic backups before making changes
- Require IDEs to be restarted to apply changes

Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id)
to enable repository validation. Without these flags, the command will only modify the local IDE configuration.

Supported IDEs: IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, RubyMine, CLion, DataGrip, GoLand, Rider, Android Studio, AppCode, RustRover, Aqua
`
147 changes: 147 additions & 0 deletions artifactory/cli/ide/jetbrains/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package jetbrains

import (
"fmt"
"net/url"
"strings"

"github.com/jfrog/gofrog/log"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide"
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ide/jetbrains"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
)

const (
repoKeyFlag = "repo-key"
urlSuffixFlag = "url-suffix"
apiType = "jetbrainsplugins"
)

func GetCommands() []components.Command {
return []components.Command{
{
Name: "jetbrains-config",
Aliases: []string{"jb"},
Hidden: true,
Flags: getFlags(),
Arguments: getArguments(),
Action: jetbrainsConfigCmd,
Description: ide.JetbrainsConfigDescription,
},
}
}

func getFlags() []components.Flag {
return []components.Flag{
components.NewStringFlag(repoKeyFlag, "Repository key for the JetBrains plugins repo. [Required if no URL is given]", components.SetMandatoryFalse()),
components.NewStringFlag(urlSuffixFlag, "Suffix for the JetBrains plugins repository URL. Default: (empty)", components.SetMandatoryFalse()),
// Server configuration flags
components.NewStringFlag("url", "JFrog Artifactory URL. (example: https://acme.jfrog.io/artifactory)", components.SetMandatoryFalse()),
components.NewStringFlag("user", "JFrog username.", components.SetMandatoryFalse()),
components.NewStringFlag("password", "JFrog password.", components.SetMandatoryFalse()),
components.NewStringFlag("access-token", "JFrog access token.", components.SetMandatoryFalse()),
components.NewStringFlag("server-id", "Server ID configured using the 'jf config' command.", components.SetMandatoryFalse()),
}
}

func getArguments() []components.Argument {
return []components.Argument{
{
Name: "repository-url",
Description: "The Artifactory JetBrains plugins repository URL (optional when using --repo-key)",
Optional: true,
},
}
}

// Main command action: orchestrates argument parsing, server config, and command execution
func jetbrainsConfigCmd(c *components.Context) error {
repoKey, repositoryURL, err := getJetbrainsRepoKeyAndURL(c)
if err != nil {
return err
}

rtDetails, err := getJetbrainsServerDetails(c)
if err != nil {
return err
}

jetbrainsCmd := jetbrains.NewJetbrainsCommand(repositoryURL, repoKey)

// Determine if this is a direct URL (argument provided) vs constructed URL (server-id + repo-key)
isDirectURL := c.GetNumberOfArgs() > 0 && isValidUrl(c.GetArgumentAt(0))
jetbrainsCmd.SetDirectURL(isDirectURL)

if rtDetails != nil {
jetbrainsCmd.SetServerDetails(rtDetails)
}

return jetbrainsCmd.Run()
}

// getJetbrainsRepoKeyAndURL determines the repo key and repository URL from args/flags
func getJetbrainsRepoKeyAndURL(c *components.Context) (repoKey, repositoryURL string, err error) {
if c.GetNumberOfArgs() > 0 && isValidUrl(c.GetArgumentAt(0)) {
repositoryURL = c.GetArgumentAt(0)
repoKey, err = ide.ExtractRepoKeyFromURL(repositoryURL)
if err != nil {
return
}
return
}

repoKey = c.GetStringFlagValue(repoKeyFlag)
if repoKey == "" {
err = fmt.Errorf("You must provide either a repository URL as the first argument or --repo-key flag.")
return
}
// Get Artifactory URL from server details (flags or default)
var artDetails *config.ServerDetails
if ide.HasServerConfigFlags(c) {
artDetails, err = pluginsCommon.CreateArtifactoryDetailsByFlags(c)
if err != nil {
err = fmt.Errorf("Failed to get Artifactory server details: %w", err)
return
}
} else {
artDetails, err = config.GetDefaultServerConf()
if err != nil {
err = fmt.Errorf("Failed to get default Artifactory server details: %w", err)
return
}
}
baseUrl := strings.TrimRight(artDetails.Url, "/")
urlSuffix := c.GetStringFlagValue(urlSuffixFlag)
if urlSuffix != "" {
urlSuffix = "/" + strings.TrimLeft(urlSuffix, "/")
}
repositoryURL = baseUrl + "/artifactory/api/jetbrainsplugins/" + repoKey + urlSuffix
return
}

// getJetbrainsServerDetails returns server details for validation, or nil if not available
func getJetbrainsServerDetails(c *components.Context) (*config.ServerDetails, error) {
if ide.HasServerConfigFlags(c) {
// Use explicit server configuration flags
rtDetails, err := pluginsCommon.CreateArtifactoryDetailsByFlags(c)
if err != nil {
return nil, fmt.Errorf("failed to create server configuration: %w", err)
}
return rtDetails, nil
}
// Use default server configuration for validation when no explicit flags provided
rtDetails, err := config.GetDefaultServerConf()
if err != nil {
// If no default server, that's okay - we'll just skip validation
log.Debug("No default server configuration found, skipping repository validation")
return nil, nil //nolint:nilerr // Intentionally ignoring error to skip validation when no default server
}
return rtDetails, nil
}

func isValidUrl(s string) bool {
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != ""
}
72 changes: 72 additions & 0 deletions artifactory/cli/ide/jetbrains/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package jetbrains

import (
"testing"

"github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/stretchr/testify/assert"
)

// ... existing code ...

func TestHasServerConfigFlags(t *testing.T) {
tests := []struct {
name string
flags map[string]string
expected bool
}{
{
name: "No flags",
flags: map[string]string{},
expected: false,
},
{
name: "Only password flag",
flags: map[string]string{"password": "mypass"},
expected: false,
},
{
name: "Password and URL flags",
flags: map[string]string{"password": "mypass", "url": "https://example.com"},
expected: true,
},
{
name: "Password and server-id flags",
flags: map[string]string{"password": "mypass", "server-id": "my-server"},
expected: true,
},
{
name: "URL flag only",
flags: map[string]string{"url": "https://example.com"},
expected: true,
},
{
name: "User flag only",
flags: map[string]string{"user": "myuser"},
expected: true,
},
{
name: "Access token flag only",
flags: map[string]string{"access-token": "mytoken"},
expected: true,
},
{
name: "Server ID flag only",
flags: map[string]string{"server-id": "my-server"},
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := &components.Context{}
for flag, value := range tt.flags {
ctx.AddStringFlag(flag, value)
}

result := ide.HasServerConfigFlags(ctx)
assert.Equal(t, tt.expected, result, "Test case: %s", tt.name)
})
}
}
Loading
Loading