From 24b8f59187a0c06b96da3728634ca1e0483ef677 Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 27 Jan 2026 13:42:51 +0100 Subject: [PATCH 1/3] fix: disable patch coverage check on main/develop branches Patch coverage should only run on PRs, not on direct pushes to main or develop. This was causing false failures on release merges. Co-Authored-By: Claude Opus 4.5 --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov.yml b/codecov.yml index 84422959..62279212 100644 --- a/codecov.yml +++ b/codecov.yml @@ -17,6 +17,7 @@ coverage: default: target: 30% threshold: 0% + only_pulls: true comment: # Codecov's recommended condensed format @@ -42,3 +43,4 @@ flag_management: threshold: 1% - type: patch target: 30% + only_pulls: true From b36fcc801ec445958c6e7546013d5dd73e6fd98e Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 27 Jan 2026 13:45:17 +0100 Subject: [PATCH 2/3] feat: detect installation method for update notifications Add detection for npm, Homebrew, and Go installations to show the appropriate update command in the update notification. Previously only npm was detected, falling back to the GitHub releases page URL. Now detects: - npm: node_modules/@tastehub/ckb path - brew: /opt/homebrew/Cellar/, /usr/local/Cellar/, linuxbrew paths - go: GOBIN, GOPATH/bin, or ~/go/bin paths Co-Authored-By: Claude Opus 4.5 --- internal/update/check.go | 140 +++++++++++++++++++++++++++++---- internal/update/check_test.go | 142 +++++++++++++++++++++++++++++++--- 2 files changed, 260 insertions(+), 22 deletions(-) diff --git a/internal/update/check.go b/internal/update/check.go index d0e7859f..aca53fd7 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -1,5 +1,6 @@ -// Package update provides npm update checking for CKB. -// It checks if a newer version is available on npm and notifies the user. +// Package update provides update checking for CKB. +// It checks if a newer version is available and notifies the user with +// the appropriate update command based on their installation method. package update import ( @@ -9,6 +10,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "strings" "time" @@ -30,6 +32,26 @@ const ( // npmPackageName is the npm package name npmPackageName = "@tastehub/ckb" + + // goModulePath is the Go module path for go install + goModulePath = "github.com/SimplyLiz/CodeMCP/cmd/ckb" + + // brewPackageName is the Homebrew formula name + brewPackageName = "ckb" +) + +// InstallMethod represents how CKB was installed +type InstallMethod string + +const ( + // InstallMethodNPM indicates installation via npm + InstallMethodNPM InstallMethod = "npm" + // InstallMethodBrew indicates installation via Homebrew + InstallMethodBrew InstallMethod = "brew" + // InstallMethodGo indicates installation via go install + InstallMethodGo InstallMethod = "go" + // InstallMethodUnknown indicates unknown installation method + InstallMethodUnknown InstallMethod = "unknown" ) // githubReleaseInfo represents the relevant fields from GitHub Releases API @@ -46,24 +68,24 @@ type UpdateInfo struct { // Checker handles update checking with caching type Checker struct { - cache *Cache - isNpmPath bool + cache *Cache + installMethod InstallMethod } // NewChecker creates a new update checker. -// It automatically detects if running from an npm installation. +// It automatically detects the installation method (npm, brew, go, or unknown). func NewChecker() *Checker { return &Checker{ - cache: NewCache(), - isNpmPath: detectNpmInstall(), + cache: NewCache(), + installMethod: detectInstallMethod(), } } -// detectNpmInstall checks if the current executable is running from an npm installation -func detectNpmInstall() bool { +// detectInstallMethod detects how CKB was installed based on the executable path +func detectInstallMethod() InstallMethod { execPath, err := os.Executable() if err != nil { - return false + return InstallMethodUnknown } // Resolve symlinks to get the real path @@ -72,15 +94,101 @@ func detectNpmInstall() bool { realPath = execPath } + // Check each installation method + if detectNpmInstall(realPath) { + return InstallMethodNPM + } + if detectBrewInstall(realPath) { + return InstallMethodBrew + } + if detectGoInstall(realPath) { + return InstallMethodGo + } + + return InstallMethodUnknown +} + +// detectNpmInstall checks if the path indicates an npm installation +func detectNpmInstall(realPath string) bool { // Check if path contains node_modules/@tastehub/ckb return strings.Contains(realPath, "node_modules") && strings.Contains(realPath, "@tastehub") && strings.Contains(realPath, "ckb") } +// detectBrewInstall checks if the path indicates a Homebrew installation +func detectBrewInstall(realPath string) bool { + if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { + return false + } + + // macOS Apple Silicon: /opt/homebrew/Cellar/ckb/... + // macOS Intel / Linux: /usr/local/Cellar/ckb/... + // Also check for /home/linuxbrew/.linuxbrew/Cellar/... + brewPrefixes := []string{ + "/opt/homebrew/Cellar/", + "/usr/local/Cellar/", + "/home/linuxbrew/.linuxbrew/Cellar/", + } + + for _, prefix := range brewPrefixes { + if strings.HasPrefix(realPath, prefix) { + return true + } + } + + // Also check HOMEBREW_PREFIX environment variable + if homebrewPrefix := os.Getenv("HOMEBREW_PREFIX"); homebrewPrefix != "" { + cellarPath := filepath.Join(homebrewPrefix, "Cellar") + if strings.HasPrefix(realPath, cellarPath) { + return true + } + } + + return false +} + +// detectGoInstall checks if the path indicates a go install installation +func detectGoInstall(realPath string) bool { + // Check for common Go binary locations + // $GOPATH/bin (defaults to ~/go/bin) + // $GOBIN if set + + // Check GOBIN first + if goBin := os.Getenv("GOBIN"); goBin != "" { + if strings.HasPrefix(realPath, goBin) { + return true + } + } + + // Check GOPATH/bin + goPath := os.Getenv("GOPATH") + if goPath == "" { + // Default GOPATH is ~/go + if home, err := os.UserHomeDir(); err == nil { + goPath = filepath.Join(home, "go") + } + } + + if goPath != "" { + goBinPath := filepath.Join(goPath, "bin") + if strings.HasPrefix(realPath, goBinPath) { + return true + } + } + + // Also check for /go/bin pattern anywhere in path (covers non-standard setups) + return strings.Contains(realPath, string(filepath.Separator)+"go"+string(filepath.Separator)+"bin"+string(filepath.Separator)) +} + +// InstallMethod returns the detected installation method +func (c *Checker) InstallMethod() InstallMethod { + return c.installMethod +} + // IsNpmInstall returns true if running from an npm installation func (c *Checker) IsNpmInstall() bool { - return c.isNpmPath + return c.installMethod == InstallMethodNPM } // CheckCached checks the cache for a pending update notification. @@ -222,10 +330,16 @@ func (c *Checker) compareVersions(latest string) *UpdateInfo { // getUpgradeCommand returns the appropriate upgrade command based on install method func (c *Checker) getUpgradeCommand() string { - if c.isNpmPath { + switch c.installMethod { + case InstallMethodNPM: return "npm update -g " + npmPackageName + case InstallMethodBrew: + return "brew upgrade " + brewPackageName + case InstallMethodGo: + return "go install " + goModulePath + "@latest" + default: + return githubReleasesPage } - return githubReleasesPage } // isNewerVersion returns true if version a is newer than version b. diff --git a/internal/update/check_test.go b/internal/update/check_test.go index c89c622a..dfbe0cf0 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -61,8 +61,8 @@ func TestChecker_Check_DisabledByEnv(t *testing.T) { defer func() { _ = os.Unsetenv("CKB_NO_UPDATE_CHECK") }() checker := &Checker{ - cache: NewCache(), - isNpmPath: true, // Pretend we're an npm install + cache: NewCache(), + installMethod: InstallMethodNPM, // Pretend we're an npm install } result := checker.Check(context.Background()) @@ -83,7 +83,7 @@ func TestChecker_CheckCached_EmptyCache(t *testing.T) { cache: &Cache{ path: filepath.Join(tmpDir, "update-check.json"), }, - isNpmPath: false, + installMethod: InstallMethodUnknown, } result := checker.CheckCached() @@ -107,8 +107,8 @@ func TestChecker_CheckCached_WithCachedUpdate(t *testing.T) { cache.Set("99.0.0") checker := &Checker{ - cache: cache, - isNpmPath: false, + cache: cache, + installMethod: InstallMethodUnknown, } result := checker.CheckCached() @@ -138,8 +138,8 @@ func TestChecker_CheckCached_NpmInstall(t *testing.T) { cache.Set("99.0.0") checker := &Checker{ - cache: cache, - isNpmPath: true, // npm install + cache: cache, + installMethod: InstallMethodNPM, // npm install } result := checker.CheckCached() @@ -154,8 +154,8 @@ func TestChecker_CheckCached_NpmInstall(t *testing.T) { func TestChecker_FetchLatestVersion_Timeout(t *testing.T) { checker := &Checker{ - cache: NewCache(), - isNpmPath: false, + cache: NewCache(), + installMethod: InstallMethodUnknown, } // Test with a very short timeout context @@ -281,4 +281,128 @@ func TestUpdateInfo_FormatUpdateMessage(t *testing.T) { t.Error("expected non-empty plain message") } }) + + t.Run("brew command", func(t *testing.T) { + info := &UpdateInfo{ + CurrentVersion: "7.3.0", + LatestVersion: "7.4.0", + UpdateCommand: "brew upgrade ckb", + } + + msg := info.FormatUpdateMessage() + if !strings.Contains(msg, "Run:") { + t.Error("expected 'Run:' prefix for brew command") + } + if !strings.Contains(msg, "brew upgrade") { + t.Error("expected brew upgrade command in message") + } + }) + + t.Run("go install command", func(t *testing.T) { + info := &UpdateInfo{ + CurrentVersion: "7.3.0", + LatestVersion: "7.4.0", + UpdateCommand: "go install github.com/SimplyLiz/CodeMCP/cmd/ckb@latest", + } + + msg := info.FormatUpdateMessage() + if !strings.Contains(msg, "Run:") { + t.Error("expected 'Run:' prefix for go install command") + } + if !strings.Contains(msg, "go install") { + t.Error("expected go install command in message") + } + }) +} + +func TestDetectNpmInstall(t *testing.T) { + tests := []struct { + path string + expected bool + }{ + {"/usr/local/lib/node_modules/@tastehub/ckb-darwin-arm64/bin/ckb", true}, + {"/home/user/.npm-global/lib/node_modules/@tastehub/ckb/bin/ckb", true}, + {"/opt/homebrew/Cellar/ckb/8.0.0/bin/ckb", false}, + {"/home/user/go/bin/ckb", false}, + {"/usr/local/bin/ckb", false}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + result := detectNpmInstall(tt.path) + if result != tt.expected { + t.Errorf("detectNpmInstall(%q) = %v, want %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestDetectBrewInstall(t *testing.T) { + tests := []struct { + path string + expected bool + }{ + {"/opt/homebrew/Cellar/ckb/8.0.0/bin/ckb", true}, + {"/usr/local/Cellar/ckb/8.0.0/bin/ckb", true}, + {"/home/linuxbrew/.linuxbrew/Cellar/ckb/8.0.0/bin/ckb", true}, + {"/usr/local/lib/node_modules/@tastehub/ckb/bin/ckb", false}, + {"/home/user/go/bin/ckb", false}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + result := detectBrewInstall(tt.path) + if result != tt.expected { + t.Errorf("detectBrewInstall(%q) = %v, want %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestDetectGoInstall(t *testing.T) { + home, _ := os.UserHomeDir() + + tests := []struct { + path string + expected bool + }{ + {filepath.Join(home, "go", "bin", "ckb"), true}, + {"/home/user/go/bin/ckb", true}, + {"/opt/homebrew/Cellar/ckb/8.0.0/bin/ckb", false}, + {"/usr/local/lib/node_modules/@tastehub/ckb/bin/ckb", false}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + result := detectGoInstall(tt.path) + if result != tt.expected { + t.Errorf("detectGoInstall(%q) = %v, want %v", tt.path, result, tt.expected) + } + }) + } +} + +func TestChecker_GetUpgradeCommand(t *testing.T) { + tests := []struct { + method InstallMethod + expected string + }{ + {InstallMethodNPM, "npm update -g @tastehub/ckb"}, + {InstallMethodBrew, "brew upgrade ckb"}, + {InstallMethodGo, "go install github.com/SimplyLiz/CodeMCP/cmd/ckb@latest"}, + {InstallMethodUnknown, githubReleasesPage}, + } + + for _, tt := range tests { + t.Run(string(tt.method), func(t *testing.T) { + checker := &Checker{ + cache: NewCache(), + installMethod: tt.method, + } + result := checker.getUpgradeCommand() + if result != tt.expected { + t.Errorf("getUpgradeCommand() for %s = %q, want %q", tt.method, result, tt.expected) + } + }) + } } From c3b7e86253350cfb0ed16180ee95ee066295f2ba Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 27 Jan 2026 13:48:03 +0100 Subject: [PATCH 3/3] feat: add `ckb update` command to self-update Add a CLI command that detects the installation method and runs the appropriate update command (npm/brew/go install). Includes --dry-run flag to preview the command without executing. For unknown installation methods, opens the GitHub releases page. Co-Authored-By: Claude Opus 4.5 --- cmd/ckb/update.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 cmd/ckb/update.go diff --git a/cmd/ckb/update.go b/cmd/ckb/update.go new file mode 100644 index 00000000..d057c130 --- /dev/null +++ b/cmd/ckb/update.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "runtime" + + "github.com/SimplyLiz/CodeMCP/internal/update" + "github.com/SimplyLiz/CodeMCP/internal/version" + + "github.com/spf13/cobra" +) + +var ( + updateDryRun bool +) + +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update CKB to the latest version", + Long: `Update CKB to the latest version using the appropriate package manager. + +Automatically detects how CKB was installed and runs the correct update command: + - npm: npm update -g @tastehub/ckb + - brew: brew upgrade ckb + - go: go install github.com/SimplyLiz/CodeMCP/cmd/ckb@latest + +If the installation method cannot be detected, opens the GitHub releases page.`, + Run: runUpdate, +} + +func init() { + rootCmd.AddCommand(updateCmd) + updateCmd.Flags().BoolVar(&updateDryRun, "dry-run", false, "Show the update command without executing it") +} + +func runUpdate(cmd *cobra.Command, args []string) { + checker := update.NewChecker() + method := checker.InstallMethod() + + fmt.Printf("Current version: %s\n", version.Version) + fmt.Printf("Install method: %s\n", formatInstallMethod(method)) + + switch method { + case update.InstallMethodNPM: + runPackageManagerUpdate("npm", []string{"update", "-g", "@tastehub/ckb"}) + case update.InstallMethodBrew: + runPackageManagerUpdate("brew", []string{"upgrade", "ckb"}) + case update.InstallMethodGo: + runPackageManagerUpdate("go", []string{"install", "github.com/SimplyLiz/CodeMCP/cmd/ckb@latest"}) + default: + openReleasesPage() + } +} + +func formatInstallMethod(method update.InstallMethod) string { + switch method { + case update.InstallMethodNPM: + return "npm" + case update.InstallMethodBrew: + return "Homebrew" + case update.InstallMethodGo: + return "go install" + default: + return "unknown" + } +} + +func runPackageManagerUpdate(command string, args []string) { + cmdStr := command + for _, arg := range args { + cmdStr += " " + arg + } + + if updateDryRun { + fmt.Printf("\nWould run: %s\n", cmdStr) + return + } + + fmt.Printf("\nRunning: %s\n\n", cmdStr) + + execCmd := exec.Command(command, args...) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + execCmd.Stdin = os.Stdin + + if err := execCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "\nUpdate failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("\nUpdate complete!") +} + +func openReleasesPage() { + url := "https://github.com/SimplyLiz/CodeMCP/releases" + + fmt.Printf("\nCould not detect installation method.\n") + fmt.Printf("Please visit: %s\n", url) + + if updateDryRun { + fmt.Printf("\nWould open: %s\n", url) + return + } + + // Try to open the URL in the default browser + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + default: + return + } + + _ = cmd.Start() +}