Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.

Commit fd926af

Browse files
feat: support self-hosted instances and GitHub Enterprise (#6)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fd9cec4 commit fd926af

9 files changed

Lines changed: 263 additions & 22 deletions

File tree

internal/api/auth.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"net/http"
88
"net/url"
9+
10+
"github.com/keywaysh/cli/internal/config"
911
)
1012

1113
// DeviceStartResponse is the response from starting device login
@@ -80,7 +82,7 @@ func (c *Client) GetRepoIdsFromBackend(ctx context.Context, repoFullName string)
8082
// GetRepoIdsFromGitHub fetches repo IDs from GitHub public API
8183
// Only works for public repos (no auth required)
8284
func GetRepoIdsFromGitHub(ctx context.Context, owner, repo string) (*RepoIds, error) {
83-
url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo)
85+
url := fmt.Sprintf("%s/repos/%s/%s", config.GetGitHubAPIURL(), owner, repo)
8486

8587
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
8688
if err != nil {

internal/cmd/readme.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"regexp"
88
"strings"
99

10+
"github.com/keywaysh/cli/internal/config"
1011
"github.com/keywaysh/cli/internal/git"
1112
"github.com/keywaysh/cli/internal/ui"
1213
"github.com/spf13/cobra"
@@ -32,7 +33,8 @@ var readmeCmd = &cobra.Command{
3233

3334
// GenerateBadge creates the markdown badge for a repository
3435
func GenerateBadge(repo string) string {
35-
return fmt.Sprintf("[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=%s)](https://www.keyway.sh/vaults/%s)", repo, repo)
36+
dashboardURL := config.GetDashboardURL()
37+
return fmt.Sprintf("[![Keyway Secrets](%s/badge.svg?repo=%s)](%s/vaults/%s)", dashboardURL, repo, dashboardURL, repo)
3638
}
3739

3840
// FindReadmePath looks for README.md in the given directory
@@ -85,8 +87,8 @@ func findLastBadgeEnd(line string) int {
8587

8688
// InsertBadgeIntoReadme inserts the badge into README content
8789
func InsertBadgeIntoReadme(content, badge string) string {
88-
// Check if badge already exists
89-
if strings.Contains(content, "keyway.sh/badge.svg") {
90+
// Check if badge already exists (check for both default and custom dashboard URLs)
91+
if strings.Contains(content, "badge.svg?repo=") {
9092
return content
9193
}
9294

internal/cmd/readme_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import (
1010
var testBadge = GenerateBadge("acme/backend")
1111

1212
func TestGenerateBadge_CorrectFormat(t *testing.T) {
13-
result := GenerateBadge("NicolasRitouet/guideeco.fr")
14-
if !strings.Contains(result, "https://www.keyway.sh/badge.svg?repo=NicolasRitouet/guideeco.fr") {
13+
result := GenerateBadge("acme/my-project")
14+
if !strings.Contains(result, "/badge.svg?repo=acme/my-project") {
1515
t.Error("badge should contain correct badge URL")
1616
}
17-
if !strings.Contains(result, "https://www.keyway.sh/vaults/NicolasRitouet/guideeco.fr") {
17+
if !strings.Contains(result, "/vaults/acme/my-project") {
1818
t.Error("badge should contain correct vault URL")
1919
}
2020
}

internal/cmd/root.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func printCustomHelp(cmd *cobra.Command) {
209209

210210
// Footer
211211
fmt.Printf(" %s %s\n", dim("Run"), fmt.Sprintf("%s %s", cyan("keyway <command> --help"), dim("for details")))
212-
fmt.Printf(" %s %s\n", dim("Docs:"), "https://docs.keyway.sh")
212+
fmt.Printf(" %s %s\n", dim("Docs:"), config.GetDocsURL())
213213

214214
// Version
215215
if cmd.Version != "" {
@@ -257,6 +257,11 @@ func Execute(ver string) error {
257257
}
258258

259259
func displayUpdateNotice(info *version.UpdateInfo) {
260+
// Skip update notice for self-hosted instances (no update command)
261+
if info.UpdateCommand == "" {
262+
return
263+
}
264+
260265
yellow := color.New(color.FgYellow).SprintFunc()
261266
fmt.Println()
262267
fmt.Printf(" %s Update available: %s → %s\n",

internal/config/config.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package config
22

3-
import "os"
3+
import (
4+
"os"
5+
"strings"
6+
)
47

58
const (
6-
// DefaultAPIURL is the production API URL
7-
DefaultAPIURL = "https://api.keyway.sh"
8-
9-
// DefaultDashboardURL is the production dashboard URL
10-
DefaultDashboardURL = "https://app.keyway.sh"
11-
12-
// DefaultPostHogHost is the PostHog host
13-
DefaultPostHogHost = "https://eu.i.posthog.com"
9+
DefaultAPIURL = "https://api.keyway.sh"
10+
DefaultDashboardURL = "https://app.keyway.sh"
11+
DefaultPostHogHost = "https://eu.i.posthog.com"
12+
DefaultGitHubAPIURL = "https://api.github.com"
13+
DefaultGitHubBaseURL = "https://github.com"
14+
DefaultDocsURL = "https://docs.keyway.sh"
1415
)
1516

1617
// Blank by default - set via build or env
@@ -66,3 +67,45 @@ func IsCI() bool {
6667
func GetToken() string {
6768
return os.Getenv("KEYWAY_TOKEN")
6869
}
70+
71+
// GetGitHubURL returns the GitHub base URL from env or default
72+
func GetGitHubURL() string {
73+
if url := os.Getenv("KEYWAY_GITHUB_URL"); url != "" {
74+
return strings.TrimSuffix(url, "/")
75+
}
76+
return DefaultGitHubBaseURL
77+
}
78+
79+
// GetGitHubAPIURL returns the GitHub API URL from env or default
80+
func GetGitHubAPIURL() string {
81+
if url := os.Getenv("KEYWAY_GITHUB_API_URL"); url != "" {
82+
return strings.TrimSuffix(url, "/")
83+
}
84+
// If KEYWAY_GITHUB_URL is set (GHE), derive API URL from it
85+
if ghURL := os.Getenv("KEYWAY_GITHUB_URL"); ghURL != "" {
86+
ghURL = strings.TrimSuffix(ghURL, "/")
87+
// For GHE: https://github.example.com -> https://github.example.com/api/v3
88+
return ghURL + "/api/v3"
89+
}
90+
return DefaultGitHubAPIURL
91+
}
92+
93+
// GetGitHubBaseURL returns the GitHub base URL from env or default
94+
// Deprecated: Use GetGitHubURL instead
95+
func GetGitHubBaseURL() string {
96+
return GetGitHubURL()
97+
}
98+
99+
// GetDocsURL returns the docs URL from env or default
100+
func GetDocsURL() string {
101+
if url := os.Getenv("KEYWAY_DOCS_URL"); url != "" {
102+
return url
103+
}
104+
return DefaultDocsURL
105+
}
106+
107+
// IsCustomAPIURL returns true if using a non-default API URL (self-hosted)
108+
func IsCustomAPIURL() bool {
109+
apiURL := os.Getenv("KEYWAY_API_URL")
110+
return apiURL != "" && apiURL != DefaultAPIURL
111+
}

internal/config/config_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,128 @@ func TestDefaultAPIURL(t *testing.T) {
130130
t.Errorf("DefaultAPIURL = %v, want https://api.keyway.sh", DefaultAPIURL)
131131
}
132132
}
133+
134+
func TestGetGitHubURL_Default(t *testing.T) {
135+
os.Unsetenv("KEYWAY_GITHUB_URL")
136+
137+
url := GetGitHubURL()
138+
if url != DefaultGitHubBaseURL {
139+
t.Errorf("GetGitHubURL() = %v, want %v", url, DefaultGitHubBaseURL)
140+
}
141+
}
142+
143+
func TestGetGitHubURL_FromEnv(t *testing.T) {
144+
os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com")
145+
defer os.Unsetenv("KEYWAY_GITHUB_URL")
146+
147+
url := GetGitHubURL()
148+
if url != "https://github.example.com" {
149+
t.Errorf("GetGitHubURL() = %v, want https://github.example.com", url)
150+
}
151+
}
152+
153+
func TestGetGitHubURL_TrimsTrailingSlash(t *testing.T) {
154+
os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com/")
155+
defer os.Unsetenv("KEYWAY_GITHUB_URL")
156+
157+
url := GetGitHubURL()
158+
if url != "https://github.example.com" {
159+
t.Errorf("GetGitHubURL() = %v, want https://github.example.com", url)
160+
}
161+
}
162+
163+
func TestGetGitHubAPIURL_Default(t *testing.T) {
164+
os.Unsetenv("KEYWAY_GITHUB_API_URL")
165+
os.Unsetenv("KEYWAY_GITHUB_URL")
166+
167+
url := GetGitHubAPIURL()
168+
if url != DefaultGitHubAPIURL {
169+
t.Errorf("GetGitHubAPIURL() = %v, want %v", url, DefaultGitHubAPIURL)
170+
}
171+
}
172+
173+
func TestGetGitHubAPIURL_FromEnv(t *testing.T) {
174+
os.Setenv("KEYWAY_GITHUB_API_URL", "https://api.github.example.com")
175+
defer os.Unsetenv("KEYWAY_GITHUB_API_URL")
176+
177+
url := GetGitHubAPIURL()
178+
if url != "https://api.github.example.com" {
179+
t.Errorf("GetGitHubAPIURL() = %v, want https://api.github.example.com", url)
180+
}
181+
}
182+
183+
func TestGetGitHubAPIURL_DerivedFromGHE(t *testing.T) {
184+
os.Unsetenv("KEYWAY_GITHUB_API_URL")
185+
os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com")
186+
defer os.Unsetenv("KEYWAY_GITHUB_URL")
187+
188+
url := GetGitHubAPIURL()
189+
if url != "https://github.example.com/api/v3" {
190+
t.Errorf("GetGitHubAPIURL() = %v, want https://github.example.com/api/v3", url)
191+
}
192+
}
193+
194+
func TestGetGitHubAPIURL_ExplicitOverridesGHE(t *testing.T) {
195+
os.Setenv("KEYWAY_GITHUB_API_URL", "https://custom-api.example.com")
196+
os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com")
197+
defer os.Unsetenv("KEYWAY_GITHUB_API_URL")
198+
defer os.Unsetenv("KEYWAY_GITHUB_URL")
199+
200+
url := GetGitHubAPIURL()
201+
if url != "https://custom-api.example.com" {
202+
t.Errorf("GetGitHubAPIURL() = %v, want https://custom-api.example.com", url)
203+
}
204+
}
205+
206+
func TestGetGitHubBaseURL_DelegatesToGetGitHubURL(t *testing.T) {
207+
os.Unsetenv("KEYWAY_GITHUB_URL")
208+
209+
if GetGitHubBaseURL() != GetGitHubURL() {
210+
t.Error("GetGitHubBaseURL() should delegate to GetGitHubURL()")
211+
}
212+
}
213+
214+
func TestGetDocsURL_Default(t *testing.T) {
215+
os.Unsetenv("KEYWAY_DOCS_URL")
216+
217+
url := GetDocsURL()
218+
if url != DefaultDocsURL {
219+
t.Errorf("GetDocsURL() = %v, want %v", url, DefaultDocsURL)
220+
}
221+
}
222+
223+
func TestGetDocsURL_FromEnv(t *testing.T) {
224+
os.Setenv("KEYWAY_DOCS_URL", "https://docs.example.com")
225+
defer os.Unsetenv("KEYWAY_DOCS_URL")
226+
227+
url := GetDocsURL()
228+
if url != "https://docs.example.com" {
229+
t.Errorf("GetDocsURL() = %v, want https://docs.example.com", url)
230+
}
231+
}
232+
233+
func TestIsCustomAPIURL_NotSet(t *testing.T) {
234+
os.Unsetenv("KEYWAY_API_URL")
235+
236+
if IsCustomAPIURL() {
237+
t.Error("IsCustomAPIURL() should return false when not set")
238+
}
239+
}
240+
241+
func TestIsCustomAPIURL_SetToDefault(t *testing.T) {
242+
os.Setenv("KEYWAY_API_URL", DefaultAPIURL)
243+
defer os.Unsetenv("KEYWAY_API_URL")
244+
245+
if IsCustomAPIURL() {
246+
t.Error("IsCustomAPIURL() should return false when set to default")
247+
}
248+
}
249+
250+
func TestIsCustomAPIURL_SetToCustom(t *testing.T) {
251+
os.Setenv("KEYWAY_API_URL", "https://api.example.com")
252+
defer os.Unsetenv("KEYWAY_API_URL")
253+
254+
if !IsCustomAPIURL() {
255+
t.Error("IsCustomAPIURL() should return true when set to custom URL")
256+
}
257+
}

internal/version/github.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/http"
8+
9+
"github.com/keywaysh/cli/internal/config"
810
)
911

1012
const (
11-
githubReleasesURL = "https://api.github.com/repos/keywaysh/cli/releases/latest"
13+
defaultGitHubReleasesURL = "https://api.github.com/repos/keywaysh/cli/releases/latest"
1214
)
1315

1416
type githubRelease struct {
1517
TagName string `json:"tag_name"`
1618
}
1719

18-
// FetchLatestVersion fetches the latest version from GitHub Releases
20+
// FetchLatestVersion fetches the latest version from GitHub Releases.
21+
// Returns an error if using a custom (self-hosted) API URL, since update
22+
// checks only apply to the official Keyway distribution.
1923
func FetchLatestVersion(ctx context.Context) (string, error) {
20-
req, err := http.NewRequestWithContext(ctx, "GET", githubReleasesURL, nil)
24+
// Skip update checks for self-hosted instances
25+
if config.IsCustomAPIURL() {
26+
return "", fmt.Errorf("update checks disabled for self-hosted instances")
27+
}
28+
29+
req, err := http.NewRequestWithContext(ctx, "GET", defaultGitHubReleasesURL, nil)
2130
if err != nil {
2231
return "", err
2332
}

internal/version/version.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strconv"
66
"strings"
77
"time"
8+
9+
"github.com/keywaysh/cli/internal/config"
810
)
911

1012
const (
@@ -30,6 +32,11 @@ func CheckForUpdate(ctx context.Context, currentVersion string) *UpdateInfo {
3032
return nil
3133
}
3234

35+
// Skip update check for self-hosted instances
36+
if config.IsCustomAPIURL() {
37+
return nil
38+
}
39+
3340
// Skip check for dev builds
3441
if currentVersion == "dev" || currentVersion == "" {
3542
return nil
@@ -79,8 +86,14 @@ func buildUpdateInfo(current, latest string, method InstallMethod) *UpdateInfo {
7986
}
8087
}
8188

82-
// GetUpdateCommand returns the update command for the given install method
89+
// GetUpdateCommand returns the update command for the given install method.
90+
// Returns an empty string for self-hosted instances where standard update
91+
// commands do not apply.
8392
func GetUpdateCommand(method InstallMethod) string {
93+
if config.IsCustomAPIURL() {
94+
return ""
95+
}
96+
8497
switch method {
8598
case InstallMethodNPM:
8699
return "npm update -g @keywaysh/cli"

0 commit comments

Comments
 (0)