Skip to content

Commit fe395ae

Browse files
agrasthnaveenku-jfrog
authored andcommitted
feat: Add IDE setup functionality to jfrog-cli-artifactory (jfrog#99)
1 parent 87615e1 commit fe395ae

File tree

15 files changed

+2171
-2
lines changed

15 files changed

+2171
-2
lines changed

artifactory/cli/cli.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const (
8383
)
8484

8585
func GetCommands() []components.Command {
86-
return []components.Command{
86+
commands := []components.Command{
8787
{
8888
Name: "upload",
8989
Flags: flagkit.GetCommandFlags(flagkit.Upload),
@@ -404,6 +404,8 @@ func GetCommands() []components.Command {
404404
Category: replicCategory,
405405
},
406406
}
407+
408+
return commands
407409
}
408410

409411
func getRetries(c *components.Context) (retries int, err error) {

artifactory/cli/ide/common.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package ide
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strings"
7+
8+
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
9+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
10+
)
11+
12+
// ValidateSingleNonEmptyArg checks that there is exactly one argument and it is not empty.
13+
func ValidateSingleNonEmptyArg(c *components.Context, usage string) (string, error) {
14+
if c.GetNumberOfArgs() != 1 {
15+
return "", pluginsCommon.WrongNumberOfArgumentsHandler(c)
16+
}
17+
arg := c.GetArgumentAt(0)
18+
if arg == "" {
19+
return "", fmt.Errorf("argument cannot be empty\n\nUsage: %s", usage)
20+
}
21+
return arg, nil
22+
}
23+
24+
// HasServerConfigFlags checks if any server configuration flags are provided
25+
func HasServerConfigFlags(c *components.Context) bool {
26+
return c.IsFlagSet("url") ||
27+
c.IsFlagSet("user") ||
28+
c.IsFlagSet("access-token") ||
29+
c.IsFlagSet("server-id") ||
30+
// Only consider password if other required fields are also provided
31+
(c.IsFlagSet("password") && (c.IsFlagSet("url") || c.IsFlagSet("server-id")))
32+
}
33+
34+
// ExtractRepoKeyFromURL extracts the repository key from both JetBrains and VSCode extension URLs.
35+
// For JetBrains: https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins
36+
// For VSCode: https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery
37+
// Returns the repo key (e.g., "jetbrains-plugins" or "vscode-extensions")
38+
func ExtractRepoKeyFromURL(repoURL string) (string, error) {
39+
if repoURL == "" {
40+
return "", fmt.Errorf("URL is empty")
41+
}
42+
43+
url := strings.TrimSpace(repoURL)
44+
url = strings.TrimPrefix(url, "https://")
45+
url = strings.TrimPrefix(url, "http://")
46+
url = strings.TrimSuffix(url, "/")
47+
48+
// Check for JetBrains plugins API
49+
if idx := strings.Index(url, "/api/jetbrainsplugins/"); idx != -1 {
50+
rest := url[idx+len("/api/jetbrainsplugins/"):]
51+
parts := strings.SplitN(rest, "/", 2)
52+
if len(parts) == 0 || parts[0] == "" {
53+
return "", fmt.Errorf("repository key not found in JetBrains URL")
54+
}
55+
return parts[0], nil
56+
}
57+
58+
// Check for VSCode extensions API
59+
if idx := strings.Index(url, "/api/vscodeextensions/"); idx != -1 {
60+
rest := url[idx+len("/api/vscodeextensions/"):]
61+
parts := strings.SplitN(rest, "/", 2)
62+
if len(parts) == 0 || parts[0] == "" {
63+
return "", fmt.Errorf("repository key not found in VSCode URL")
64+
}
65+
return parts[0], nil
66+
}
67+
68+
return "", fmt.Errorf("URL does not contain a supported API type (/api/jetbrainsplugins/ or /api/vscodeextensions/)")
69+
}
70+
71+
// IsValidUrl checks if a string is a valid URL with scheme and host
72+
func IsValidUrl(s string) bool {
73+
u, err := url.Parse(s)
74+
return err == nil && u.Scheme != "" && u.Host != ""
75+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ide
2+
3+
const VscodeConfigDescription = `
4+
Configure VSCode to use JFrog Artifactory for extensions.
5+
6+
The service URL should be in the format:
7+
https://<artifactory-url>/artifactory/api/vscodeextensions/<repo-key>/_apis/public/gallery
8+
9+
Examples:
10+
jf vscode-config https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery
11+
12+
This command will:
13+
- Modify the VSCode product.json file to change the extensions gallery URL
14+
- Create an automatic backup before making changes
15+
- Require VSCode to be restarted to apply changes
16+
17+
Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id)
18+
to enable repository validation. Without these flags, the command will only modify the local VSCode configuration.
19+
20+
Note: On macOS/Linux, you may need to run with sudo for system-installed VSCode.
21+
`
22+
23+
const JetbrainsConfigDescription = `
24+
Configure JetBrains IDEs to use JFrog Artifactory for plugins.
25+
26+
The repository URL should be in the format:
27+
https://<artifactory-url>/artifactory/api/jetbrainsplugins/<repo-key>
28+
29+
Examples:
30+
jf jetbrains-config https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins
31+
32+
This command will:
33+
- Detect all installed JetBrains IDEs
34+
- Modify each IDE's idea.properties file to add the plugins repository URL
35+
- Create automatic backups before making changes
36+
- Require IDEs to be restarted to apply changes
37+
38+
Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id)
39+
to enable repository validation. Without these flags, the command will only modify the local IDE configuration.
40+
41+
Supported IDEs: IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, RubyMine, CLion, DataGrip, GoLand, Rider, Android Studio, AppCode, RustRover, Aqua
42+
`
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package jetbrains
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/jfrog/gofrog/log"
8+
"github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide"
9+
"github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ide/jetbrains"
10+
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
11+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
12+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
13+
)
14+
15+
const (
16+
repoKeyFlag = "repo-key"
17+
urlSuffixFlag = "url-suffix"
18+
apiType = "jetbrainsplugins"
19+
)
20+
21+
func GetCommands() []components.Command {
22+
return []components.Command{
23+
{
24+
Name: "jetbrains-config",
25+
Aliases: []string{"jb"},
26+
Hidden: true,
27+
Flags: getFlags(),
28+
Arguments: getArguments(),
29+
Action: jetbrainsConfigCmd,
30+
Description: ide.JetbrainsConfigDescription,
31+
},
32+
}
33+
}
34+
35+
func getFlags() []components.Flag {
36+
return []components.Flag{
37+
components.NewStringFlag(repoKeyFlag, "Repository key for the JetBrains plugins repo. [Required if no URL is given]", components.SetMandatoryFalse()),
38+
components.NewStringFlag(urlSuffixFlag, "Suffix for the JetBrains plugins repository URL. Default: (empty)", components.SetMandatoryFalse()),
39+
// Server configuration flags
40+
components.NewStringFlag("url", "JFrog Artifactory URL. (example: https://acme.jfrog.io/artifactory)", components.SetMandatoryFalse()),
41+
components.NewStringFlag("user", "JFrog username.", components.SetMandatoryFalse()),
42+
components.NewStringFlag("password", "JFrog password.", components.SetMandatoryFalse()),
43+
components.NewStringFlag("access-token", "JFrog access token.", components.SetMandatoryFalse()),
44+
components.NewStringFlag("server-id", "Server ID configured using the 'jf config' command.", components.SetMandatoryFalse()),
45+
}
46+
}
47+
48+
func getArguments() []components.Argument {
49+
return []components.Argument{
50+
{
51+
Name: "repository-url",
52+
Description: "The Artifactory JetBrains plugins repository URL (optional when using --repo-key)",
53+
Optional: true,
54+
},
55+
}
56+
}
57+
58+
// Main command action: orchestrates argument parsing, server config, and command execution
59+
func jetbrainsConfigCmd(c *components.Context) error {
60+
repoKey, repositoryURL, err := getJetbrainsRepoKeyAndURL(c)
61+
if err != nil {
62+
return err
63+
}
64+
65+
rtDetails, err := getJetbrainsServerDetails(c)
66+
if err != nil {
67+
return err
68+
}
69+
70+
jetbrainsCmd := jetbrains.NewJetbrainsCommand(repositoryURL, repoKey)
71+
72+
// Determine if this is a direct URL (argument provided) vs constructed URL (server-id + repo-key)
73+
isDirectURL := c.GetNumberOfArgs() > 0 && ide.IsValidUrl(c.GetArgumentAt(0))
74+
jetbrainsCmd.SetDirectURL(isDirectURL)
75+
76+
if rtDetails != nil {
77+
jetbrainsCmd.SetServerDetails(rtDetails)
78+
}
79+
80+
return jetbrainsCmd.Run()
81+
}
82+
83+
// getJetbrainsRepoKeyAndURL determines the repo key and repository URL from args/flags
84+
func getJetbrainsRepoKeyAndURL(c *components.Context) (repoKey, repositoryURL string, err error) {
85+
if c.GetNumberOfArgs() > 0 && ide.IsValidUrl(c.GetArgumentAt(0)) {
86+
repositoryURL = c.GetArgumentAt(0)
87+
repoKey, err = ide.ExtractRepoKeyFromURL(repositoryURL)
88+
if err != nil {
89+
return
90+
}
91+
return
92+
}
93+
94+
repoKey = c.GetStringFlagValue(repoKeyFlag)
95+
if repoKey == "" {
96+
err = fmt.Errorf("You must provide either a repository URL as the first argument or --repo-key flag.")
97+
return
98+
}
99+
// Get Artifactory URL from server details (flags or default)
100+
var artDetails *config.ServerDetails
101+
if ide.HasServerConfigFlags(c) {
102+
artDetails, err = pluginsCommon.CreateArtifactoryDetailsByFlags(c)
103+
if err != nil {
104+
err = fmt.Errorf("Failed to get Artifactory server details: %w", err)
105+
return
106+
}
107+
} else {
108+
artDetails, err = config.GetDefaultServerConf()
109+
if err != nil {
110+
err = fmt.Errorf("Failed to get default Artifactory server details: %w", err)
111+
return
112+
}
113+
}
114+
// Use ArtifactoryUrl if available (when using flags), otherwise use Url (when using config)
115+
baseUrl := artDetails.ArtifactoryUrl
116+
if baseUrl == "" {
117+
baseUrl = artDetails.Url
118+
}
119+
baseUrl = strings.TrimRight(baseUrl, "/")
120+
121+
urlSuffix := c.GetStringFlagValue(urlSuffixFlag)
122+
if urlSuffix != "" {
123+
urlSuffix = "/" + strings.TrimLeft(urlSuffix, "/")
124+
}
125+
repositoryURL = baseUrl + "/api/jetbrainsplugins/" + repoKey + urlSuffix
126+
return
127+
}
128+
129+
// getJetbrainsServerDetails returns server details for validation, or nil if not available
130+
func getJetbrainsServerDetails(c *components.Context) (*config.ServerDetails, error) {
131+
if ide.HasServerConfigFlags(c) {
132+
// Use explicit server configuration flags
133+
rtDetails, err := pluginsCommon.CreateArtifactoryDetailsByFlags(c)
134+
if err != nil {
135+
return nil, fmt.Errorf("failed to create server configuration: %w", err)
136+
}
137+
return rtDetails, nil
138+
}
139+
// Use default server configuration for validation when no explicit flags provided
140+
rtDetails, err := config.GetDefaultServerConf()
141+
if err != nil {
142+
// If no default server, that's okay - we'll just skip validation
143+
log.Debug("No default server configuration found, skipping repository validation")
144+
return nil, nil //nolint:nilerr // Intentionally ignoring error to skip validation when no default server
145+
}
146+
return rtDetails, nil
147+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package jetbrains
2+
3+
import (
4+
"testing"
5+
6+
"github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide"
7+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// ... existing code ...
12+
13+
func TestHasServerConfigFlags(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
flags map[string]string
17+
expected bool
18+
}{
19+
{
20+
name: "No flags",
21+
flags: map[string]string{},
22+
expected: false,
23+
},
24+
{
25+
name: "Only password flag",
26+
flags: map[string]string{"password": "mypass"},
27+
expected: false,
28+
},
29+
{
30+
name: "Password and URL flags",
31+
flags: map[string]string{"password": "mypass", "url": "https://example.com"},
32+
expected: true,
33+
},
34+
{
35+
name: "Password and server-id flags",
36+
flags: map[string]string{"password": "mypass", "server-id": "my-server"},
37+
expected: true,
38+
},
39+
{
40+
name: "URL flag only",
41+
flags: map[string]string{"url": "https://example.com"},
42+
expected: true,
43+
},
44+
{
45+
name: "User flag only",
46+
flags: map[string]string{"user": "myuser"},
47+
expected: true,
48+
},
49+
{
50+
name: "Access token flag only",
51+
flags: map[string]string{"access-token": "mytoken"},
52+
expected: true,
53+
},
54+
{
55+
name: "Server ID flag only",
56+
flags: map[string]string{"server-id": "my-server"},
57+
expected: true,
58+
},
59+
}
60+
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
ctx := &components.Context{}
64+
for flag, value := range tt.flags {
65+
ctx.AddStringFlag(flag, value)
66+
}
67+
68+
result := ide.HasServerConfigFlags(ctx)
69+
assert.Equal(t, tt.expected, result, "Test case: %s", tt.name)
70+
})
71+
}
72+
}

0 commit comments

Comments
 (0)