A Claude subscription signs in and answers through the local `claude`, so
on a server or in the Docker image, which has none, it couldn't be used
(Jorben on Discord: could it be downloaded on first use, as Bun is).
- internal/claudecode: Anthropic's native build, as claude.ai/install.sh
takes it: downloads.claude.ai/claude-code-releases/latest names the
version, its manifest.json the platform's SHA-256 and size (linux
glibc/musl, darwin, win32). A build that doesn't match both is not kept.
It goes to <cache>/claude-code/<version>/claude, one version kept. Find
takes the machine's own claude first, then the download.
- magpie claude-code [install [--yes]|remove]: says the version, platform,
size, the URL it comes from, where it goes, that its checksum is checked
and that it is Anthropic's software under Anthropic's terms, then asks
[y/N]. magpie accounts add claude offers it when there is no Claude Code
(not on Windows, where one in WSL may be the one).
- the gateway's claudeBinary and the sign-in's claudeExecutable take
claudecode.Find; their "not installed" errors name the command.
- Docker docs (en, zh, ja): Claude sign-in needs `docker exec -it magpie
magpie claude-code install` first; kept in /config/cache.
The Dockerfile is unchanged: distroless cc-debian12 has glibc.
Run:
- go test -tags nogui ./... under a temp HOME with pinned caches: all ok
- gofmt -l: none; go vet (darwin), GOOS=linux and GOOS=windows go vet
-tags nogui ./...: clean; darwin, linux, windows builds
- TestClaudeSubscriptionRunsTheDownloadedClaudeCode fails on the old
proc.FindTool lookup ("Claude Code is not installed"); taking the
checksum compare out fails TestInstallRefusesABuildTheManifestDoesntName
- real: magpie claude-code install --yes downloaded Claude Code 2.1.296
darwin-arm64 (230 MB) into a sandbox HOME, checked; it ran --version
- real, in gcr.io/distroless/cc-debian12:nonroot with the image's /config
layout: install downloaded 2.1.296 linux-arm64 (245 MB) to
/config/cache/magpie/claude-code, it ran `2.1.296 (Claude Code)` there,
and magpie accounts add claude went on to print the claude.ai sign-in
link and ask for the pasted address
- the claudecode tests built for linux/arm64 pass in debian:12-slim
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/yetone/magpie/internal/claudecode"
|
|
)
|
|
|
|
// claudeCodeOut runs `magpie claude-code args…` and gives what it printed.
|
|
func claudeCodeOut(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := os.Stdout
|
|
os.Stdout = w
|
|
var out strings.Builder
|
|
done := make(chan struct{})
|
|
go func() {
|
|
b, _ := io.ReadAll(r)
|
|
out.Write(b)
|
|
close(done)
|
|
}()
|
|
err = claudeCodeCmd(args)
|
|
w.Close()
|
|
os.Stdout = old
|
|
<-done
|
|
r.Close()
|
|
return out.String(), err
|
|
}
|
|
|
|
// `magpie claude-code install --yes` on a machine with no Claude Code (a
|
|
// container, Jorben on Discord) downloads the release's build, says from
|
|
// where and to where, and the subscription then runs it.
|
|
func TestClaudeCodeInstallDownloadsTheRelease(t *testing.T) {
|
|
platform, err := claudecode.Platform(runtime.GOOS, runtime.GOARCH, false)
|
|
if err != nil {
|
|
t.Skip(err)
|
|
}
|
|
if runtime.GOOS == "linux" {
|
|
if _, err := os.Stat("/lib/libc.musl-x86_64.so.1"); err == nil {
|
|
t.Skip("a musl Linux asks for the -musl build")
|
|
}
|
|
}
|
|
const body = "#!/bin/sh\necho '2.1.296 (Claude Code)'\n"
|
|
sum := sha256.Sum256([]byte(body))
|
|
exe := map[bool]string{true: "claude.exe", false: "claude"}[runtime.GOOS == "windows"]
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/latest":
|
|
fmt.Fprint(w, "2.1.296")
|
|
case "/2.1.296/manifest.json":
|
|
fmt.Fprintf(w, `{"version":"2.1.296","manifestSignatureEnforcement":"flag","platforms":{%q:{"binary":%q,"checksum":%q,"size":%d}}}`,
|
|
platform, exe, hex.EncodeToString(sum[:]), len(body))
|
|
case "/2.1.296/" + platform + "/" + exe:
|
|
fmt.Fprint(w, body)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
oldBase, oldOwn := claudecode.Base, claudeCode.own
|
|
claudecode.Base, claudeCode.own = srv.URL, func() string { return "" }
|
|
t.Cleanup(func() { claudecode.Base, claudeCode.own = oldBase, oldOwn; _ = claudecode.Remove() })
|
|
|
|
out, err := claudeCodeOut(t)
|
|
if err != nil || !strings.Contains(out, "magpie claude-code install") {
|
|
t.Fatalf("status before: %q, %v", out, err)
|
|
}
|
|
out, err = claudeCodeOut(t, "install", "--yes")
|
|
if err != nil {
|
|
t.Fatalf("install: %v\n%s", err, out)
|
|
}
|
|
for _, want := range []string{"2.1.296", srv.URL + "/2.1.296/" + platform + "/" + exe, claudecode.Root(), "SHA-256", "downloaded and checked"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("install didn't say %q:\n%s", want, out)
|
|
}
|
|
}
|
|
if p, v := claudecode.Downloaded(); v != "2.1.296" || p == "" {
|
|
t.Fatalf("Downloaded() = %q, %q", p, v)
|
|
}
|
|
if out, _ := claudeCodeOut(t, "install", "--yes"); !strings.Contains(out, "downloaded already") {
|
|
t.Errorf("a second install: %q", out)
|
|
}
|
|
if out, _ := claudeCodeOut(t); !strings.Contains(out, "downloaded by magpie") {
|
|
t.Errorf("status after: %q", out)
|
|
}
|
|
if _, err := claudeCodeOut(t, "remove"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p, _ := claudecode.Downloaded(); p != "" {
|
|
t.Errorf("remove left %s", p)
|
|
}
|
|
}
|