Skip to content

Commit 3990176

Browse files
committed
Add pkg/go — Go module wrapper for go install distribution
Adds a thin Go wrapper at pkg/go/cmd/codebase-memory-mcp that detects the current platform and architecture, downloads the matching pre-built binary from GitHub Releases, verifies its SHA-256 checksum, caches it, and replaces the current process via exec. Install via: go install github.com/DeusData/codebase-memory-mcp/pkg/go/cmd/codebase-memory-mcp@latest The Go module proxy indexes this automatically once live on main.
1 parent 592be8e commit 3990176

2 files changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// codebase-memory-mcp — Go installer wrapper.
2+
//
3+
// On first run, downloads the pre-built binary for the current platform from
4+
// GitHub Releases, caches it, and replaces the current process with it.
5+
// Subsequent runs skip directly to exec.
6+
//
7+
// Install:
8+
//
9+
// go install github.com/DeusData/codebase-memory-mcp/pkg/go/cmd/codebase-memory-mcp@latest
10+
package main
11+
12+
import (
13+
"archive/tar"
14+
"archive/zip"
15+
"compress/gzip"
16+
"crypto/sha256"
17+
"encoding/hex"
18+
"fmt"
19+
"io"
20+
"net/http"
21+
"os"
22+
"os/exec"
23+
"path/filepath"
24+
"runtime"
25+
"strings"
26+
"syscall"
27+
)
28+
29+
const (
30+
repo = "DeusData/codebase-memory-mcp"
31+
version = "0.6.0"
32+
)
33+
34+
func main() {
35+
bin, err := ensureBinary()
36+
if err != nil {
37+
fmt.Fprintf(os.Stderr, "codebase-memory-mcp: %v\n", err)
38+
os.Exit(1)
39+
}
40+
if err := execBinary(bin, os.Args[1:]); err != nil {
41+
fmt.Fprintf(os.Stderr, "codebase-memory-mcp: %v\n", err)
42+
os.Exit(1)
43+
}
44+
}
45+
46+
func ensureBinary() (string, error) {
47+
bin := binPath()
48+
if _, err := os.Stat(bin); err == nil {
49+
return bin, nil
50+
}
51+
if err := download(bin); err != nil {
52+
return "", err
53+
}
54+
return bin, nil
55+
}
56+
57+
func binPath() string {
58+
name := "codebase-memory-mcp"
59+
if runtime.GOOS == "windows" {
60+
name += ".exe"
61+
}
62+
return filepath.Join(cacheDir(), version, name)
63+
}
64+
65+
func cacheDir() string {
66+
if d := os.Getenv("CBM_CACHE_DIR"); d != "" {
67+
return d
68+
}
69+
switch runtime.GOOS {
70+
case "windows":
71+
if d := os.Getenv("LOCALAPPDATA"); d != "" {
72+
return filepath.Join(d, "codebase-memory-mcp")
73+
}
74+
case "darwin":
75+
if home, err := os.UserHomeDir(); err == nil {
76+
return filepath.Join(home, "Library", "Caches", "codebase-memory-mcp")
77+
}
78+
}
79+
if d := os.Getenv("XDG_CACHE_HOME"); d != "" {
80+
return filepath.Join(d, "codebase-memory-mcp")
81+
}
82+
if home, err := os.UserHomeDir(); err == nil {
83+
return filepath.Join(home, ".cache", "codebase-memory-mcp")
84+
}
85+
return filepath.Join(os.TempDir(), "codebase-memory-mcp")
86+
}
87+
88+
func goos() string {
89+
switch runtime.GOOS {
90+
case "darwin":
91+
return "darwin"
92+
case "linux":
93+
return "linux"
94+
case "windows":
95+
return "windows"
96+
default:
97+
return runtime.GOOS
98+
}
99+
}
100+
101+
func goarch() string {
102+
switch runtime.GOARCH {
103+
case "amd64":
104+
return "amd64"
105+
case "arm64":
106+
return "arm64"
107+
default:
108+
return runtime.GOARCH
109+
}
110+
}
111+
112+
func download(dest string) error {
113+
platform := goos()
114+
arch := goarch()
115+
ext := "tar.gz"
116+
if platform == "windows" {
117+
ext = "zip"
118+
}
119+
120+
archive := fmt.Sprintf("codebase-memory-mcp-%s-%s.%s", platform, arch, ext)
121+
url := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", repo, version, archive)
122+
checksumURL := fmt.Sprintf("https://github.com/%s/releases/download/v%s/checksums.txt", repo, version)
123+
124+
fmt.Fprintf(os.Stderr, "codebase-memory-mcp: downloading v%s for %s/%s...\n", version, platform, arch)
125+
126+
tmp, err := os.MkdirTemp("", "cbm-install-*")
127+
if err != nil {
128+
return err
129+
}
130+
defer os.RemoveAll(tmp)
131+
132+
archivePath := filepath.Join(tmp, "cbm."+ext)
133+
if err := httpGet(url, archivePath); err != nil {
134+
return fmt.Errorf("download failed: %w", err)
135+
}
136+
137+
// Verify checksum if available (non-fatal if checksums.txt unreachable)
138+
if checksums, err := fetchChecksums(checksumURL); err == nil {
139+
if expected, ok := checksums[archive]; ok {
140+
if err := verifyChecksum(archivePath, expected); err != nil {
141+
return err
142+
}
143+
}
144+
}
145+
146+
binName := "codebase-memory-mcp"
147+
if platform == "windows" {
148+
binName += ".exe"
149+
}
150+
151+
if ext == "tar.gz" {
152+
if err := extractTarGz(archivePath, tmp, binName); err != nil {
153+
return fmt.Errorf("extraction failed: %w", err)
154+
}
155+
} else {
156+
if err := extractZip(archivePath, tmp, binName); err != nil {
157+
return fmt.Errorf("extraction failed: %w", err)
158+
}
159+
}
160+
161+
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
162+
return fmt.Errorf("could not create cache dir: %w", err)
163+
}
164+
165+
if err := copyFile(filepath.Join(tmp, binName), dest); err != nil {
166+
return fmt.Errorf("could not install binary: %w", err)
167+
}
168+
169+
if err := os.Chmod(dest, 0755); err != nil {
170+
return fmt.Errorf("could not set permissions: %w", err)
171+
}
172+
173+
return nil
174+
}
175+
176+
func httpGet(url, dest string) error {
177+
resp, err := http.Get(url) //nolint:gosec
178+
if err != nil {
179+
return err
180+
}
181+
defer resp.Body.Close()
182+
if resp.StatusCode != http.StatusOK {
183+
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
184+
}
185+
f, err := os.Create(dest)
186+
if err != nil {
187+
return err
188+
}
189+
defer f.Close()
190+
_, err = io.Copy(f, resp.Body)
191+
return err
192+
}
193+
194+
func fetchChecksums(url string) (map[string]string, error) {
195+
resp, err := http.Get(url) //nolint:gosec
196+
if err != nil {
197+
return nil, err
198+
}
199+
defer resp.Body.Close()
200+
if resp.StatusCode != http.StatusOK {
201+
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
202+
}
203+
body, err := io.ReadAll(resp.Body)
204+
if err != nil {
205+
return nil, err
206+
}
207+
result := make(map[string]string)
208+
for _, line := range strings.Split(string(body), "\n") {
209+
parts := strings.Fields(line)
210+
if len(parts) == 2 {
211+
result[parts[1]] = parts[0]
212+
}
213+
}
214+
return result, nil
215+
}
216+
217+
func verifyChecksum(path, expected string) error {
218+
f, err := os.Open(path)
219+
if err != nil {
220+
return err
221+
}
222+
defer f.Close()
223+
h := sha256.New()
224+
if _, err := io.Copy(h, f); err != nil {
225+
return err
226+
}
227+
actual := hex.EncodeToString(h.Sum(nil))
228+
if actual != expected {
229+
return fmt.Errorf("checksum mismatch: expected %s, got %s", expected, actual)
230+
}
231+
return nil
232+
}
233+
234+
func extractTarGz(archivePath, destDir, targetFile string) error {
235+
f, err := os.Open(archivePath)
236+
if err != nil {
237+
return err
238+
}
239+
defer f.Close()
240+
gz, err := gzip.NewReader(f)
241+
if err != nil {
242+
return err
243+
}
244+
defer gz.Close()
245+
tr := tar.NewReader(gz)
246+
for {
247+
hdr, err := tr.Next()
248+
if err == io.EOF {
249+
break
250+
}
251+
if err != nil {
252+
return err
253+
}
254+
if filepath.Base(hdr.Name) == targetFile {
255+
out, err := os.Create(filepath.Join(destDir, targetFile))
256+
if err != nil {
257+
return err
258+
}
259+
defer out.Close()
260+
_, err = io.Copy(out, tr) //nolint:gosec
261+
return err
262+
}
263+
}
264+
return fmt.Errorf("%s not found in archive", targetFile)
265+
}
266+
267+
func extractZip(archivePath, destDir, targetFile string) error {
268+
r, err := zip.OpenReader(archivePath)
269+
if err != nil {
270+
return err
271+
}
272+
defer r.Close()
273+
for _, f := range r.File {
274+
if filepath.Base(f.Name) == targetFile {
275+
rc, err := f.Open()
276+
if err != nil {
277+
return err
278+
}
279+
defer rc.Close()
280+
out, err := os.Create(filepath.Join(destDir, targetFile))
281+
if err != nil {
282+
return err
283+
}
284+
defer out.Close()
285+
_, err = io.Copy(out, rc) //nolint:gosec
286+
return err
287+
}
288+
}
289+
return fmt.Errorf("%s not found in archive", targetFile)
290+
}
291+
292+
func copyFile(src, dst string) error {
293+
in, err := os.Open(src)
294+
if err != nil {
295+
return err
296+
}
297+
defer in.Close()
298+
out, err := os.Create(dst)
299+
if err != nil {
300+
return err
301+
}
302+
defer out.Close()
303+
_, err = io.Copy(out, in)
304+
return err
305+
}
306+
307+
func execBinary(bin string, args []string) error {
308+
if runtime.GOOS == "windows" {
309+
cmd := exec.Command(bin, args...)
310+
cmd.Stdin = os.Stdin
311+
cmd.Stdout = os.Stdout
312+
cmd.Stderr = os.Stderr
313+
return cmd.Run()
314+
}
315+
return syscall.Exec(bin, append([]string{bin}, args...), os.Environ())
316+
}

pkg/go/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/DeusData/codebase-memory-mcp/pkg/go
2+
3+
go 1.26.1

0 commit comments

Comments
 (0)