Skip to content

Commit 221fb57

Browse files
committed
auto update for npm users
1 parent 63c1e5f commit 221fb57

6 files changed

Lines changed: 175 additions & 18 deletions

File tree

homebrew-tap

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ func main() {
3030
return
3131
}
3232

33+
// If installed via npm and a newer release exists, swap the binary on disk
34+
// and re-exec into it. Does nothing for non-npm installs or when already up
35+
// to date. Must run before any child processes are spawned.
36+
tui.CheckAndSelfUpdate(Version)
37+
3338
// Set up directories:
3439
// Browser data: ~/.local/share/reels/
3540
// Cache: ~/.cache/reels/,

reels-bin

Submodule reels-bin updated from 8944a22 to 21989e3

tui/update.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package tui
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"path/filepath"
11+
"runtime"
12+
"strings"
13+
"syscall"
14+
"time"
15+
)
16+
17+
// CheckAndSelfUpdate. When reels was installed via npm and a newer release is
18+
// available, downloads the new platform binary, atomically swaps it into place
19+
// over the running executable, then re-execs the new binary with the same
20+
// argv/env. On success this function does not return, the new program takes over.
21+
//
22+
// On any failure (not npm, no update, network error, no write perms, etc.),
23+
// it returns silently and lets the app continue normally.
24+
//
25+
// Must be called BEFORE the TUI starts and BEFORE any child processes (Chrome)
26+
// are spawned, since exec replaces the current image but does not touch
27+
// children.
28+
func CheckAndSelfUpdate(currentVersion string) {
29+
if currentVersion == "" || currentVersion == "dev" {
30+
return
31+
}
32+
exePath, ok := detectNpmInstall()
33+
if !ok {
34+
return
35+
}
36+
asset, ok := releaseAssetName()
37+
if !ok {
38+
return
39+
}
40+
latest, ok := fetchLatestVersion()
41+
if !ok || latest == "" || latest == currentVersion {
42+
return
43+
}
44+
45+
url := fmt.Sprintf("https://github.com/njyeung/reels/releases/download/v%s/%s", latest, asset)
46+
if err := downloadAndReplace(url, exePath); err != nil {
47+
fmt.Fprintf(os.Stderr, "reels: self-update failed: %v\n", err)
48+
return
49+
}
50+
51+
// Since we're on UNIX, we can re-exec the new binary with the same argv and env.
52+
// On success this replaces the running process with a new process and does not return
53+
if err := syscall.Exec(exePath, os.Args, os.Environ()); err != nil {
54+
fmt.Fprintf(os.Stderr, "reels: failed to relaunch after update: %v\n", err)
55+
os.Exit(1)
56+
}
57+
}
58+
59+
// detectNpmInstall returns the resolved binary path if reels was installed via
60+
// npm (i.e. lives under a node_modules/@reels/<plat>/bin/reels path).
61+
func detectNpmInstall() (string, bool) {
62+
exe, err := os.Executable()
63+
if err != nil {
64+
return "", false
65+
}
66+
resolved, err := filepath.EvalSymlinks(exe)
67+
if err != nil {
68+
resolved = exe
69+
}
70+
norm := filepath.ToSlash(resolved)
71+
if !strings.Contains(norm, "/node_modules/@reels/") {
72+
return "", false
73+
}
74+
if !strings.HasSuffix(norm, "/bin/reels") {
75+
return "", false
76+
}
77+
return resolved, true
78+
}
79+
80+
// fetchLatestVersion queries the GitHub releases API for the most recent tag
81+
// (with the leading "v" stripped). Used by both the pre-TUI self-update and
82+
// the in-TUI banner check.
83+
func fetchLatestVersion() (string, bool) {
84+
client := &http.Client{Timeout: 3 * time.Second}
85+
resp, err := client.Get("https://api.github.com/repos/njyeung/reels/releases/latest")
86+
if err != nil {
87+
return "", false
88+
}
89+
defer resp.Body.Close()
90+
var release struct {
91+
TagName string `json:"tag_name"`
92+
}
93+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
94+
return "", false
95+
}
96+
return strings.TrimPrefix(release.TagName, "v"), true
97+
}
98+
99+
// releaseAssetName maps the current GOOS/GOARCH to the asset name uploaded
100+
// by the GitHub Actions release workflow.
101+
func releaseAssetName() (string, bool) {
102+
switch runtime.GOOS + "/" + runtime.GOARCH {
103+
case "linux/amd64":
104+
return "reels-linux-amd64", true
105+
case "linux/arm64":
106+
return "reels-linux-arm64", true
107+
case "darwin/arm64":
108+
return "reels-darwin-arm64", true
109+
}
110+
return "", false
111+
}
112+
113+
func downloadAndReplace(url, target string) error {
114+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
115+
defer cancel()
116+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
117+
if err != nil {
118+
return err
119+
}
120+
resp, err := http.DefaultClient.Do(req)
121+
if err != nil {
122+
return err
123+
}
124+
defer resp.Body.Close()
125+
if resp.StatusCode != http.StatusOK {
126+
return fmt.Errorf("download failed: %s", resp.Status)
127+
}
128+
return writeAtomic(target, resp.Body)
129+
}
130+
131+
// writeAtomic streams body into a temp file in the same directory as target,
132+
// then renames it onto target. On Unix this is atomic and works even if the
133+
// target file is currently being executed (the kernel keeps the old inode
134+
// alive until the process exits).
135+
func writeAtomic(target string, body io.Reader) error {
136+
dir := filepath.Dir(target)
137+
tmp, err := os.CreateTemp(dir, ".reels-update-*")
138+
if err != nil {
139+
return err
140+
}
141+
tmpPath := tmp.Name()
142+
cleanup := true
143+
defer func() {
144+
if cleanup {
145+
os.Remove(tmpPath)
146+
}
147+
}()
148+
149+
if _, err := io.Copy(tmp, body); err != nil {
150+
tmp.Close()
151+
return err
152+
}
153+
if err := tmp.Close(); err != nil {
154+
return err
155+
}
156+
if err := os.Chmod(tmpPath, 0o755); err != nil {
157+
return err
158+
}
159+
if err := os.Rename(tmpPath, target); err != nil {
160+
return err
161+
}
162+
cleanup = false
163+
return nil
164+
}

tui/view_loading.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,11 @@ func (m Model) checkVersion() tea.Msg {
4343
if m.version == "dev" {
4444
return versionCheckMsg{}
4545
}
46-
client := &http.Client{Timeout: 3 * time.Second}
47-
resp, err := client.Get("https://api.github.com/repos/njyeung/reels/releases/latest")
48-
if err != nil {
46+
latest, ok := fetchLatestVersion()
47+
if !ok || latest == "" || latest == m.version {
4948
return versionCheckMsg{}
5049
}
51-
defer resp.Body.Close()
52-
var release struct {
53-
TagName string `json:"tag_name"`
54-
}
55-
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
56-
return versionCheckMsg{}
57-
}
58-
latest := strings.TrimPrefix(release.TagName, "v")
59-
if latest != "" && latest != m.version {
60-
return versionCheckMsg{latest: latest}
61-
}
62-
return versionCheckMsg{}
50+
return versionCheckMsg{latest: latest}
6351
}
6452

6553
func renderLoadingScreen(width, height int, barText string, barStyle lipgloss.Style, scrollOffset int) string {

0 commit comments

Comments
 (0)