|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "fmt" |
5 | 6 | "io" |
| 7 | + "net/http" |
6 | 8 | "os" |
7 | 9 | "os/exec" |
8 | 10 | "os/signal" |
9 | 11 | "path/filepath" |
| 12 | + "runtime" |
10 | 13 | "strings" |
11 | 14 | "syscall" |
12 | 15 | "time" |
@@ -65,6 +68,8 @@ func main() { |
65 | 68 | case "decrypt": |
66 | 69 | requireCmd("age") |
67 | 70 | cmdDecrypt(os.Args[2:]) |
| 71 | + case "update": |
| 72 | + cmdUpdate() |
68 | 73 | case "version", "--version", "-V": |
69 | 74 | fmt.Printf("sear %s\n", version) |
70 | 75 | case "help", "--help", "-h": |
@@ -933,6 +938,100 @@ func cmdVerify(args []string) { |
933 | 938 | } |
934 | 939 | } |
935 | 940 |
|
| 941 | +// ── update ────────────────────────────────────────────────────────── |
| 942 | + |
| 943 | +func cmdUpdate() { |
| 944 | + const repo = "8ff/sear" |
| 945 | + |
| 946 | + if version == "dev" { |
| 947 | + die("update not supported for dev builds (build with -ldflags to set version)") |
| 948 | + } |
| 949 | + |
| 950 | + // Fetch latest release |
| 951 | + resp, err := http.Get("https://api.github.com/repos/" + repo + "/releases/latest") |
| 952 | + if err != nil { |
| 953 | + die("failed to check for updates: %v", err) |
| 954 | + } |
| 955 | + defer resp.Body.Close() |
| 956 | + |
| 957 | + if resp.StatusCode != 200 { |
| 958 | + die("failed to check for updates: HTTP %d", resp.StatusCode) |
| 959 | + } |
| 960 | + |
| 961 | + var release struct { |
| 962 | + TagName string `json:"tag_name"` |
| 963 | + } |
| 964 | + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { |
| 965 | + die("failed to parse release info: %v", err) |
| 966 | + } |
| 967 | + |
| 968 | + latest := release.TagName |
| 969 | + if latest == version || latest == "" { |
| 970 | + fmt.Printf("sear %s is already the latest version\n", version) |
| 971 | + return |
| 972 | + } |
| 973 | + |
| 974 | + // Download new binary |
| 975 | + goos := runtime.GOOS |
| 976 | + goarch := runtime.GOARCH |
| 977 | + ext := "" |
| 978 | + if goos == "windows" { |
| 979 | + ext = ".exe" |
| 980 | + } |
| 981 | + binary := fmt.Sprintf("sear-%s-%s%s", goos, goarch, ext) |
| 982 | + url := fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", repo, latest, binary) |
| 983 | + |
| 984 | + fmt.Printf("Updating sear %s -> %s (%s/%s)\n", version, latest, goos, goarch) |
| 985 | + |
| 986 | + dlResp, err := http.Get(url) |
| 987 | + if err != nil { |
| 988 | + die("failed to download update: %v", err) |
| 989 | + } |
| 990 | + defer dlResp.Body.Close() |
| 991 | + |
| 992 | + if dlResp.StatusCode != 200 { |
| 993 | + die("failed to download update: HTTP %d", dlResp.StatusCode) |
| 994 | + } |
| 995 | + |
| 996 | + // Write to temp file |
| 997 | + tmp, err := os.CreateTemp("", "sear-update-*") |
| 998 | + if err != nil { |
| 999 | + die("failed to create temp file: %v", err) |
| 1000 | + } |
| 1001 | + tmpPath := tmp.Name() |
| 1002 | + |
| 1003 | + if _, err := io.Copy(tmp, dlResp.Body); err != nil { |
| 1004 | + tmp.Close() |
| 1005 | + os.Remove(tmpPath) |
| 1006 | + die("failed to download update: %v", err) |
| 1007 | + } |
| 1008 | + tmp.Close() |
| 1009 | + |
| 1010 | + if err := os.Chmod(tmpPath, 0755); err != nil { |
| 1011 | + os.Remove(tmpPath) |
| 1012 | + die("failed to set permissions: %v", err) |
| 1013 | + } |
| 1014 | + |
| 1015 | + // Replace current binary |
| 1016 | + self, err := os.Executable() |
| 1017 | + if err != nil { |
| 1018 | + os.Remove(tmpPath) |
| 1019 | + die("cannot locate current binary: %v", err) |
| 1020 | + } |
| 1021 | + self, err = filepath.EvalSymlinks(self) |
| 1022 | + if err != nil { |
| 1023 | + os.Remove(tmpPath) |
| 1024 | + die("cannot resolve binary path: %v", err) |
| 1025 | + } |
| 1026 | + |
| 1027 | + if err := os.Rename(tmpPath, self); err != nil { |
| 1028 | + os.Remove(tmpPath) |
| 1029 | + die("failed to replace binary (try: sudo sear update): %v", err) |
| 1030 | + } |
| 1031 | + |
| 1032 | + fmt.Printf("Done: sear %s\n", latest) |
| 1033 | +} |
| 1034 | + |
936 | 1035 | // ── helpers ───────────────────────────────────────────────────────── |
937 | 1036 |
|
938 | 1037 | // findLatestIdentityFile finds the most recently created age-yubikey-identity file |
@@ -1157,6 +1256,9 @@ Key Management: |
1157 | 1256 | keygen Generate ed25519-sk signing key on YubiKey |
1158 | 1257 | age-keygen Generate age encryption key on YubiKey |
1159 | 1258 |
|
| 1259 | +Other: |
| 1260 | + update Update sear to the latest release |
| 1261 | +
|
1160 | 1262 | Run 'sear <command> -h' for command help. |
1161 | 1263 | Run 'sear help setup' for YubiKey getting started guide. |
1162 | 1264 | Run 'sear help yubikey' for YubiKey management cheatsheet. |
|
0 commit comments