From 1226be9fc7fc611f5fcfd131759a0e9d7d716923 Mon Sep 17 00:00:00 2001 From: Letiancheng Lee <130898955+PeterTianbuhan@users.noreply.github.com> Date: Sun, 31 May 2026 05:13:31 -0700 Subject: [PATCH 1/2] fix: support custom SSH usernames --- internal/install/conflict_test.go | 1 + internal/install/install.go | 6 +++--- internal/install/source.go | 33 ++++++++++++++++--------------- internal/install/source_test.go | 31 +++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/internal/install/conflict_test.go b/internal/install/conflict_test.go index 17d2fc6e..6b948832 100644 --- a/internal/install/conflict_test.go +++ b/internal/install/conflict_test.go @@ -17,6 +17,7 @@ func TestNormalizeCloneURL(t *testing.T) { {"https://github.com/owner/repo", "github.com/owner/repo"}, {"git@github.com:owner/repo.git", "github.com/owner/repo"}, {"git@github.com:owner/repo", "github.com/owner/repo"}, + {"acme@acme.ghe.com:MyOrg/repo.git", "acme.ghe.com/myorg/repo"}, {"https://github.com/Owner/Repo.git", "github.com/owner/repo"}, {"https://github.com/owner/repo/", "github.com/owner/repo"}, {"http://github.com/owner/repo.git", "github.com/owner/repo"}, diff --git a/internal/install/install.go b/internal/install/install.go index cb47677e..10364f06 100644 --- a/internal/install/install.go +++ b/internal/install/install.go @@ -168,9 +168,9 @@ func normalizeCloneURL(u string) string { u = strings.TrimSpace(u) u = strings.TrimSuffix(u, ".git") u = strings.TrimSuffix(u, "/") - // git@github.com:owner/repo → github.com/owner/repo - if strings.HasPrefix(u, "git@") { - u = strings.TrimPrefix(u, "git@") + // user@github.com:owner/repo → github.com/owner/repo + if at := strings.Index(u, "@"); at > 0 && !strings.Contains(u, "://") { + u = u[at+1:] u = strings.Replace(u, ":", "/", 1) } // https://github.com/owner/repo → github.com/owner/repo diff --git a/internal/install/source.go b/internal/install/source.go index db69d81f..bd2e8207 100644 --- a/internal/install/source.go +++ b/internal/install/source.go @@ -52,8 +52,8 @@ type Source struct { // GitHub URL pattern: github.com/owner/repo[/path/to/subdir] var githubPattern = regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)(?:/(.+))?$`) -// Git SSH pattern: git@host:owner/repo[.git][//subdir] -var gitSSHPattern = regexp.MustCompile(`^git@([^:]+):([^/]+)/(.+?)(?:\.git)?(?://(.+))?$`) +// Git SSH pattern: user@host:owner/repo[.git][//subdir] +var gitSSHPattern = regexp.MustCompile(`^([^@:\s]+)@([^:\s]+):([^/]+)/(.+?)(?:\.git)?(?://(.+))?$`) // Git HTTPS pattern: https://host/path (flexible path for GitLab subgroups) var gitHTTPSPattern = regexp.MustCompile(`^(https?)://([^/]+)/(.+)$`) @@ -179,7 +179,7 @@ func expandGitHubShorthand(input string) string { if strings.HasPrefix(input, "github.com/") || strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") || - strings.HasPrefix(input, "git@") || + gitSSHPattern.MatchString(input) || strings.HasPrefix(input, "file://") || isLocalPath(input) { return input @@ -299,17 +299,18 @@ func trimSkillFileSuffix(path string, isBlob bool) (string, bool) { } func parseGitSSH(matches []string, source *Source) (*Source, error) { - // matches: [full, host, owner, repo, subdir] - host := matches[1] - owner := matches[2] - repo := strings.TrimSuffix(matches[3], ".git") + // matches: [full, user, host, owner, repo, subdir] + user := matches[1] + host := matches[2] + owner := matches[3] + repo := strings.TrimSuffix(matches[4], ".git") subdir := "" - if len(matches) > 4 { - subdir = matches[4] + if len(matches) > 5 { + subdir = matches[5] } source.Type = SourceTypeGitSSH - source.CloneURL = fmt.Sprintf("git@%s:%s/%s.git", host, owner, repo) + source.CloneURL = fmt.Sprintf("%s@%s:%s/%s.git", user, host, owner, repo) if subdir != "" { source.Subdir = subdir @@ -541,13 +542,13 @@ func (s *Source) gitHubOwnerRepo() (owner, repo string) { return "", "" } - // SSH clone URL: git@host:owner/repo.git + // SSH clone URL: user@host:owner/repo.git if sshMatches := gitSSHPattern.FindStringSubmatch(cloneURL); sshMatches != nil { - host := strings.ToLower(strings.TrimSpace(sshMatches[1])) + host := strings.ToLower(strings.TrimSpace(sshMatches[2])) if !strings.Contains(host, "github") { return "", "" } - return sshMatches[2], strings.TrimSuffix(sshMatches[3], ".git") + return sshMatches[3], strings.TrimSuffix(sshMatches[4], ".git") } u, err := url.Parse(cloneURL) @@ -614,10 +615,10 @@ func (s *Source) TrackName() string { } } - // Try SSH format: git@host:owner/repo.git + // Try SSH format: user@host:owner/repo.git if sshMatches := gitSSHPattern.FindStringSubmatch(s.Raw); sshMatches != nil { - owner := sshMatches[2] - repo := strings.TrimSuffix(sshMatches[3], ".git") + owner := sshMatches[3] + repo := strings.TrimSuffix(sshMatches[4], ".git") // Replace / with - to handle subgroup paths (e.g., group/subgroup/repo) return owner + "-" + strings.ReplaceAll(repo, "/", "-") } diff --git a/internal/install/source_test.go b/internal/install/source_test.go index 8530bb66..7c4bbf16 100644 --- a/internal/install/source_test.go +++ b/internal/install/source_test.go @@ -215,6 +215,18 @@ func TestParseSource_GitSSH(t *testing.T) { wantCloneURL: "git@gitlab.com:user/repo.git", wantName: "repo", }, + { + name: "custom ssh username", + input: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantCloneURL: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantName: "my-skills", + }, + { + name: "custom ssh username without .git", + input: "acme@acme.ghe.com:MyOrg/my-skills", + wantCloneURL: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantName: "my-skills", + }, { name: "gitlab ssh nested subgroup", input: "git@gitlab.example.com:org/subgroup/my-skills.git", @@ -255,6 +267,13 @@ func TestParseSource_GitSSH(t *testing.T) { wantSubdir: "pdf", wantName: "pdf", }, + { + name: "custom ssh username with subpath", + input: "acme@acme.ghe.com:MyOrg/my-skills.git//agents/reviewer", + wantCloneURL: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantSubdir: "agents/reviewer", + wantName: "reviewer", + }, } for _, tt := range tests { @@ -823,6 +842,13 @@ func TestParseSource_GitHubEnterprise(t *testing.T) { wantCloneURL: "git@mycompany.github.com:team/skills.git", wantName: "skills", }, + { + name: "GHE Data Residency SSH", + input: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantType: SourceTypeGitSSH, + wantCloneURL: "acme@acme.ghe.com:MyOrg/my-skills.git", + wantName: "my-skills", + }, { name: "GHE SSH with subdir", input: "git@github.mycompany.com:org/repo.git//path/to/skill", @@ -876,6 +902,11 @@ func TestParseSource_GitHubEnterprise_TrackName(t *testing.T) { raw: "git@github.mycompany.com:org/skills.git", want: "org-skills", }, + { + name: "GHE Data Residency SSH", + raw: "acme@acme.ghe.com:MyOrg/my-skills.git", + want: "MyOrg-my-skills", + }, } for _, tt := range tests { From 5fd487b8d27be7e9d4889fd0f3fc94464d95bdbe Mon Sep 17 00:00:00 2001 From: Letiancheng Lee <130898955+PeterTianbuhan@users.noreply.github.com> Date: Sun, 31 May 2026 06:00:15 -0700 Subject: [PATCH 2/2] fix: recognize GHE data residency hosts --- internal/install/auth.go | 2 +- internal/install/auth_test.go | 2 ++ internal/install/github_download.go | 7 ++----- internal/install/github_download_test.go | 5 +++++ internal/install/github_host.go | 19 +++++++++++++++++++ internal/install/source.go | 4 ++-- internal/install/source_test.go | 12 ++++++++++++ 7 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 internal/install/github_host.go diff --git a/internal/install/auth.go b/internal/install/auth.go index c21a511f..a699f493 100644 --- a/internal/install/auth.go +++ b/internal/install/auth.go @@ -50,7 +50,7 @@ func detectPlatform(cloneURL string) Platform { if host == "" { return PlatformUnknown } - if strings.Contains(host, "github") { + if isGitHubLikeHost(host) { return PlatformGitHub } if strings.Contains(host, "gitlab") { diff --git a/internal/install/auth_test.go b/internal/install/auth_test.go index 38a1215c..f23ea766 100644 --- a/internal/install/auth_test.go +++ b/internal/install/auth_test.go @@ -57,6 +57,8 @@ func TestDetectPlatform(t *testing.T) { }{ {"github.com", "https://github.com/org/repo.git", PlatformGitHub}, {"github enterprise", "https://github.mycompany.com/org/repo.git", PlatformGitHub}, + {"github enterprise data residency", "https://acme.ghe.com/org/repo.git", PlatformGitHub}, + {"github enterprise data residency ssh", "acme@acme.ghe.com:org/repo.git", PlatformGitHub}, {"gitlab.com", "https://gitlab.com/org/repo.git", PlatformGitLab}, {"self-hosted gitlab", "https://gitlab.internal.co/org/repo.git", PlatformGitLab}, {"bitbucket.org", "https://bitbucket.org/team/repo.git", PlatformBitbucket}, diff --git a/internal/install/github_download.go b/internal/install/github_download.go index 82c531c3..b5a0711f 100644 --- a/internal/install/github_download.go +++ b/internal/install/github_download.go @@ -246,13 +246,10 @@ func gitHubAPIBase(source *Source) (string, error) { if host == "" { return "", fmt.Errorf("unable to determine source host") } - if !strings.Contains(host, "github") { + if !isGitHubLikeHost(host) { return "", fmt.Errorf("source host %q is not GitHub-compatible", host) } - if host == "github.com" { - return "https://api.github.com", nil - } - return fmt.Sprintf("https://%s/api/v3", host), nil + return gitHubAPIBaseForHost(host), nil } func buildGitHubContentsURL(apiBase, owner, repo, path string) string { diff --git a/internal/install/github_download_test.go b/internal/install/github_download_test.go index 838a96a5..bda5beb4 100644 --- a/internal/install/github_download_test.go +++ b/internal/install/github_download_test.go @@ -112,6 +112,11 @@ func TestGitHubAPIBase(t *testing.T) { source: &Source{CloneURL: "https://github.acme.com/acme/repo.git"}, want: "https://github.acme.com/api/v3", }, + { + name: "ghe data residency", + source: &Source{CloneURL: "https://acme.ghe.com/acme/repo.git"}, + want: "https://api.acme.ghe.com", + }, { name: "non-github host", source: &Source{CloneURL: "https://gitlab.com/acme/repo.git"}, diff --git a/internal/install/github_host.go b/internal/install/github_host.go new file mode 100644 index 00000000..bd0cf14b --- /dev/null +++ b/internal/install/github_host.go @@ -0,0 +1,19 @@ +package install + +import "strings" + +func isGitHubLikeHost(host string) bool { + host = strings.ToLower(strings.TrimSpace(host)) + return strings.Contains(host, "github") || strings.HasSuffix(host, ".ghe.com") +} + +func gitHubAPIBaseForHost(host string) string { + host = strings.ToLower(strings.TrimSpace(host)) + if host == "github.com" { + return "https://api.github.com" + } + if strings.HasSuffix(host, ".ghe.com") { + return "https://api." + host + } + return "https://" + host + "/api/v3" +} diff --git a/internal/install/source.go b/internal/install/source.go index bd2e8207..d1ae8c55 100644 --- a/internal/install/source.go +++ b/internal/install/source.go @@ -545,7 +545,7 @@ func (s *Source) gitHubOwnerRepo() (owner, repo string) { // SSH clone URL: user@host:owner/repo.git if sshMatches := gitSSHPattern.FindStringSubmatch(cloneURL); sshMatches != nil { host := strings.ToLower(strings.TrimSpace(sshMatches[2])) - if !strings.Contains(host, "github") { + if !isGitHubLikeHost(host) { return "", "" } return sshMatches[3], strings.TrimSuffix(sshMatches[4], ".git") @@ -556,7 +556,7 @@ func (s *Source) gitHubOwnerRepo() (owner, repo string) { return "", "" } host := strings.ToLower(u.Hostname()) - if !strings.Contains(host, "github") { + if !isGitHubLikeHost(host) { return "", "" } diff --git a/internal/install/source_test.go b/internal/install/source_test.go index 7c4bbf16..25af0cc5 100644 --- a/internal/install/source_test.go +++ b/internal/install/source_test.go @@ -948,6 +948,18 @@ func TestSource_GitHubOwnerRepo(t *testing.T) { wantOwner: "team", wantRepo: "repo", }, + { + name: "ghe data residency https", + raw: "https://acme.ghe.com/MyOrg/my-skills/agents/reviewer", + wantOwner: "MyOrg", + wantRepo: "my-skills", + }, + { + name: "ghe data residency ssh", + raw: "acme@acme.ghe.com:MyOrg/my-skills.git//agents/reviewer", + wantOwner: "MyOrg", + wantRepo: "my-skills", + }, { name: "non-github host", raw: "https://gitlab.com/team/repo",