From a3c3f03a549839763afc175c708e9b9faf242bfd Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:24:55 +0200 Subject: [PATCH 1/9] Implement automatic update feature for Anytype CLI --- cmd/update/update.go | 320 +--------------------------- core/autoupdate/autoupdate.go | 111 ++++++++++ core/update/update.go | 385 ++++++++++++++++++++++++++++++++++ main.go | 3 + 4 files changed, 504 insertions(+), 315 deletions(-) create mode 100644 core/autoupdate/autoupdate.go create mode 100644 core/update/update.go diff --git a/cmd/update/update.go b/cmd/update/update.go index 673dc07..df25cc9 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -1,29 +1,11 @@ package update import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/anyproto/anytype-cli/core" "github.com/anyproto/anytype-cli/core/output" + "github.com/anyproto/anytype-cli/core/update" "github.com/spf13/cobra" ) -const ( - githubOwner = "anyproto" - githubRepo = "anytype-cli" -) - func NewUpdateCmd() *cobra.Command { return &cobra.Command{ Use: "update", @@ -36,26 +18,21 @@ func NewUpdateCmd() *cobra.Command { func runUpdate(cmd *cobra.Command, args []string) error { output.Info("Checking for updates...") - latest, err := getLatestVersion() + latest, err := update.GetLatestVersion() if err != nil { return output.Error("failed to check latest version: %w", err) } - current := core.GetVersion() - - currentBase := current - if idx := strings.Index(current, "-"); idx != -1 { - currentBase = current[:idx] - } + current := update.GetCurrentVersion() - if currentBase >= latest { + if !update.NeedsUpdate(current, latest) { output.Info("Already up to date (%s)", current) return nil } output.Info("Updating from %s to %s...", current, latest) - if err := downloadAndInstall(latest); err != nil { + if err := update.DownloadAndInstall(latest); err != nil { return output.Error("update failed: %w", err) } @@ -63,290 +40,3 @@ func runUpdate(cmd *cobra.Command, args []string) error { output.Info("Restart your terminal or run 'anytype' to use the new version") return nil } - -func getLatestVersion() (string, error) { - resp, err := githubAPI("GET", "/releases/latest", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", handleAPIError(resp) - } - - var release struct { - TagName string `json:"tag_name"` - } - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { - return "", output.Error("failed to parse release: %w", err) - } - - return release.TagName, nil -} - -func downloadAndInstall(version string) error { - tempDir, err := os.MkdirTemp("", "anytype-update-*") - if err != nil { - return output.Error("failed to create temp dir: %w", err) - } - defer os.RemoveAll(tempDir) - - archivePath := filepath.Join(tempDir, getArchiveName(version)) - if err := downloadRelease(version, archivePath); err != nil { - return err - } - - if err := extractArchive(archivePath, tempDir); err != nil { - return output.Error("failed to extract: %w", err) - } - - binaryName := "anytype" - if runtime.GOOS == "windows" { - binaryName = "anytype.exe" - } - - newBinary := filepath.Join(tempDir, binaryName) - if _, err := os.Stat(newBinary); err != nil { - return output.Error("binary not found in archive (expected %s)", binaryName) - } - - if err := replaceBinary(newBinary); err != nil { - return output.Error("failed to install: %w", err) - } - - return nil -} - -func getArchiveName(version string) string { - base := fmt.Sprintf("anytype-cli-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) - if runtime.GOOS == "windows" { - return base + ".zip" - } - return base + ".tar.gz" -} - -func downloadRelease(version, destination string) error { - archiveName := filepath.Base(destination) - output.Info("Downloading %s...", archiveName) - - if token := os.Getenv("GITHUB_TOKEN"); token != "" { - return downloadViaAPI(version, archiveName, destination) - } - - url := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", - githubOwner, githubRepo, version, archiveName) - - return downloadFile(url, destination, "") -} - -func downloadViaAPI(version, filename, destination string) error { - resp, err := githubAPI("GET", fmt.Sprintf("/releases/tags/%s", version), nil) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return handleAPIError(resp) - } - - var release struct { - Assets []struct { - Name string `json:"name"` - URL string `json:"url"` - } `json:"assets"` - } - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { - return output.Error("failed to parse release: %w", err) - } - - var assetURL string - for _, asset := range release.Assets { - if asset.Name == filename { - assetURL = asset.URL - break - } - } - if assetURL == "" { - return output.Error("release asset %s not found", filename) - } - - return downloadFile(assetURL, destination, os.Getenv("GITHUB_TOKEN")) -} - -func downloadFile(url, destination, token string) error { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - - if token != "" { - req.Header.Set("Authorization", "token "+token) - req.Header.Set("Accept", "application/octet-stream") - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return output.Error("download failed with status %d", resp.StatusCode) - } - - out, err := os.Create(destination) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - return err -} - -func extractArchive(archivePath, destDir string) error { - if strings.HasSuffix(archivePath, ".zip") { - return extractZip(archivePath, destDir) - } - return extractTarGz(archivePath, destDir) -} - -func extractTarGz(archivePath, destDir string) error { - file, err := os.Open(archivePath) - if err != nil { - return err - } - defer file.Close() - - gz, err := gzip.NewReader(file) - if err != nil { - return err - } - defer gz.Close() - - tr := tar.NewReader(gz) - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - target := filepath.Join(destDir, header.Name) - - switch header.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(target, 0755); err != nil { - return err - } - case tar.TypeReg: - if err := writeFile(target, tr, header.FileInfo().Mode()); err != nil { - return err - } - } - } - return nil -} - -func extractZip(archivePath, destDir string) error { - r, err := zip.OpenReader(archivePath) - if err != nil { - return err - } - defer r.Close() - - for _, f := range r.File { - target := filepath.Join(destDir, f.Name) - - if f.FileInfo().IsDir() { - _ = os.MkdirAll(target, f.Mode()) - continue - } - - rc, err := f.Open() - if err != nil { - return err - } - - if err := writeFile(target, rc, f.Mode()); err != nil { - rc.Close() - return err - } - rc.Close() - } - return nil -} - -func writeFile(path string, r io.Reader, mode os.FileMode) error { - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return err - } - - out, err := os.Create(path) - if err != nil { - return err - } - defer out.Close() - - if _, err := io.Copy(out, r); err != nil { - return err - } - - return os.Chmod(path, mode) -} - -func replaceBinary(newBinary string) error { - if err := os.Chmod(newBinary, 0755); err != nil { - return err - } - - currentBinary, err := os.Executable() - if err != nil { - return err - } - currentBinary, err = filepath.EvalSymlinks(currentBinary) - if err != nil { - return err - } - - if err := os.Rename(newBinary, currentBinary); err != nil { - if runtime.GOOS != "windows" { - cmd := exec.Command("sudo", "mv", newBinary, currentBinary) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return output.Error("failed to replace binary: %w", err) - } - } else { - return output.Error("failed to replace binary: %w", err) - } - } - - return nil -} - -func githubAPI(method, endpoint string, body io.Reader) (*http.Response, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s%s", githubOwner, githubRepo, endpoint) - - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - - req.Header.Set("Accept", "application/vnd.github.v3+json") - if token := os.Getenv("GITHUB_TOKEN"); token != "" { - req.Header.Set("Authorization", "token "+token) - } - - return http.DefaultClient.Do(req) -} - -func handleAPIError(resp *http.Response) error { - body, _ := io.ReadAll(resp.Body) - return output.Error("API error %d: %s", resp.StatusCode, string(body)) -} diff --git a/core/autoupdate/autoupdate.go b/core/autoupdate/autoupdate.go new file mode 100644 index 0000000..b542418 --- /dev/null +++ b/core/autoupdate/autoupdate.go @@ -0,0 +1,111 @@ +package autoupdate + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/anyproto/anytype-cli/core/update" +) + +const ( + checkInterval = 24 * time.Hour + updateCheckFile = ".update-check" + updateLockFile = ".update-lock" +) + +type updateCheck struct { + LastCheck time.Time `json:"lastCheck"` + LastVersion string `json:"lastVersion"` +} + +func CheckAndUpdate() { + go func() { + if err := performUpdateCheck(); err != nil { + return + } + }() +} + +func performUpdateCheck() error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + anytypeDir := filepath.Join(home, ".anytype") + if err := os.MkdirAll(anytypeDir, 0755); err != nil { + return err + } + + lockPath := filepath.Join(anytypeDir, updateLockFile) + checkPath := filepath.Join(anytypeDir, updateCheckFile) + + lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) + if err != nil { + return nil + } + defer func() { + lockFile.Close() + os.Remove(lockPath) + }() + + if !shouldCheckForUpdate(checkPath) { + return nil + } + + latest, err := update.GetLatestVersion() + if err != nil { + return err + } + + current := update.GetCurrentVersion() + + check := updateCheck{ + LastCheck: time.Now(), + LastVersion: latest, + } + saveCheckInfo(checkPath, check) + + if !update.NeedsUpdate(current, latest) { + return nil + } + + if !update.CanUpdateBinary() { + return nil + } + + tempDir, err := os.MkdirTemp("", "anytype-autoupdate-*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + if err := update.DownloadAndInstall(latest); err != nil { + return err + } + + fmt.Printf("\n✓ Anytype CLI has been automatically updated from %s to %s\n\n", current, latest) + return nil +} + +func shouldCheckForUpdate(checkPath string) bool { + data, err := os.ReadFile(checkPath) + if err != nil { + return true + } + + var check updateCheck + if err := json.Unmarshal(data, &check); err != nil { + return true + } + + return time.Since(check.LastCheck) > checkInterval +} + +func saveCheckInfo(checkPath string, check updateCheck) { + data, _ := json.Marshal(check) + _ = os.WriteFile(checkPath, data, 0644) +} diff --git a/core/update/update.go b/core/update/update.go new file mode 100644 index 0000000..f71953d --- /dev/null +++ b/core/update/update.go @@ -0,0 +1,385 @@ +package update + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/anyproto/anytype-cli/core" +) + +const ( + GithubOwner = "anyproto" + GithubRepo = "anytype-cli" +) + +type Release struct { + Version string + URL string +} + +func GetLatestVersion() (string, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", GithubOwner, GithubRepo) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + req.Header.Set("Accept", "application/vnd.github.v3+json") + if token := os.Getenv("GITHUB_TOKEN"); token != "" { + req.Header.Set("Authorization", "token "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) + } + + var release struct { + TagName string `json:"tag_name"` + } + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return "", fmt.Errorf("failed to parse release: %w", err) + } + + return release.TagName, nil +} + +func NeedsUpdate(current, latest string) bool { + currentBase := current + if idx := strings.Index(current, "-"); idx != -1 { + currentBase = current[:idx] + } + return currentBase < latest +} + +func GetCurrentVersion() string { + return core.GetVersion() +} + +func DownloadAndInstall(version string) error { + tempDir, err := os.MkdirTemp("", "anytype-update-*") + if err != nil { + return fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tempDir) + + archivePath := filepath.Join(tempDir, getArchiveName(version)) + if err := downloadRelease(version, archivePath); err != nil { + return err + } + + if err := extractArchive(archivePath, tempDir); err != nil { + return fmt.Errorf("failed to extract: %w", err) + } + + binaryName := "anytype" + if runtime.GOOS == "windows" { + binaryName = "anytype.exe" + } + + newBinary := filepath.Join(tempDir, binaryName) + if _, err := os.Stat(newBinary); err != nil { + return fmt.Errorf("binary not found in archive (expected %s)", binaryName) + } + + if err := replaceBinary(newBinary); err != nil { + return fmt.Errorf("failed to install: %w", err) + } + + return nil +} + +func CanUpdateBinary() bool { + execPath, err := os.Executable() + if err != nil { + return false + } + + execPath, err = filepath.EvalSymlinks(execPath) + if err != nil { + return false + } + + file, err := os.OpenFile(execPath, os.O_WRONLY, 0) + if err != nil { + dir := filepath.Dir(execPath) + testFile := filepath.Join(dir, ".anytype-update-test") + if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { + return false + } + os.Remove(testFile) + return true + } + file.Close() + return true +} + +func getArchiveName(version string) string { + base := fmt.Sprintf("anytype-cli-%s-%s-%s", version, runtime.GOOS, runtime.GOARCH) + if runtime.GOOS == "windows" { + return base + ".zip" + } + return base + ".tar.gz" +} + +func downloadRelease(version, destination string) error { + archiveName := filepath.Base(destination) + + if token := os.Getenv("GITHUB_TOKEN"); token != "" { + return downloadViaAPI(version, archiveName, destination) + } + + url := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", + GithubOwner, GithubRepo, version, archiveName) + + return downloadFile(url, destination, "") +} + +func downloadViaAPI(version, filename, destination string) error { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", + GithubOwner, GithubRepo, version) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("Accept", "application/vnd.github.v3+json") + if token := os.Getenv("GITHUB_TOKEN"); token != "" { + req.Header.Set("Authorization", "token "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) + } + + var release struct { + Assets []struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"assets"` + } + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return fmt.Errorf("failed to parse release: %w", err) + } + + var assetURL string + for _, asset := range release.Assets { + if asset.Name == filename { + assetURL = asset.URL + break + } + } + if assetURL == "" { + return fmt.Errorf("release asset %s not found", filename) + } + + return downloadFile(assetURL, destination, os.Getenv("GITHUB_TOKEN")) +} + +func downloadFile(url, destination, token string) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + if token != "" { + req.Header.Set("Authorization", "token "+token) + req.Header.Set("Accept", "application/octet-stream") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed with status %d", resp.StatusCode) + } + + out, err := os.Create(destination) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +func extractArchive(archivePath, destDir string) error { + if strings.HasSuffix(archivePath, ".zip") { + return extractZip(archivePath, destDir) + } + return extractTarGz(archivePath, destDir) +} + +func extractTarGz(archivePath, destDir string) error { + file, err := os.Open(archivePath) + if err != nil { + return err + } + defer file.Close() + + gz, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gz.Close() + + tr := tar.NewReader(gz) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(destDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := writeFile(target, tr, header.FileInfo().Mode()); err != nil { + return err + } + } + } + return nil +} + +func extractZip(archivePath, destDir string) error { + r, err := zip.OpenReader(archivePath) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + target := filepath.Join(destDir, f.Name) + + if f.FileInfo().IsDir() { + _ = os.MkdirAll(target, f.Mode()) + continue + } + + rc, err := f.Open() + if err != nil { + return err + } + + if err := writeFile(target, rc, f.Mode()); err != nil { + rc.Close() + return err + } + rc.Close() + } + return nil +} + +func writeFile(path string, r io.Reader, mode os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, r); err != nil { + return err + } + + return os.Chmod(path, mode) +} + +func replaceBinary(newBinary string) error { + if err := os.Chmod(newBinary, 0755); err != nil { + return err + } + + currentBinary, err := os.Executable() + if err != nil { + return err + } + currentBinary, err = filepath.EvalSymlinks(currentBinary) + if err != nil { + return err + } + + if err := os.Rename(newBinary, currentBinary); err != nil { + if runtime.GOOS != "windows" { + cmd := exec.Command("sudo", "mv", newBinary, currentBinary) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to replace binary: %w", err) + } + } else { + return fmt.Errorf("failed to replace binary: %w", err) + } + } + + return nil +} + +func ReplaceBinaryNonInteractive(newBinary string) error { + if err := os.Chmod(newBinary, 0755); err != nil { + return err + } + + currentBinary, err := os.Executable() + if err != nil { + return err + } + currentBinary, err = filepath.EvalSymlinks(currentBinary) + if err != nil { + return err + } + + if err := os.Rename(newBinary, currentBinary); err != nil { + if runtime.GOOS != "windows" { + cmd := exec.Command("sudo", "-n", "mv", newBinary, currentBinary) + if err := cmd.Run(); err != nil { + return err + } + } else { + return err + } + } + + return nil +} diff --git a/main.go b/main.go index 0944425..5b35c4c 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,12 @@ package main import ( "github.com/anyproto/anytype-cli/cmd" "github.com/anyproto/anytype-cli/core" + "github.com/anyproto/anytype-cli/core/autoupdate" ) func main() { + autoupdate.CheckAndUpdate() + defer func() { core.CloseEventReceiver() core.CloseGRPCConnection() From ace97c641c554475ce95369c2b6dcde7cced1e77 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:04:11 +0100 Subject: [PATCH 2/9] Move RunE for update command --- cmd/update/update.go | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/cmd/update/update.go b/cmd/update/update.go index 79df88e..1a7a232 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -11,33 +11,31 @@ func NewUpdateCmd() *cobra.Command { Use: "update", Short: "Update to the latest version", Long: "Download and install the latest version of the Anytype CLI from GitHub releases.", - RunE: runUpdate, - } -} + RunE: func(cmd *cobra.Command, args []string) error { + output.Info("Checking for updates...") -func runUpdate(cmd *cobra.Command, args []string) error { - output.Info("Checking for updates...") + latest, err := update.GetLatestVersion() + if err != nil { + return output.Error("Failed to check latest version: %w", err) + } - latest, err := update.GetLatestVersion() - if err != nil { - return output.Error("Failed to check latest version: %w", err) - } + current := update.GetCurrentVersion() - current := update.GetCurrentVersion() + if !update.NeedsUpdate(current, latest) { + output.Info("Already up to date (%s)", current) + return nil + } - if !update.NeedsUpdate(current, latest) { - output.Info("Already up to date (%s)", current) - return nil - } + output.Info("Updating from %s to %s...", current, latest) - output.Info("Updating from %s to %s...", current, latest) + if err := update.DownloadAndInstall(latest); err != nil { + return output.Error("Update failed: %w", err) + } - if err := update.DownloadAndInstall(latest); err != nil { - return output.Error("update failed: %w", err) + output.Success("Successfully updated to %s", latest) + output.Info("If the service is installed, restart it with: anytype service restart") + output.Info("Otherwise, restart your terminal or run 'anytype' to use the new version") + return nil + }, } - - output.Success("Successfully updated to %s", latest) - output.Info("If the service is installed, restart it with: anytype service restart") - output.Info("Otherwise, restart your terminal or run 'anytype' to use the new version") - return nil } From 1f2b07c16be056bf63e3cc2404501df1ad83a975 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:05:21 +0100 Subject: [PATCH 3/9] Remove non-interactive binary replacement function --- core/update/update.go | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/core/update/update.go b/core/update/update.go index f71953d..96f4080 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -355,31 +355,3 @@ func replaceBinary(newBinary string) error { return nil } - -func ReplaceBinaryNonInteractive(newBinary string) error { - if err := os.Chmod(newBinary, 0755); err != nil { - return err - } - - currentBinary, err := os.Executable() - if err != nil { - return err - } - currentBinary, err = filepath.EvalSymlinks(currentBinary) - if err != nil { - return err - } - - if err := os.Rename(newBinary, currentBinary); err != nil { - if runtime.GOOS != "windows" { - cmd := exec.Command("sudo", "-n", "mv", newBinary, currentBinary) - if err := cmd.Run(); err != nil { - return err - } - } else { - return err - } - } - - return nil -} From 725bdf574a03652f7f9480d2c316bade6f33af23 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:54:17 +0100 Subject: [PATCH 4/9] Update code style guidelines for naming conventions and output handling --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6e349ed..f73135c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -220,5 +220,7 @@ func NewConfigCmd() *cobra.Command { ## Code Style Guidelines -### Naming Conventions -- **Use `Id` instead of `ID`**: Throughout the codebase, prefer `Id` over `ID` for consistency (e.g., `UserId`, `SpaceId`, `ObjectId`) \ No newline at end of file +- **Constants**: Centralize all configuration values, paths, URLs, and time intervals in `core/config/constants.go` +- **User Output**: Always use `core/output` package functions (`output.Success()`, `output.Error()`, `output.Warning()`, `output.Info()`) instead of `fmt.Printf/Println` +- **No Emojis**: Avoid emojis in output messages unless explicitly requested +- **Use `Id` instead of `ID`**: Throughout the codebase, prefer `Id` over `ID` for consistency (e.g., `UserId`, `SpaceId`, `ObjectId`) From 0bd20ea62a173bed5f0617a5ce9b14c098c48745 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:56:44 +0100 Subject: [PATCH 5/9] Enhance auto-update functionality with error logging and service restart --- cmd/service/service.go | 16 +++--- core/autoupdate/autoupdate.go | 100 +++++++++++++++++++++++++++------- core/config/constants.go | 31 ++++++++++- core/update/update.go | 74 +++++++++++++++++++------ go.mod | 1 + go.sum | 2 + 6 files changed, 180 insertions(+), 44 deletions(-) diff --git a/cmd/service/service.go b/cmd/service/service.go index 2f27cdd..4593dd6 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -12,8 +12,8 @@ import ( "github.com/anyproto/anytype-cli/core/serviceprogram" ) -// getService creates a service instance with our standard configuration -func getService() (service.Service, error) { +// GetService creates a service instance with our standard configuration +func GetService() (service.Service, error) { options := service.KeyValue{ "UserService": true, } @@ -81,7 +81,7 @@ func NewServiceCmd() *cobra.Command { } func installService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } @@ -102,7 +102,7 @@ func installService(cmd *cobra.Command, args []string) error { } func uninstallService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } @@ -117,7 +117,7 @@ func uninstallService(cmd *cobra.Command, args []string) error { } func startService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } @@ -140,7 +140,7 @@ func startService(cmd *cobra.Command, args []string) error { } func stopService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } @@ -163,7 +163,7 @@ func stopService(cmd *cobra.Command, args []string) error { } func restartService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } @@ -186,7 +186,7 @@ func restartService(cmd *cobra.Command, args []string) error { } func statusService(cmd *cobra.Command, args []string) error { - s, err := getService() + s, err := GetService() if err != nil { return output.Error("Failed to create service: %w", err) } diff --git a/core/autoupdate/autoupdate.go b/core/autoupdate/autoupdate.go index b542418..b54719b 100644 --- a/core/autoupdate/autoupdate.go +++ b/core/autoupdate/autoupdate.go @@ -2,18 +2,18 @@ package autoupdate import ( "encoding/json" + "errors" "fmt" + "log" "os" "path/filepath" "time" - "github.com/anyproto/anytype-cli/core/update" -) + "github.com/kardianos/service" -const ( - checkInterval = 24 * time.Hour - updateCheckFile = ".update-check" - updateLockFile = ".update-lock" + serviceCmd "github.com/anyproto/anytype-cli/cmd/service" + "github.com/anyproto/anytype-cli/core/config" + "github.com/anyproto/anytype-cli/core/update" ) type updateCheck struct { @@ -24,24 +24,41 @@ type updateCheck struct { func CheckAndUpdate() { go func() { if err := performUpdateCheck(); err != nil { - return + logUpdateError(err) } }() } +func logUpdateError(err error) { + logPath := config.GetUpdateLogFilePath() + logDir := filepath.Dir(logPath) + + if mkdirErr := os.MkdirAll(logDir, 0755); mkdirErr != nil { + return + } + + f, openErr := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if openErr != nil { + return + } + defer f.Close() + + logger := log.New(f, "", log.LstdFlags) + logger.Printf("Autoupdate error: %v\n", err) +} + func performUpdateCheck() error { - home, err := os.UserHomeDir() - if err != nil { - return err + configDir := config.GetConfigDir() + if configDir == "" { + return fmt.Errorf("could not determine config directory") } - anytypeDir := filepath.Join(home, ".anytype") - if err := os.MkdirAll(anytypeDir, 0755); err != nil { + if err := os.MkdirAll(configDir, 0755); err != nil { return err } - lockPath := filepath.Join(anytypeDir, updateLockFile) - checkPath := filepath.Join(anytypeDir, updateCheckFile) + lockPath := config.GetUpdateLockFilePath() + checkPath := config.GetUpdateCheckFilePath() lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) if err != nil { @@ -87,7 +104,44 @@ func performUpdateCheck() error { return err } - fmt.Printf("\n✓ Anytype CLI has been automatically updated from %s to %s\n\n", current, latest) + fmt.Printf("\n✓ Anytype CLI has been automatically updated from %s to %s\n", current, latest) + + // Check if service is running and restart it automatically + if err := restartServiceIfRunning(); err != nil { + fmt.Printf("⚠️ Failed to restart service: %v\n", err) + fmt.Println(" Restart manually with: anytype service restart") + } + fmt.Println() + + return nil +} + +// restartServiceIfRunning checks if the service is running and restarts it +func restartServiceIfRunning() error { + s, err := serviceCmd.GetService() + if err != nil { + return fmt.Errorf("failed to create service: %w", err) + } + + // Check status + status, err := s.Status() + if err != nil { + if errors.Is(err, service.ErrNotInstalled) { + // Service not installed, nothing to restart + return nil + } + return fmt.Errorf("failed to get service status: %w", err) + } + + // Only restart if running + if status == service.StatusRunning { + fmt.Println("Restarting service with new binary...") + if err := s.Restart(); err != nil { + return fmt.Errorf("failed to restart service: %w", err) + } + fmt.Println("Service restarted successfully") + } + return nil } @@ -102,10 +156,18 @@ func shouldCheckForUpdate(checkPath string) bool { return true } - return time.Since(check.LastCheck) > checkInterval + return time.Since(check.LastCheck) > config.UpdateCheckInterval } -func saveCheckInfo(checkPath string, check updateCheck) { - data, _ := json.Marshal(check) - _ = os.WriteFile(checkPath, data, 0644) +func saveCheckInfo(checkPath string, check updateCheck) error { + data, err := json.Marshal(check) + if err != nil { + return fmt.Errorf("failed to marshal check info: %w", err) + } + + if err := os.WriteFile(checkPath, data, 0644); err != nil { + return fmt.Errorf("failed to write check info: %w", err) + } + + return nil } diff --git a/core/config/constants.go b/core/config/constants.go index ee37111..7afe82f 100644 --- a/core/config/constants.go +++ b/core/config/constants.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "runtime" + "time" ) const ( @@ -23,8 +24,16 @@ const ( // URLs GRPCDNSAddress = "dns:///" + DefaultGRPCAddress + // GitHub repository + GitHubOwner = "anyproto" + GitHubRepo = "anytype-cli" + + // GitHub domains + GitHubAPIBaseURL = "https://api.github.com" + GitHubWebBaseURL = "https://github.com" + // External URLs - GitHubBaseURL = "https://github.com/anyproto/anytype-cli" + GitHubBaseURL = GitHubWebBaseURL + "/" + GitHubOwner + "/" + GitHubRepo GitHubCommitURL = GitHubBaseURL + "/commit/" GitHubReleaseURL = GitHubBaseURL + "/releases/tag/" @@ -37,6 +46,14 @@ const ( DataDirName = "data" LogsDirName = "logs" AnytypeName = "anytype" + + // Update related files + UpdateCheckFile = ".update-check" + UpdateLockFile = ".update-lock" + UpdateLogFile = "autoupdate.log" + + // Update check interval + UpdateCheckInterval = 24 * time.Hour ) func GetWorkDir() string { @@ -77,3 +94,15 @@ func GetDataDir() string { func GetLogsDir() string { return filepath.Join(GetConfigDir(), LogsDirName) } + +func GetUpdateCheckFilePath() string { + return filepath.Join(GetConfigDir(), UpdateCheckFile) +} + +func GetUpdateLockFilePath() string { + return filepath.Join(GetConfigDir(), UpdateLockFile) +} + +func GetUpdateLogFilePath() string { + return filepath.Join(GetLogsDir(), UpdateLogFile) +} diff --git a/core/update/update.go b/core/update/update.go index 96f4080..01342c4 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -13,14 +13,17 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/anyproto/anytype-cli/core" + "github.com/anyproto/anytype-cli/core/config" + "github.com/anyproto/anytype-cli/core/output" + "github.com/hashicorp/go-version" ) -const ( - GithubOwner = "anyproto" - GithubRepo = "anytype-cli" -) +var httpClient = &http.Client{ + Timeout: 5 * time.Minute, +} type Release struct { Version string @@ -28,7 +31,7 @@ type Release struct { } func GetLatestVersion() (string, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", GithubOwner, GithubRepo) + url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", config.GitHubAPIBaseURL, config.GitHubOwner, config.GitHubRepo) req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -40,7 +43,7 @@ func GetLatestVersion() (string, error) { req.Header.Set("Authorization", "token "+token) } - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return "", err } @@ -62,11 +65,17 @@ func GetLatestVersion() (string, error) { } func NeedsUpdate(current, latest string) bool { - currentBase := current - if idx := strings.Index(current, "-"); idx != -1 { - currentBase = current[:idx] + currentVer, err := version.NewVersion(current) + if err != nil { + return false } - return currentBase < latest + + latestVer, err := version.NewVersion(latest) + if err != nil { + return false + } + + return currentVer.LessThan(latestVer) } func GetCurrentVersion() string { @@ -146,15 +155,15 @@ func downloadRelease(version, destination string) error { return downloadViaAPI(version, archiveName, destination) } - url := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", - GithubOwner, GithubRepo, version, archiveName) + url := fmt.Sprintf("%s/%s/%s/releases/download/%s/%s", + config.GitHubWebBaseURL, config.GitHubOwner, config.GitHubRepo, version, archiveName) return downloadFile(url, destination, "") } func downloadViaAPI(version, filename, destination string) error { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", - GithubOwner, GithubRepo, version) + url := fmt.Sprintf("%s/repos/%s/%s/releases/tags/%s", + config.GitHubAPIBaseURL, config.GitHubOwner, config.GitHubRepo, version) req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -166,7 +175,7 @@ func downloadViaAPI(version, filename, destination string) error { req.Header.Set("Authorization", "token "+token) } - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return err } @@ -212,7 +221,7 @@ func downloadFile(url, destination, token string) error { req.Header.Set("Accept", "application/octet-stream") } - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return err } @@ -239,6 +248,21 @@ func extractArchive(archivePath, destDir string) error { return extractTarGz(archivePath, destDir) } +// isValidExtractionPath validates that the target path is within the destination directory to prevent path traversal attacks +func isValidExtractionPath(target, destDir string) bool { + cleanTarget := filepath.Clean(target) + cleanDest := filepath.Clean(destDir) + + // Check if target starts with destDir + rel, err := filepath.Rel(cleanDest, cleanTarget) + if err != nil { + return false + } + + // If the relative path starts with "..", it's trying to escape the destDir + return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".." +} + func extractTarGz(archivePath, destDir string) error { file, err := os.Open(archivePath) if err != nil { @@ -264,6 +288,10 @@ func extractTarGz(archivePath, destDir string) error { target := filepath.Join(destDir, header.Name) + if !isValidExtractionPath(target, destDir) { + return fmt.Errorf("illegal file path in archive: %s", header.Name) + } + switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(target, 0755); err != nil { @@ -288,6 +316,10 @@ func extractZip(archivePath, destDir string) error { for _, f := range r.File { target := filepath.Join(destDir, f.Name) + if !isValidExtractionPath(target, destDir) { + return fmt.Errorf("illegal file path in archive: %s", f.Name) + } + if f.FileInfo().IsDir() { _ = os.MkdirAll(target, f.Mode()) continue @@ -341,6 +373,16 @@ func replaceBinary(newBinary string) error { if err := os.Rename(newBinary, currentBinary); err != nil { if runtime.GOOS != "windows" { + output.Warning("Update requires elevated permissions to replace the binary") + output.Info("Binary location: %s", currentBinary) + fmt.Fprint(os.Stdout, "Continue with sudo? [y/N]: ") + + var response string + fmt.Scanln(&response) + if strings.ToLower(strings.TrimSpace(response)) != "y" { + return fmt.Errorf("update cancelled by user") + } + cmd := exec.Command("sudo", "mv", newBinary, currentBinary) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/go.mod b/go.mod index dcf78f8..ea48d75 100644 --- a/go.mod +++ b/go.mod @@ -123,6 +123,7 @@ require ( github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hbagdi/go-unsplash v0.0.0-20230414214043-474fc02c9119 // indirect diff --git a/go.sum b/go.sum index aea289a..c1bddf9 100644 --- a/go.sum +++ b/go.sum @@ -445,6 +445,8 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= From 27a2b672eb0b11a55562b6e80c4d5f6a1287e0b3 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:00:54 +0100 Subject: [PATCH 6/9] Refactor auto-update output handling and improve service restart messages --- core/autoupdate/autoupdate.go | 18 ++++++------------ core/update/update.go | 5 ----- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/core/autoupdate/autoupdate.go b/core/autoupdate/autoupdate.go index b54719b..6000bb2 100644 --- a/core/autoupdate/autoupdate.go +++ b/core/autoupdate/autoupdate.go @@ -13,6 +13,7 @@ import ( serviceCmd "github.com/anyproto/anytype-cli/cmd/service" "github.com/anyproto/anytype-cli/core/config" + "github.com/anyproto/anytype-cli/core/output" "github.com/anyproto/anytype-cli/core/update" ) @@ -94,24 +95,17 @@ func performUpdateCheck() error { return nil } - tempDir, err := os.MkdirTemp("", "anytype-autoupdate-*") - if err != nil { - return err - } - defer os.RemoveAll(tempDir) - if err := update.DownloadAndInstall(latest); err != nil { return err } - fmt.Printf("\n✓ Anytype CLI has been automatically updated from %s to %s\n", current, latest) + output.Success("Anytype CLI has been automatically updated from %s to %s", current, latest) // Check if service is running and restart it automatically if err := restartServiceIfRunning(); err != nil { - fmt.Printf("⚠️ Failed to restart service: %v\n", err) - fmt.Println(" Restart manually with: anytype service restart") + output.Warning("Failed to restart service: %v", err) + output.Info("Restart manually with: anytype service restart") } - fmt.Println() return nil } @@ -135,11 +129,11 @@ func restartServiceIfRunning() error { // Only restart if running if status == service.StatusRunning { - fmt.Println("Restarting service with new binary...") + output.Info("Restarting service with new binary...") if err := s.Restart(); err != nil { return fmt.Errorf("failed to restart service: %w", err) } - fmt.Println("Service restarted successfully") + output.Info("Service restarted successfully") } return nil diff --git a/core/update/update.go b/core/update/update.go index 01342c4..02bfdc9 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -25,11 +25,6 @@ var httpClient = &http.Client{ Timeout: 5 * time.Minute, } -type Release struct { - Version string - URL string -} - func GetLatestVersion() (string, error) { url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", config.GitHubAPIBaseURL, config.GitHubOwner, config.GitHubRepo) From 0729a7a83e636f49a34c93d39929f6c1058395a4 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:03:54 +0100 Subject: [PATCH 7/9] Move from autoupdate package to update --- core/{autoupdate => update}/autoupdate.go | 13 ++++++------- main.go | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) rename core/{autoupdate => update}/autoupdate.go (92%) diff --git a/core/autoupdate/autoupdate.go b/core/update/autoupdate.go similarity index 92% rename from core/autoupdate/autoupdate.go rename to core/update/autoupdate.go index 6000bb2..9c6ceaf 100644 --- a/core/autoupdate/autoupdate.go +++ b/core/update/autoupdate.go @@ -1,4 +1,4 @@ -package autoupdate +package update import ( "encoding/json" @@ -14,7 +14,6 @@ import ( serviceCmd "github.com/anyproto/anytype-cli/cmd/service" "github.com/anyproto/anytype-cli/core/config" "github.com/anyproto/anytype-cli/core/output" - "github.com/anyproto/anytype-cli/core/update" ) type updateCheck struct { @@ -74,12 +73,12 @@ func performUpdateCheck() error { return nil } - latest, err := update.GetLatestVersion() + latest, err := GetLatestVersion() if err != nil { return err } - current := update.GetCurrentVersion() + current := GetCurrentVersion() check := updateCheck{ LastCheck: time.Now(), @@ -87,15 +86,15 @@ func performUpdateCheck() error { } saveCheckInfo(checkPath, check) - if !update.NeedsUpdate(current, latest) { + if !NeedsUpdate(current, latest) { return nil } - if !update.CanUpdateBinary() { + if !CanUpdateBinary() { return nil } - if err := update.DownloadAndInstall(latest); err != nil { + if err := DownloadAndInstall(latest); err != nil { return err } diff --git a/main.go b/main.go index 5b35c4c..67f12d6 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,11 @@ package main import ( "github.com/anyproto/anytype-cli/cmd" "github.com/anyproto/anytype-cli/core" - "github.com/anyproto/anytype-cli/core/autoupdate" + "github.com/anyproto/anytype-cli/core/update" ) func main() { - autoupdate.CheckAndUpdate() + update.CheckAndUpdate() defer func() { core.CloseEventReceiver() From 671d5633437f2d1df2561c955a5cb72c1fc29d84 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:10:22 +0100 Subject: [PATCH 8/9] Handle errors in saveCheckInfo and Scanln functions --- core/update/autoupdate.go | 2 +- core/update/update.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/update/autoupdate.go b/core/update/autoupdate.go index 9c6ceaf..2bc70ad 100644 --- a/core/update/autoupdate.go +++ b/core/update/autoupdate.go @@ -84,7 +84,7 @@ func performUpdateCheck() error { LastCheck: time.Now(), LastVersion: latest, } - saveCheckInfo(checkPath, check) + _ = saveCheckInfo(checkPath, check) if !NeedsUpdate(current, latest) { return nil diff --git a/core/update/update.go b/core/update/update.go index 02bfdc9..4aee9f1 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -373,7 +373,7 @@ func replaceBinary(newBinary string) error { fmt.Fprint(os.Stdout, "Continue with sudo? [y/N]: ") var response string - fmt.Scanln(&response) + _, _ = fmt.Scanln(&response) if strings.ToLower(strings.TrimSpace(response)) != "y" { return fmt.Errorf("update cancelled by user") } From c063950895cb2419dd7396a246f5276381129e92 Mon Sep 17 00:00:00 2001 From: Jannis Metrikat <120120832+jmetrikat@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:25:47 +0100 Subject: [PATCH 9/9] Refactor update binary replacement logic --- core/update/update.go | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/core/update/update.go b/core/update/update.go index 4aee9f1..f058449 100644 --- a/core/update/update.go +++ b/core/update/update.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -17,7 +16,6 @@ import ( "github.com/anyproto/anytype-cli/core" "github.com/anyproto/anytype-cli/core/config" - "github.com/anyproto/anytype-cli/core/output" "github.com/hashicorp/go-version" ) @@ -121,17 +119,11 @@ func CanUpdateBinary() bool { return false } - file, err := os.OpenFile(execPath, os.O_WRONLY, 0) + file, err := os.OpenFile(execPath, os.O_RDWR, 0) if err != nil { - dir := filepath.Dir(execPath) - testFile := filepath.Join(dir, ".anytype-update-test") - if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { - return false - } - os.Remove(testFile) - return true + return false } - file.Close() + _ = file.Close() return true } @@ -367,27 +359,7 @@ func replaceBinary(newBinary string) error { } if err := os.Rename(newBinary, currentBinary); err != nil { - if runtime.GOOS != "windows" { - output.Warning("Update requires elevated permissions to replace the binary") - output.Info("Binary location: %s", currentBinary) - fmt.Fprint(os.Stdout, "Continue with sudo? [y/N]: ") - - var response string - _, _ = fmt.Scanln(&response) - if strings.ToLower(strings.TrimSpace(response)) != "y" { - return fmt.Errorf("update cancelled by user") - } - - cmd := exec.Command("sudo", "mv", newBinary, currentBinary) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to replace binary: %w", err) - } - } else { - return fmt.Errorf("failed to replace binary: %w", err) - } + return fmt.Errorf("failed to replace binary (insufficient permissions): %w", err) } return nil