//go:build windows package pkgmanager import ( "fmt" "os" "os/exec" "path/filepath" "strings" "golang.org/x/sys/windows/registry" ) func listWindowsPackages() ([]PackageInfo, error) { seen := make(map[string]PackageInfo) if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil { return nil, fmt.Errorf("enumerate current user uninstall failed: %w", err) } if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil { // non-fatal; 32-bit view may not exist _ = err } if err := enumerateRegistryUninstall(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil { _ = err } if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil { if wingetPkgs, err := listWingetPackages(); err == nil { for _, p := range wingetPkgs { key := p.Name + "@" + p.Manager if _, ok := seen[key]; !ok { seen[key] = p } } } } packages := make([]PackageInfo, 0, len(seen)) for _, p := range seen { packages = append(packages, p) } return packages, nil } func enumerateRegistryUninstall(root registry.Key, path string, seen map[string]PackageInfo) error { k, err := registry.OpenKey(root, path, registry.READ) if err != nil { return err } defer k.Close() subKeys, err := k.ReadSubKeyNames(0) if err != nil { return err } for _, name := range subKeys { sk, err := registry.OpenKey(k, name, registry.READ) if err != nil { continue } displayName, _, err := sk.GetStringValue("DisplayName") if err != nil || displayName == "" { sk.Close() continue } displayVersion, _, _ := sk.GetStringValue("DisplayVersion") version := strings.TrimSpace(displayVersion) if version == "" { version = "unknown" } key := displayName + "@windows" if _, ok := seen[key]; !ok { seen[key] = PackageInfo{ Name: displayName, Version: version, Manager: "windows", } } sk.Close() } return nil } func BootstrapManagers() error { var errs []string if !lookupWinget() { if err := installWingetViaAppInstaller(); err != nil { errs = append(errs, "winget: "+err.Error()) } } if _, err := exec.LookPath("choco"); err != nil { if err := installChocolatey(); err != nil { errs = append(errs, "chocolatey: "+err.Error()) } } if len(errs) > 0 { return fmt.Errorf("bootstrap failed: %s", strings.Join(errs, "; ")) } return nil } func installChocolatey() error { // Official Chocolatey install script (requires admin; agent runs as LocalSystem). cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Set-ExecutionPolicy Bypass -Scope Process -Force; "+ "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; "+ "iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("chocolatey install script failed: %w\nOutput: %s", err, string(out)) } return nil } func lookupWinget() bool { if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil { return true } appInstaller := `Microsoft.DesktopAppInstaller_8wekyb3d8bbwe` appxReg, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore`, registry.READ|registry.ENUMERATE_SUB_KEYS) if err == nil { defer appxReg.Close() if keys, err := appxReg.ReadSubKeyNames(0); err == nil { for _, keyName := range keys { if strings.Contains(keyName, "DesktopAppInstaller") { _ = appInstaller return true } } } } return false } func installWingetViaAppInstaller() error { // Microsoft App Installer download link (redirects to latest winget bundle) installerURL := "https://aka.ms/getwinget" appInstallerPath := filepath.Join(os.TempDir(), "DesktopAppInstaller.msixbundle") // Download App Installer bundle downloadCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; "+ "Invoke-WebRequest -Uri '"+installerURL+"' -OutFile '"+appInstallerPath+"' -UseBasicParsing") if out, err := downloadCmd.CombinedOutput(); err != nil { return fmt.Errorf("download App Installer failed: %w\nOutput: %s", err, string(out)) } // Install bundle (dependencies may need to be allowed; add-AppxPackage on msixbundle handles it) installCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", "Add-AppxPackage -Path '"+appInstallerPath+"' -ForceUpdateFromAnyVersion") if out, err := installCmd.CombinedOutput(); err != nil { return fmt.Errorf("install App Installer failed: %w\nOutput: %s", err, string(out)) } return nil }