Files
magpie/update_cli.go
T
yetone bc141ad834 update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00

200 lines
6.3 KiB
Go

package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/yetone/magpie/internal/settings"
"github.com/yetone/magpie/internal/update"
)
// updateEveries are the intervals magpie update auto takes, by their
// minutes in settings.UpdateEveries.
var updateEveries = map[string]int{"30m": 30, "1h": 60, "6h": 360, "24h": 1440}
// updateAutoCmd is `magpie update auto [on|off] [30m|1h|6h|24h]`: whether
// the app asks for a newer version (and downloads it) by itself, and how
// often (#472). With neither, it says how it is set.
func updateAutoCmd(args []string) error {
s := settings.Load()
for _, a := range args {
switch a {
case "on", "off":
s.NoAutoUpdate = a == "off"
default:
m, ok := updateEveries[a]
if !ok {
return fmt.Errorf("magpie update auto takes on, off or how often (30m, 1h, 6h or 24h), not %q", a)
}
s.UpdateEvery = m
}
}
if len(args) > 0 {
if err := settings.Save(s); err != nil {
return err
}
}
every := "6h"
for k, m := range updateEveries {
if m == s.UpdateEvery {
every = k
}
}
if s.NoAutoUpdate {
fmt.Println("automatic updates are off", muted.Render("· magpie update or Settings' Check looks for one (every "+every+" when on)"))
return nil
}
fmt.Println("automatic updates are on", muted.Render("· magpie looks for a newer version every "+every+" and downloads it"))
return nil
}
// updateMirrorCmd is `magpie update mirror [<prefix>|off]`: the GitHub
// download mirror every update goes through, the app's own among them
// (settings.UpdateMirror). With nothing, it says which.
func updateMirrorCmd(args []string) error {
s := settings.Load()
if len(args) > 1 {
return fmt.Errorf("magpie update mirror takes one prefix, or off")
}
if len(args) == 1 {
switch m := strings.TrimSpace(args[0]); m {
case "off", "none", "":
s.UpdateMirror = ""
default:
if err := update.CheckMirror(m); err != nil {
return err
}
s.UpdateMirror = m
}
if err := settings.Save(s); err != nil {
return err
}
}
if s.UpdateMirror == "" {
fmt.Println("updates are downloaded from GitHub", muted.Render("· magpie update mirror <prefix> has them come through a mirror; the checksum is still usemagpie.ai's"))
return nil
}
fmt.Println("updates are downloaded through", bold.Render(s.UpdateMirror), muted.Render("· each is checked against usemagpie.ai's checksum · magpie update mirror off goes back to GitHub"))
return nil
}
// updateFlags takes --proxy and --mirror (each as --flag value or
// --flag=value) out of args.
func updateFlags(args []string) (rest []string, proxy, mirror string, err error) {
for i := 0; i < len(args); i++ {
a := args[i]
name, val, eq := strings.Cut(a, "=")
if name != "--proxy" && name != "--mirror" {
rest = append(rest, a)
continue
}
if !eq {
if i+1 >= len(args) {
return nil, "", "", fmt.Errorf("%s needs a value", name)
}
i++
val = args[i]
}
if name == "--proxy" {
if err := update.CheckProxy(val); err != nil {
return nil, "", "", err
}
proxy = val
continue
}
if val != "off" {
if err := update.CheckMirror(val); err != nil {
return nil, "", "", err
}
}
mirror = val
}
return rest, proxy, mirror, nil
}
// updateCmd is `magpie update [check] [--proxy <url>] [--mirror <prefix>]`:
// the app replaces its bundle, the terminal build its binary.
func updateCmd(args []string) error {
args, proxy, mirror, err := updateFlags(args)
if err != nil {
return err
}
if len(args) > 1 && args[1] == "auto" {
return updateAutoCmd(args[2:])
}
if len(args) > 1 && args[1] == "mirror" {
return updateMirrorCmd(args[2:])
}
if len(args) > 2 || len(args) == 2 && args[1] != "check" {
return fmt.Errorf("usage: magpie update [check] [--proxy <url>] [--mirror <prefix>]")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
ctx = update.WithMirror(update.WithProxy(ctx, proxy), mirror)
rel, err := update.Latest(ctx)
if err != nil {
return err
}
if !update.Newer(rel.Version, version) {
if update.Released(version) {
fmt.Println(green.Render("✓"), "magpie", version, muted.Render("is the latest"))
} else {
fmt.Println("magpie", version, muted.Render("was built from source; the latest release is "+rel.Version))
}
return nil
}
fmt.Println("magpie", bold.Render(rel.Version), "is out", muted.Render("(you have "+version+") · "+rel.URL))
if len(args) > 1 && args[1] == "check" {
return nil
}
if !update.Released(version) {
return fmt.Errorf("this magpie was built from source; update it the way you built it, or get the release from %s", update.Site)
}
if app := update.Bundle(); app != "" {
if update.Stuck(app) != "" {
return fmt.Errorf("%s can't be replaced where it is; move magpie to Applications, or download the new version from %s", tilde(app), update.Site)
}
fmt.Println(muted.Render(" downloading " + update.AppAsset() + via(ctx) + " …"))
staged, err := update.Stage(ctx, rel, app)
if err != nil {
return err
}
err = update.Install(staged, app)
if update.NeedsAdmin(err) {
err = update.InstallAsAdmin(staged, app)
}
if err != nil {
return err
}
fmt.Println(green.Render("✓"), "updated", tilde(app), "to", rel.Version, muted.Render("· quit and reopen magpie to use it"))
if stale := update.StaleCLI(rel.Version); stale != "" {
// The copy won't follow the app; the fix is the link install.sh
// makes. `magpie update` run from the copy can't do it (it
// replaces the copy with another copy), so name the real step.
bin := filepath.Join(app, "Contents", "MacOS", "magpie")
fmt.Println(muted.Render(" the `magpie` command at " + tilde(stale) + " is a copy behind this app; re-run the installer, or link it: ln -sf " + tilde(bin) + " " + tilde(stale)))
}
return nil
}
if exe, err := update.Executable(); err == nil && update.Homebrew(exe) {
return fmt.Errorf("this magpie was installed with Homebrew; update it with: brew upgrade magpie")
}
fmt.Println(muted.Render(" downloading " + update.BinaryAsset() + via(ctx) + " …"))
if err := update.ReplaceBinary(ctx, rel); err != nil {
return err
}
fmt.Println(green.Render("✓"), "updated to", rel.Version)
return nil
}
// via says the mirror a download goes through, if any.
func via(ctx context.Context) string {
if m := update.Mirror(ctx); m != "" {
return " through " + m
}
return ""
}