Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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 internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"fmt"
"net/http"
"net/url"

"github.com/keywaysh/cli/internal/config"
)

// DeviceStartResponse is the response from starting device login
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions internal/cmd/readme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/readme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
7 changes: 6 additions & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func printCustomHelp(cmd *cobra.Command) {

// Footer
fmt.Printf(" %s %s\n", dim("Run"), fmt.Sprintf("%s %s", cyan("keyway <command> --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 != "" {
Expand Down Expand Up @@ -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",
Expand Down
61 changes: 52 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Comment on lines +107 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize API URLs before comparison to avoid false “custom” detection.

If KEYWAY_API_URL is set to the default with a trailing slash, Line 110 will incorrectly treat it as custom and disable update checks. Normalize both sides before comparing.

🛠️ Proposed fix
 func IsCustomAPIURL() bool {
-	apiURL := os.Getenv("KEYWAY_API_URL")
-	return apiURL != "" && apiURL != DefaultAPIURL
+	apiURL := strings.TrimSuffix(GetAPIURL(), "/")
+	defaultURL := strings.TrimSuffix(DefaultAPIURL, "/")
+	return apiURL != defaultURL
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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
}
// IsCustomAPIURL returns true if using a non-default API URL (self-hosted)
func IsCustomAPIURL() bool {
apiURL := strings.TrimSuffix(os.Getenv("KEYWAY_API_URL"), "/")
defaultURL := strings.TrimSuffix(DefaultAPIURL, "/")
return apiURL != "" && apiURL != defaultURL
}
🤖 Prompt for AI Agents
In `@internal/config/config.go` around lines 107 - 111, IsCustomAPIURL incorrectly
treats values with trailing slashes as custom; update the function
IsCustomAPIURL to normalize both the environment value (KEYWAY_API_URL) and
DefaultAPIURL before comparing (e.g., TrimSpace, remove trailing slashes and
optionally lower-case) so "https://api.example.com/" equals
"https://api.example.com"; then compare the normalized strings and return true
only when non-empty and different from the normalized default.

125 changes: 125 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
15 changes: 12 additions & 3 deletions internal/version/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
15 changes: 14 additions & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strconv"
"strings"
"time"

"github.com/keywaysh/cli/internal/config"
)

const (
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading