diff --git a/internal/api/auth.go b/internal/api/auth.go index cdb3411..5d448d5 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" "net/url" + + "github.com/keywaysh/cli/internal/config" ) // DeviceStartResponse is the response from starting device login @@ -80,7 +82,7 @@ func (c *Client) GetRepoIdsFromBackend(ctx context.Context, repoFullName string) // GetRepoIdsFromGitHub fetches repo IDs from GitHub public API // Only works for public repos (no auth required) func GetRepoIdsFromGitHub(ctx context.Context, owner, repo string) (*RepoIds, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) + url := fmt.Sprintf("%s/repos/%s/%s", config.GetGitHubAPIURL(), owner, repo) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { diff --git a/internal/cmd/readme.go b/internal/cmd/readme.go index 550b1b4..635a59e 100644 --- a/internal/cmd/readme.go +++ b/internal/cmd/readme.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + "github.com/keywaysh/cli/internal/config" "github.com/keywaysh/cli/internal/git" "github.com/keywaysh/cli/internal/ui" "github.com/spf13/cobra" @@ -32,7 +33,8 @@ var readmeCmd = &cobra.Command{ // GenerateBadge creates the markdown badge for a repository func GenerateBadge(repo string) string { - return fmt.Sprintf("[![Keyway Secrets](https://www.keyway.sh/badge.svg?repo=%s)](https://www.keyway.sh/vaults/%s)", repo, repo) + dashboardURL := config.GetDashboardURL() + return fmt.Sprintf("[![Keyway Secrets](%s/badge.svg?repo=%s)](%s/vaults/%s)", dashboardURL, repo, dashboardURL, repo) } // FindReadmePath looks for README.md in the given directory @@ -85,8 +87,8 @@ func findLastBadgeEnd(line string) int { // InsertBadgeIntoReadme inserts the badge into README content func InsertBadgeIntoReadme(content, badge string) string { - // Check if badge already exists - if strings.Contains(content, "keyway.sh/badge.svg") { + // Check if badge already exists (check for both default and custom dashboard URLs) + if strings.Contains(content, "badge.svg?repo=") { return content } diff --git a/internal/cmd/readme_test.go b/internal/cmd/readme_test.go index 347af22..63248c3 100644 --- a/internal/cmd/readme_test.go +++ b/internal/cmd/readme_test.go @@ -10,11 +10,11 @@ import ( var testBadge = GenerateBadge("acme/backend") func TestGenerateBadge_CorrectFormat(t *testing.T) { - result := GenerateBadge("NicolasRitouet/guideeco.fr") - if !strings.Contains(result, "https://www.keyway.sh/badge.svg?repo=NicolasRitouet/guideeco.fr") { + result := GenerateBadge("acme/my-project") + if !strings.Contains(result, "/badge.svg?repo=acme/my-project") { t.Error("badge should contain correct badge URL") } - if !strings.Contains(result, "https://www.keyway.sh/vaults/NicolasRitouet/guideeco.fr") { + if !strings.Contains(result, "/vaults/acme/my-project") { t.Error("badge should contain correct vault URL") } } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index ee77b1d..fd39d95 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -209,7 +209,7 @@ func printCustomHelp(cmd *cobra.Command) { // Footer fmt.Printf(" %s %s\n", dim("Run"), fmt.Sprintf("%s %s", cyan("keyway --help"), dim("for details"))) - fmt.Printf(" %s %s\n", dim("Docs:"), "https://docs.keyway.sh") + fmt.Printf(" %s %s\n", dim("Docs:"), config.GetDocsURL()) // Version if cmd.Version != "" { @@ -257,6 +257,11 @@ func Execute(ver string) error { } func displayUpdateNotice(info *version.UpdateInfo) { + // Skip update notice for self-hosted instances (no update command) + if info.UpdateCommand == "" { + return + } + yellow := color.New(color.FgYellow).SprintFunc() fmt.Println() fmt.Printf(" %s Update available: %s → %s\n", diff --git a/internal/config/config.go b/internal/config/config.go index 15b5522..d302fc9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,16 +1,17 @@ package config -import "os" +import ( + "os" + "strings" +) const ( - // DefaultAPIURL is the production API URL - DefaultAPIURL = "https://api.keyway.sh" - - // DefaultDashboardURL is the production dashboard URL - DefaultDashboardURL = "https://app.keyway.sh" - - // DefaultPostHogHost is the PostHog host - DefaultPostHogHost = "https://eu.i.posthog.com" + DefaultAPIURL = "https://api.keyway.sh" + DefaultDashboardURL = "https://app.keyway.sh" + DefaultPostHogHost = "https://eu.i.posthog.com" + DefaultGitHubAPIURL = "https://api.github.com" + DefaultGitHubBaseURL = "https://github.com" + DefaultDocsURL = "https://docs.keyway.sh" ) // Blank by default - set via build or env @@ -66,3 +67,45 @@ func IsCI() bool { func GetToken() string { return os.Getenv("KEYWAY_TOKEN") } + +// GetGitHubURL returns the GitHub base URL from env or default +func GetGitHubURL() string { + if url := os.Getenv("KEYWAY_GITHUB_URL"); url != "" { + return strings.TrimSuffix(url, "/") + } + return DefaultGitHubBaseURL +} + +// GetGitHubAPIURL returns the GitHub API URL from env or default +func GetGitHubAPIURL() string { + if url := os.Getenv("KEYWAY_GITHUB_API_URL"); url != "" { + return strings.TrimSuffix(url, "/") + } + // If KEYWAY_GITHUB_URL is set (GHE), derive API URL from it + if ghURL := os.Getenv("KEYWAY_GITHUB_URL"); ghURL != "" { + ghURL = strings.TrimSuffix(ghURL, "/") + // For GHE: https://github.example.com -> https://github.example.com/api/v3 + return ghURL + "/api/v3" + } + return DefaultGitHubAPIURL +} + +// GetGitHubBaseURL returns the GitHub base URL from env or default +// Deprecated: Use GetGitHubURL instead +func GetGitHubBaseURL() string { + return GetGitHubURL() +} + +// GetDocsURL returns the docs URL from env or default +func GetDocsURL() string { + if url := os.Getenv("KEYWAY_DOCS_URL"); url != "" { + return url + } + return DefaultDocsURL +} + +// IsCustomAPIURL returns true if using a non-default API URL (self-hosted) +func IsCustomAPIURL() bool { + apiURL := os.Getenv("KEYWAY_API_URL") + return apiURL != "" && apiURL != DefaultAPIURL +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f09f778..d498630 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -130,3 +130,128 @@ func TestDefaultAPIURL(t *testing.T) { t.Errorf("DefaultAPIURL = %v, want https://api.keyway.sh", DefaultAPIURL) } } + +func TestGetGitHubURL_Default(t *testing.T) { + os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubURL() + if url != DefaultGitHubBaseURL { + t.Errorf("GetGitHubURL() = %v, want %v", url, DefaultGitHubBaseURL) + } +} + +func TestGetGitHubURL_FromEnv(t *testing.T) { + os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com") + defer os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubURL() + if url != "https://github.example.com" { + t.Errorf("GetGitHubURL() = %v, want https://github.example.com", url) + } +} + +func TestGetGitHubURL_TrimsTrailingSlash(t *testing.T) { + os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com/") + defer os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubURL() + if url != "https://github.example.com" { + t.Errorf("GetGitHubURL() = %v, want https://github.example.com", url) + } +} + +func TestGetGitHubAPIURL_Default(t *testing.T) { + os.Unsetenv("KEYWAY_GITHUB_API_URL") + os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubAPIURL() + if url != DefaultGitHubAPIURL { + t.Errorf("GetGitHubAPIURL() = %v, want %v", url, DefaultGitHubAPIURL) + } +} + +func TestGetGitHubAPIURL_FromEnv(t *testing.T) { + os.Setenv("KEYWAY_GITHUB_API_URL", "https://api.github.example.com") + defer os.Unsetenv("KEYWAY_GITHUB_API_URL") + + url := GetGitHubAPIURL() + if url != "https://api.github.example.com" { + t.Errorf("GetGitHubAPIURL() = %v, want https://api.github.example.com", url) + } +} + +func TestGetGitHubAPIURL_DerivedFromGHE(t *testing.T) { + os.Unsetenv("KEYWAY_GITHUB_API_URL") + os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com") + defer os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubAPIURL() + if url != "https://github.example.com/api/v3" { + t.Errorf("GetGitHubAPIURL() = %v, want https://github.example.com/api/v3", url) + } +} + +func TestGetGitHubAPIURL_ExplicitOverridesGHE(t *testing.T) { + os.Setenv("KEYWAY_GITHUB_API_URL", "https://custom-api.example.com") + os.Setenv("KEYWAY_GITHUB_URL", "https://github.example.com") + defer os.Unsetenv("KEYWAY_GITHUB_API_URL") + defer os.Unsetenv("KEYWAY_GITHUB_URL") + + url := GetGitHubAPIURL() + if url != "https://custom-api.example.com" { + t.Errorf("GetGitHubAPIURL() = %v, want https://custom-api.example.com", url) + } +} + +func TestGetGitHubBaseURL_DelegatesToGetGitHubURL(t *testing.T) { + os.Unsetenv("KEYWAY_GITHUB_URL") + + if GetGitHubBaseURL() != GetGitHubURL() { + t.Error("GetGitHubBaseURL() should delegate to GetGitHubURL()") + } +} + +func TestGetDocsURL_Default(t *testing.T) { + os.Unsetenv("KEYWAY_DOCS_URL") + + url := GetDocsURL() + if url != DefaultDocsURL { + t.Errorf("GetDocsURL() = %v, want %v", url, DefaultDocsURL) + } +} + +func TestGetDocsURL_FromEnv(t *testing.T) { + os.Setenv("KEYWAY_DOCS_URL", "https://docs.example.com") + defer os.Unsetenv("KEYWAY_DOCS_URL") + + url := GetDocsURL() + if url != "https://docs.example.com" { + t.Errorf("GetDocsURL() = %v, want https://docs.example.com", url) + } +} + +func TestIsCustomAPIURL_NotSet(t *testing.T) { + os.Unsetenv("KEYWAY_API_URL") + + if IsCustomAPIURL() { + t.Error("IsCustomAPIURL() should return false when not set") + } +} + +func TestIsCustomAPIURL_SetToDefault(t *testing.T) { + os.Setenv("KEYWAY_API_URL", DefaultAPIURL) + defer os.Unsetenv("KEYWAY_API_URL") + + if IsCustomAPIURL() { + t.Error("IsCustomAPIURL() should return false when set to default") + } +} + +func TestIsCustomAPIURL_SetToCustom(t *testing.T) { + os.Setenv("KEYWAY_API_URL", "https://api.example.com") + defer os.Unsetenv("KEYWAY_API_URL") + + if !IsCustomAPIURL() { + t.Error("IsCustomAPIURL() should return true when set to custom URL") + } +} diff --git a/internal/version/github.go b/internal/version/github.go index 1baf298..b207a31 100644 --- a/internal/version/github.go +++ b/internal/version/github.go @@ -5,19 +5,28 @@ import ( "encoding/json" "fmt" "net/http" + + "github.com/keywaysh/cli/internal/config" ) const ( - githubReleasesURL = "https://api.github.com/repos/keywaysh/cli/releases/latest" + defaultGitHubReleasesURL = "https://api.github.com/repos/keywaysh/cli/releases/latest" ) type githubRelease struct { TagName string `json:"tag_name"` } -// FetchLatestVersion fetches the latest version from GitHub Releases +// FetchLatestVersion fetches the latest version from GitHub Releases. +// Returns an error if using a custom (self-hosted) API URL, since update +// checks only apply to the official Keyway distribution. func FetchLatestVersion(ctx context.Context) (string, error) { - req, err := http.NewRequestWithContext(ctx, "GET", githubReleasesURL, nil) + // Skip update checks for self-hosted instances + if config.IsCustomAPIURL() { + return "", fmt.Errorf("update checks disabled for self-hosted instances") + } + + req, err := http.NewRequestWithContext(ctx, "GET", defaultGitHubReleasesURL, nil) if err != nil { return "", err } diff --git a/internal/version/version.go b/internal/version/version.go index 6aac5ed..464208e 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" "time" + + "github.com/keywaysh/cli/internal/config" ) const ( @@ -30,6 +32,11 @@ func CheckForUpdate(ctx context.Context, currentVersion string) *UpdateInfo { return nil } + // Skip update check for self-hosted instances + if config.IsCustomAPIURL() { + return nil + } + // Skip check for dev builds if currentVersion == "dev" || currentVersion == "" { return nil @@ -79,8 +86,14 @@ func buildUpdateInfo(current, latest string, method InstallMethod) *UpdateInfo { } } -// GetUpdateCommand returns the update command for the given install method +// GetUpdateCommand returns the update command for the given install method. +// Returns an empty string for self-hosted instances where standard update +// commands do not apply. func GetUpdateCommand(method InstallMethod) string { + if config.IsCustomAPIURL() { + return "" + } + switch method { case InstallMethodNPM: return "npm update -g @keywaysh/cli" diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 9e49644..906fbaf 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -1,6 +1,10 @@ package version -import "testing" +import ( + "context" + "os" + "testing" +) func TestIsNewerVersion(t *testing.T) { tests := []struct { @@ -92,6 +96,9 @@ func TestParseVersion(t *testing.T) { } func TestGetUpdateCommand(t *testing.T) { + // Ensure default API URL for these tests + os.Unsetenv("KEYWAY_API_URL") + tests := []struct { method InstallMethod expected string @@ -111,3 +118,38 @@ func TestGetUpdateCommand(t *testing.T) { }) } } + +func TestGetUpdateCommand_SelfHosted(t *testing.T) { + os.Setenv("KEYWAY_API_URL", "https://api.example.com") + defer os.Unsetenv("KEYWAY_API_URL") + + methods := []InstallMethod{InstallMethodNPM, InstallMethodHomebrew, InstallMethodBinary} + for _, method := range methods { + result := GetUpdateCommand(method) + if result != "" { + t.Errorf("GetUpdateCommand(%q) with self-hosted = %q, want empty", method, result) + } + } +} + +func TestCheckForUpdate_SelfHosted(t *testing.T) { + os.Setenv("KEYWAY_API_URL", "https://api.example.com") + defer os.Unsetenv("KEYWAY_API_URL") + + ctx := context.Background() + result := CheckForUpdate(ctx, "1.0.0") + if result != nil { + t.Error("CheckForUpdate() should return nil for self-hosted instances") + } +} + +func TestFetchLatestVersion_SelfHosted(t *testing.T) { + os.Setenv("KEYWAY_API_URL", "https://api.example.com") + defer os.Unsetenv("KEYWAY_API_URL") + + ctx := context.Background() + _, err := FetchLatestVersion(ctx) + if err == nil { + t.Error("FetchLatestVersion() should return error for self-hosted instances") + } +}