From 1d0bab0778a738cdfbbbfc5858cf7823fb034841 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Thu, 8 May 2025 19:25:12 +0200 Subject: [PATCH 1/5] [AGENT-163] Update command for Windows --- internal/util/upgrade.go | 206 +++++++++++---------------------------- 1 file changed, 58 insertions(+), 148 deletions(-) diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index 78a0d2e7..c9dd4ced 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "internal/goarch" "io" "net/http" "os" @@ -95,18 +96,6 @@ func getBinaryName() string { return binaryName } -func isWindowsMsiInstallation() bool { - if runtime.GOOS != "windows" { - return false - } - exe, err := os.Executable() - if err != nil { - return false - } - return strings.Contains(strings.ToLower(exe), "\\program files\\") || - strings.Contains(strings.ToLower(exe), "\\program files (x86)\\") -} - func getReleaseAssetName() string { goos := runtime.GOOS arch := runtime.GOARCH @@ -128,18 +117,8 @@ func getReleaseAssetName() string { return fmt.Sprintf("agentuity_%s_%s.%s", strings.Title(goos), archName, extension) } -func getMsiInstallerName() string { - arch := runtime.GOARCH - var msiArch string - if arch == "amd64" { - msiArch = "x64" - } else if arch == "386" { - msiArch = "x86" - } else { - msiArch = arch - } - - return fmt.Sprintf("agentuity-%s.msi", msiArch) +func getWindowsExecutableZip() string { + return fmt.Sprintf("agentuity_Windows_%s.zip", goarch.GOARCH) } func isAdmin(ctx context.Context) bool { @@ -175,7 +154,7 @@ func UpgradeCLI(ctx context.Context, logger logger.Logger, force bool) error { } } - if isWindowsMsiInstallation() { + if runtime.GOOS == "windows" { release, err := GetLatestRelease(ctx) if err != nil { return fmt.Errorf("failed to get latest release: %w", err) @@ -186,7 +165,7 @@ func UpgradeCLI(ctx context.Context, logger logger.Logger, force bool) error { return nil } - return upgradeWithWindowsMsi(ctx, logger, release) + return upgradeWithWindows(ctx, logger, release) } release, err := GetLatestRelease(ctx) // Using public function from version.go @@ -208,8 +187,8 @@ func UpgradeCLI(ctx context.Context, logger logger.Logger, force bool) error { } defer os.RemoveAll(tempDir) - assetURL := fmt.Sprintf("https://github.com/agentuity/cli/releases/download/v%s/%s", release, assetName) - checksumURL := fmt.Sprintf("https://github.com/agentuity/cli/releases/download/v%s/%s", release, checksumFileName) + assetURL := fmt.Sprintf("https://agentuity.sh/release/cli/v%s/%s", release, assetName) + checksumURL := fmt.Sprintf("https://agentuity.sh/release/cli/v%s/%s", release, checksumFileName) assetPath := filepath.Join(tempDir, assetName) checksumPath := filepath.Join(tempDir, checksumFileName) @@ -640,154 +619,85 @@ func upgradeWithHomebrew(ctx context.Context, logger logger.Logger) error { return nil } -func upgradeWithWindowsMsi(ctx context.Context, logger logger.Logger, version string) error { - if os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" || os.Getenv("NONINTERACTIVE") != "" { - tui.ShowWarning("Non-interactive environment detected, skipping automatic MSI installation") +func upgradeWithWindows(ctx context.Context, logger logger.Logger, version string) error { + tui.ShowWarning("Non-interactive environment detected, skipping automatic MSI installation") - tempDir, err := os.MkdirTemp("", "agentuity-upgrade-msi") - if err != nil { - return fmt.Errorf("failed to create temp directory: %w", err) - } - defer os.RemoveAll(tempDir) - - installerName := getMsiInstallerName() - if strings.HasPrefix(version, "v") { - version = strings.TrimPrefix(version, "v") - } - installerURL := fmt.Sprintf("https://github.com/agentuity/cli/releases/download/v%s/%s", version, installerName) - installerPath := filepath.Join(tempDir, installerName) - - var downloadErr error - downloadAction := func() { - if err := downloadFile(installerURL, installerPath); err != nil { - downloadErr = fmt.Errorf("failed to download MSI installer: %w", err) - } - } - tui.ShowSpinner("Downloading MSI installer...", downloadAction) - if downloadErr != nil { - return downloadErr - } - - homePath := os.Getenv("HOME") - if homePath == "" { - homePath = os.Getenv("USERPROFILE") - } - if homePath == "" { - return fmt.Errorf("unable to determine home directory") - } - - destPath := filepath.Join(homePath, installerName) - if err := copyFile(installerPath, destPath); err != nil { - return fmt.Errorf("failed to copy MSI to %s: %w", destPath, err) - } - - tui.ShowSuccess("MSI installer copied to %s", destPath) - tui.ShowWarning("To install manually, run the MSI installer at: %s", destPath) - return nil - } - - if !isAdmin(ctx) { - tui.ShowWarning("Administrator privileges required to upgrade the CLI on Windows") - tui.ShowWarning("Please restart the CLI with administrator privileges and try again") - return fmt.Errorf("administrator privileges required") - } - - tempDir, err := os.MkdirTemp("", "agentuity-upgrade-msi") + tempDir, err := os.MkdirTemp("", "agentuity-upgrade") if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tempDir) - var uninstallErr error - uninstallAction := func() { - uninstallScriptPath := filepath.Join(tempDir, "uninstall.ps1") - uninstallScript := ` -$products = Get-CimInstance -Class Win32_Product | Where-Object { $_.Name -like "*Agentuity*" } -if ($products) { - foreach ($product in $products) { - $result = $product | Invoke-CimMethod -MethodName Uninstall - if ($result.ReturnValue -ne 0) { - throw "Failed to uninstall $($product.Name) with return code $($result.ReturnValue)" - } - } -} -` - if err := os.WriteFile(uninstallScriptPath, []byte(uninstallScript), 0644); err != nil { - uninstallErr = fmt.Errorf("failed to create uninstall script: %w", err) - return - } - - uninstallCmd := exec.CommandContext(ctx, "powershell", "-ExecutionPolicy", "Bypass", "-File", uninstallScriptPath) - output, err := uninstallCmd.CombinedOutput() - if err != nil { - uninstallErr = fmt.Errorf("failed to run uninstall script: %w, output: %s", err, string(output)) - return - } - logger.Trace(strings.TrimSpace(string(output))) - tui.ShowSuccess("Uninstalled existing installation") - } - tui.ShowSpinner("Checking for existing installations...", uninstallAction) - if uninstallErr != nil { - tui.ShowWarning("Uninstall failed, continuing with installation: %v", uninstallErr) - } - - installerName := getMsiInstallerName() + zipFileName := getWindowsExecutableZip() if strings.HasPrefix(version, "v") { version = strings.TrimPrefix(version, "v") } - installerURL := fmt.Sprintf("https://github.com/agentuity/cli/releases/download/v%s/%s", version, installerName) - installerPath := filepath.Join(tempDir, installerName) + + zipFileURL := fmt.Sprintf("https://agentuity.sh/release/cli/v%s/%s", version, zipFileName) + zipFilePath := filepath.Join(tempDir, zipFileName) var downloadErr error downloadAction := func() { - if err := downloadFile(installerURL, installerPath); err != nil { - downloadErr = fmt.Errorf("failed to download MSI installer: %w", err) + if err := downloadFile(zipFileURL, zipFilePath); err != nil { + downloadErr = fmt.Errorf("failed to download executable: %w", err) } } - tui.ShowSpinner("Downloading MSI installer...", downloadAction) + tui.ShowSpinner("Downloading Executable installer...", downloadAction) if downloadErr != nil { return downloadErr } - var installErr error - installAction := func() { - installCmd := exec.CommandContext(ctx, "msiexec", "/i", installerPath, "/qn", "/norestart") - if err := installCmd.Run(); err != nil { - tui.ShowWarning("Direct install failed, trying with update approach: %v", err) + // unzip the file without action + zipReader, err := zip.OpenReader(zipFilePath) + if err != nil { + return fmt.Errorf("failed to open zip file: %w", err) + } + defer zipReader.Close() - updateCmd := exec.CommandContext(ctx, "msiexec", "/update", installerPath, "/qn") - if err := updateCmd.Run(); err != nil { - tui.ShowWarning("Update approach failed, trying install with reinstall: %v", err) + var tmpExecutablePath string + for _, file := range zipReader.File { + if file.Name == getBinaryName() { + err := func() error { + f, err := file.Open() + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() - reinstallCmd := exec.CommandContext(ctx, "msiexec", "/i", installerPath, "/qn", "REINSTALLMODE=amus", "REINSTALL=ALL") - if err := reinstallCmd.Run(); err != nil { - installErr = fmt.Errorf("failed to run MSI installer: %w", err) + tmpExecutablePath = filepath.Join(tempDir, file.Name) + destFile, err := os.Create(tmpExecutablePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) } + defer destFile.Close() + + _, err = io.Copy(destFile, f) + return err + }() + if err != nil { + return err } + break } } - tui.ShowSpinner("Installing upgrade...", installAction) - if installErr != nil { - tui.ShowWarning("Automatic installation failed: %v", installErr) - homePath := os.Getenv("HOME") - if homePath == "" { - homePath = os.Getenv("USERPROFILE") - } - if homePath == "" { - return fmt.Errorf("unable to determine home directory: %w", installErr) - } + if tmpExecutablePath == "" { + return fmt.Errorf("agentuity.exe not found in zip file") + } - destPath := filepath.Join(homePath, installerName) - if err := copyFile(installerPath, destPath); err != nil { - return fmt.Errorf("failed to copy MSI to %s: %w", destPath, err) - } + homePath := os.Getenv("HOME") + if homePath == "" { + homePath = os.Getenv("USERPROFILE") + } + if homePath == "" { + return fmt.Errorf("unable to determine home directory") + } - tui.ShowSuccess("MSI installer copied to %s", destPath) - tui.ShowWarning("To install manually, run the MSI installer at: %s", destPath) - return nil + destPath := filepath.Join(homePath, getBinaryName()) + if err := copyFile(tmpExecutablePath, destPath); err != nil { + return fmt.Errorf("failed to copy executable to %s: %w", destPath, err) } - tui.ShowSuccess("Successfully upgraded to version %s", version) + tui.ShowSuccess("Executable copied to %s", destPath) return nil } From cd9c7ce9f83675ea1cdd73671dbff114aebecf30 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Thu, 8 May 2025 19:27:59 +0200 Subject: [PATCH 2/5] wrong package --- internal/util/upgrade.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index c9dd4ced..025624ad 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "internal/goarch" "io" "net/http" "os" @@ -118,7 +117,7 @@ func getReleaseAssetName() string { } func getWindowsExecutableZip() string { - return fmt.Sprintf("agentuity_Windows_%s.zip", goarch.GOARCH) + return fmt.Sprintf("agentuity_Windows_%s.zip", runtime.GOARCH) } func isAdmin(ctx context.Context) bool { From a3a337747bbe3de84d8c4a5a720a8b97ca5b6fc0 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Thu, 8 May 2025 19:35:26 +0200 Subject: [PATCH 3/5] Update to correct architectures --- internal/util/upgrade.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index 025624ad..a230af59 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -117,7 +117,18 @@ func getReleaseAssetName() string { } func getWindowsExecutableZip() string { - return fmt.Sprintf("agentuity_Windows_%s.zip", runtime.GOARCH) + arch := runtime.GOARCH + + var msiArch string + if arch == "amd64" { + msiArch = "x86_64" + } else if arch == "386" { + msiArch = "i386" + } else { + msiArch = arch + } + + return fmt.Sprintf("agentuity_Windows_%s.zip", msiArch) } func isAdmin(ctx context.Context) bool { From 7f2ddb401143dd7955311856805ed146fbd1f93a Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Thu, 8 May 2025 19:44:05 +0200 Subject: [PATCH 4/5] unused code --- internal/util/upgrade.go | 126 --------------------------------------- 1 file changed, 126 deletions(-) diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index a230af59..0ca843a4 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -116,35 +116,6 @@ func getReleaseAssetName() string { return fmt.Sprintf("agentuity_%s_%s.%s", strings.Title(goos), archName, extension) } -func getWindowsExecutableZip() string { - arch := runtime.GOARCH - - var msiArch string - if arch == "amd64" { - msiArch = "x86_64" - } else if arch == "386" { - msiArch = "i386" - } else { - msiArch = arch - } - - return fmt.Sprintf("agentuity_Windows_%s.zip", msiArch) -} - -func isAdmin(ctx context.Context) bool { - if runtime.GOOS != "windows" { - return false - } - - cmd := exec.CommandContext(ctx, "powershell", "-Command", "([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)") - output, err := cmd.Output() - if err != nil { - return false - } - - return strings.TrimSpace(string(output)) == "True" -} - func UpgradeCLI(ctx context.Context, logger logger.Logger, force bool) error { if runtime.GOOS == "darwin" { exe, err := os.Executable() @@ -164,20 +135,6 @@ func UpgradeCLI(ctx context.Context, logger logger.Logger, force bool) error { } } - if runtime.GOOS == "windows" { - release, err := GetLatestRelease(ctx) - if err != nil { - return fmt.Errorf("failed to get latest release: %w", err) - } - - if Version == release && !force { - tui.ShowSuccess("You are already on the latest version (%s)", release) - return nil - } - - return upgradeWithWindows(ctx, logger, release) - } - release, err := GetLatestRelease(ctx) // Using public function from version.go if err != nil { return fmt.Errorf("failed to get latest release: %w", err) @@ -628,86 +585,3 @@ func upgradeWithHomebrew(ctx context.Context, logger logger.Logger) error { tui.ShowSuccess("Successfully upgraded to version %s via Homebrew", newVersion) return nil } - -func upgradeWithWindows(ctx context.Context, logger logger.Logger, version string) error { - tui.ShowWarning("Non-interactive environment detected, skipping automatic MSI installation") - - tempDir, err := os.MkdirTemp("", "agentuity-upgrade") - if err != nil { - return fmt.Errorf("failed to create temp directory: %w", err) - } - defer os.RemoveAll(tempDir) - - zipFileName := getWindowsExecutableZip() - if strings.HasPrefix(version, "v") { - version = strings.TrimPrefix(version, "v") - } - - zipFileURL := fmt.Sprintf("https://agentuity.sh/release/cli/v%s/%s", version, zipFileName) - zipFilePath := filepath.Join(tempDir, zipFileName) - - var downloadErr error - downloadAction := func() { - if err := downloadFile(zipFileURL, zipFilePath); err != nil { - downloadErr = fmt.Errorf("failed to download executable: %w", err) - } - } - tui.ShowSpinner("Downloading Executable installer...", downloadAction) - if downloadErr != nil { - return downloadErr - } - - // unzip the file without action - zipReader, err := zip.OpenReader(zipFilePath) - if err != nil { - return fmt.Errorf("failed to open zip file: %w", err) - } - defer zipReader.Close() - - var tmpExecutablePath string - for _, file := range zipReader.File { - if file.Name == getBinaryName() { - err := func() error { - f, err := file.Open() - if err != nil { - return fmt.Errorf("failed to open file: %w", err) - } - defer f.Close() - - tmpExecutablePath = filepath.Join(tempDir, file.Name) - destFile, err := os.Create(tmpExecutablePath) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } - defer destFile.Close() - - _, err = io.Copy(destFile, f) - return err - }() - if err != nil { - return err - } - break - } - } - - if tmpExecutablePath == "" { - return fmt.Errorf("agentuity.exe not found in zip file") - } - - homePath := os.Getenv("HOME") - if homePath == "" { - homePath = os.Getenv("USERPROFILE") - } - if homePath == "" { - return fmt.Errorf("unable to determine home directory") - } - - destPath := filepath.Join(homePath, getBinaryName()) - if err := copyFile(tmpExecutablePath, destPath); err != nil { - return fmt.Errorf("failed to copy executable to %s: %w", destPath, err) - } - - tui.ShowSuccess("Executable copied to %s", destPath) - return nil -} From 0bb6af9919becbdb445543a6248f9fa87a3eb53c Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Thu, 8 May 2025 19:55:27 +0200 Subject: [PATCH 5/5] schedule update on busy error --- internal/util/upgrade.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/util/upgrade.go b/internal/util/upgrade.go index 0ca843a4..b5ac91cc 100644 --- a/internal/util/upgrade.go +++ b/internal/util/upgrade.go @@ -260,7 +260,7 @@ func replaceBinary(ctx context.Context, logger logger.Logger, assetPath, version fileMode := info.Mode() if err := checkWritePermission(currentExe); err != nil { - if strings.Contains(err.Error(), "binary is currently running") { + if strings.Contains(err.Error(), "binary is currently running") || strings.Contains(err.Error(), "is being used by another process") { var updateErr error updateAction := func() { updateErr = updateRunningBinary(currentExe, binaryPath, fileMode) @@ -272,6 +272,7 @@ func replaceBinary(ctx context.Context, logger logger.Logger, assetPath, version tui.ShowSuccess("Successfully scheduled update to %s. The update will complete when this process exits.", version) return nil } + return fmt.Errorf("insufficient permissions to update binary: %w", err) }