feat(upgrade): cli $0 update work well!

This commit is contained in:
sky5454 2026-03-31 04:27:33 +08:00
parent ffd430285b
commit 0b4c26c1db

View file

@ -27,51 +27,70 @@ import (
// release of the current project is used. platform/arch can be used to // release of the current project is used. platform/arch can be used to
// select the correct asset (e.g. "linux", "amd64"). // select the correct asset (e.g. "linux", "amd64").
func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error) { func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error) {
assetURL, err := findAssetURL(releaseURL, platform, arch) assetURL, err := findAssetURL(releaseURL, platform, arch)
if err != nil { if err != nil {
return "", err return "", err
} }
// Download asset to temp file // Download asset to temp file. Use the asset URL extension so
tmpFile, err := os.CreateTemp("", "picoclaw-release-*.archive") // extractArchive can detect the archive format (zip/tar.gz/tar).
if err != nil { tmpPattern := "picoclaw-release-*"
return "", err if u, perr := url.Parse(assetURL); perr == nil {
} base := filepath.Base(u.Path)
tmpPath := tmpFile.Name() lbase := strings.ToLower(base)
defer func() { _ = tmpFile.Close() }() switch {
case strings.HasSuffix(lbase, ".zip"):
tmpPattern += ".zip"
case strings.HasSuffix(lbase, ".tar.gz") || strings.HasSuffix(lbase, ".tgz"):
tmpPattern += ".tar.gz"
case strings.HasSuffix(lbase, ".tar"):
tmpPattern += ".tar"
default:
tmpPattern += ".archive"
}
} else {
tmpPattern += ".archive"
}
resp, err := http.Get(assetURL) tmpFile, err := os.CreateTemp("", tmpPattern)
if err != nil { if err != nil {
os.Remove(tmpPath) return "", err
return "", err }
} tmpPath := tmpFile.Name()
defer resp.Body.Close() defer func() { _ = tmpFile.Close() }()
if resp.StatusCode != http.StatusOK {
os.Remove(tmpPath)
return "", fmt.Errorf("failed to download asset: status %d", resp.StatusCode)
}
if _, err := io.Copy(tmpFile, resp.Body); err != nil { resp, err := http.Get(assetURL)
os.Remove(tmpPath) if err != nil {
return "", err os.Remove(tmpPath)
} return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
os.Remove(tmpPath)
return "", fmt.Errorf("failed to download asset: status %d", resp.StatusCode)
}
// Extract if _, err := io.Copy(tmpFile, resp.Body); err != nil {
destDir, err := os.MkdirTemp("", "picoclaw-extract-*") os.Remove(tmpPath)
if err != nil { return "", err
os.Remove(tmpPath) }
return "", err
}
if err := extractArchive(tmpPath, destDir); err != nil { // Extract
os.Remove(tmpPath) destDir, err := os.MkdirTemp("", "picoclaw-extract-*")
os.RemoveAll(destDir) if err != nil {
return "", err os.Remove(tmpPath)
} return "", err
}
// cleanup archive file; keep extracted contents if err := extractArchive(tmpPath, destDir); err != nil {
_ = os.Remove(tmpPath) os.Remove(tmpPath)
return destDir, nil os.RemoveAll(destDir)
return "", err
}
// cleanup archive file; keep extracted contents
_ = os.Remove(tmpPath)
return destDir, nil
} }
// UpdateSelfFromRelease downloads the release matching the given parameters, // UpdateSelfFromRelease downloads the release matching the given parameters,
@ -80,352 +99,473 @@ func DownloadAndExtractRelease(releaseURL, platform, arch string) (string, error
// If releaseURL is empty, the latest release is used. If platform or arch // If releaseURL is empty, the latest release is used. If platform or arch
// is empty, runtime values are used. // is empty, runtime values are used.
func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error { func UpdateSelfFromRelease(releaseURL, platform, arch, programName string) error {
if platform == "" { if platform == "" {
platform = runtime.GOOS platform = runtime.GOOS
} }
if arch == "" { if arch == "" {
arch = runtime.GOARCH arch = runtime.GOARCH
} }
dir, err := DownloadAndExtractRelease(releaseURL, platform, arch) dir, err := DownloadAndExtractRelease(releaseURL, platform, arch)
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
binPath, err := findBinaryInDir(dir, programName) binPath, err := findBinaryInDir(dir, programName)
if err != nil { if err != nil {
return err return err
} }
// ensure executable bit on non-windows // ensure executable bit on non-windows
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
_ = os.Chmod(binPath, 0755) _ = os.Chmod(binPath, 0755)
} }
f, err := os.Open(binPath) f, err := os.Open(binPath)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
if err := selfupdate.Apply(f, selfupdate.Options{}); err != nil { if err := selfupdate.Apply(f, selfupdate.Options{}); err != nil {
return fmt.Errorf("apply update: %w", err) return fmt.Errorf("apply update: %w", err)
} }
return nil return nil
} }
// UpdateSelf updates the running executable by fetching the latest release // UpdateSelf updates the running executable by fetching the latest release
// and applying the binary matching programName. // and applying the binary matching programName.
func UpdateSelf(programName string) error { func UpdateSelf(programName string) error {
// Use production repo by default. // By default, let findAssetURL select the nightly build when no explicit
return UpdateSelfFromRelease(GetProdReleaseAPIURL(), runtime.GOOS, runtime.GOARCH, programName) // release URL is provided. Passing an empty releaseURL triggers that path.
return UpdateSelfFromRelease("", runtime.GOOS, runtime.GOARCH, programName)
} }
// GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner. // GetReleaseAPIURL returns the GitHub Releases API URL for the given repo owner.
// Example: owner="sky5454" -> https://api.github.com/repos/sky5454/picoclaw/releases/latest // Example: owner="sky5454" -> https://api.github.com/repos/sky5454/picoclaw/releases/latest
func GetReleaseAPIURL(owner string) string { func GetReleaseAPIURL(owner string) string {
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/latest", owner) return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/latest", owner)
} }
// GetProdReleaseAPIURL returns the production release API URL (upstream). // GetProdReleaseAPIURL returns the production release API URL (upstream).
func GetProdReleaseAPIURL() string { func GetProdReleaseAPIURL() string {
return GetReleaseAPIURL("sipeed") return GetReleaseAPIURL("sipeed")
}
// GetReleaseTagAPIURL returns the GitHub Releases API URL for a specific tag.
// Example: owner="sipeed", tag="nightly" -> https://api.github.com/repos/sipeed/picoclaw/releases/tags/nightly
func GetReleaseTagAPIURL(owner, tag string) string {
return fmt.Sprintf("https://api.github.com/repos/%s/picoclaw/releases/tags/%s", owner, tag)
}
// GetNightlyReleaseAPIURL returns the nightly release API URL for the production repo.
func GetNightlyReleaseAPIURL() string {
return GetReleaseTagAPIURL("sipeed", "nightly")
} }
// findAssetURL resolves the appropriate asset URL for the given release // findAssetURL resolves the appropriate asset URL for the given release
// selector. It accepts direct archive URLs as well as GitHub release URLs // selector. It accepts direct archive URLs as well as GitHub release URLs
// or empty (latest release for the project). // or empty (latest release for the project).
func findAssetURL(releaseURL, platform, arch string) (string, error) { func findAssetURL(releaseURL, platform, arch string) (string, error) {
if looksLikeDirectAssetURL(releaseURL) { if looksLikeDirectAssetURL(releaseURL) {
return releaseURL, nil return releaseURL, nil
} }
apiURL := buildReleaseAPIURL(releaseURL) apiURL := buildReleaseAPIURL(releaseURL)
if apiURL == "" { if apiURL == "" {
// Default to production repo API URL when no explicit release URL is provided. // If caller provided an empty releaseURL, default to the nightly tag
apiURL = GetProdReleaseAPIURL() // from the production repo. Otherwise fall back to the production
} // latest release API URL.
if strings.TrimSpace(releaseURL) == "" {
apiURL = GetNightlyReleaseAPIURL()
} else {
apiURL = GetProdReleaseAPIURL()
}
}
resp, err := http.Get(apiURL) resp, err := http.Get(apiURL)
if err != nil { if err != nil {
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to query releases: status %d", resp.StatusCode) return "", fmt.Errorf("failed to query releases: status %d", resp.StatusCode)
} }
var data struct { var data struct {
TagName string `json:"tag_name"` TagName string `json:"tag_name"`
Assets []struct { Assets []struct {
Name string `json:"name"` Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"` BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"` } `json:"assets"`
} }
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", err return "", err
} }
// prefer exact platform+arch match // Selection order: platform -> arch -> extension.
var fallback string platformLower := strings.ToLower(platform)
for _, a := range data.Assets { archLower := strings.ToLower(arch)
n := strings.ToLower(a.Name)
if platform != "" && arch != "" {
if strings.Contains(n, strings.ToLower(platform)) && strings.Contains(n, strings.ToLower(arch)) {
return a.BrowserDownloadURL, nil
}
}
if platform != "" && strings.Contains(n, strings.ToLower(platform)) {
if fallback == "" {
fallback = a.BrowserDownloadURL
}
}
if fallback == "" {
fallback = a.BrowserDownloadURL
}
}
if fallback != "" { isZip := func(name string) bool {
return fallback, nil return strings.HasSuffix(name, ".zip")
} }
return "", errors.New("no suitable release asset found") isTarGz := func(name string) bool {
return strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, ".tgz")
}
isTar := func(name string) bool { return strings.HasSuffix(name, ".tar") }
// collect indices of assets that contain platform (if provided)
var platformIdx []int
for i, a := range data.Assets {
n := strings.ToLower(a.Name)
if platform == "" || strings.Contains(n, platformLower) {
platformIdx = append(platformIdx, i)
}
}
pickBest := func(idxs []int) (string, bool) {
if len(idxs) == 0 {
return "", false
}
// prefer arch matches within idxs
var archIdx []int
if arch != "" {
aliases := archAliases(archLower)
for _, i := range idxs {
n := strings.ToLower(data.Assets[i].Name)
for _, ali := range aliases {
if strings.Contains(n, ali) {
archIdx = append(archIdx, i)
break
}
}
}
}
candidates := archIdx
if len(candidates) == 0 {
candidates = idxs
}
// extension preference
if platformLower == "windows" {
// prefer .zip only
for _, i := range candidates {
if isZip(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
// if no zip found, fallthrough to first candidate
return data.Assets[candidates[0]].BrowserDownloadURL, true
}
// non-windows: prefer tar.gz/tgz, then tar, then zip
for _, i := range candidates {
if isTarGz(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
for _, i := range candidates {
if isTar(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
for _, i := range candidates {
if isZip(strings.ToLower(data.Assets[i].Name)) {
return data.Assets[i].BrowserDownloadURL, true
}
}
// fallback to first candidate
return data.Assets[candidates[0]].BrowserDownloadURL, true
}
// Try platform matches first
if url, ok := pickBest(platformIdx); ok {
return url, nil
}
// If no platform matches, try arch-only matches
if arch != "" {
var archOnlyIdx []int
for i, a := range data.Assets {
n := strings.ToLower(a.Name)
if strings.Contains(n, archLower) {
archOnlyIdx = append(archOnlyIdx, i)
}
}
if url, ok := pickBest(archOnlyIdx); ok {
return url, nil
}
}
// Fallback to first asset
if len(data.Assets) > 0 {
return data.Assets[0].BrowserDownloadURL, nil
}
return "", errors.New("no suitable release asset found")
} }
func looksLikeDirectAssetURL(u string) bool { func looksLikeDirectAssetURL(u string) bool {
if u == "" { if u == "" {
return false return false
} }
lower := strings.ToLower(u) lower := strings.ToLower(u)
if strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".tar") { if strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") || strings.HasSuffix(lower, ".tar") {
return true return true
} }
if strings.Contains(lower, "/releases/download/") { if strings.Contains(lower, "/releases/download/") {
return true return true
} }
return false return false
} }
func buildReleaseAPIURL(releaseURL string) string { func buildReleaseAPIURL(releaseURL string) string {
if releaseURL == "" { if releaseURL == "" {
return "" return ""
} }
if strings.Contains(releaseURL, "api.github.com") { if strings.Contains(releaseURL, "api.github.com") {
return releaseURL return releaseURL
} }
u, err := url.Parse(releaseURL) u, err := url.Parse(releaseURL)
if err != nil { if err != nil {
return "" return ""
} }
if u.Host != "github.com" { if u.Host != "github.com" {
return "" return ""
} }
parts := strings.Split(strings.Trim(u.Path, "/"), "/") parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) < 2 { if len(parts) < 2 {
return "" return ""
} }
owner := parts[0] owner := parts[0]
repo := parts[1] repo := parts[1]
// if tag specified // if tag specified
if len(parts) >= 5 && parts[2] == "releases" && parts[3] == "tag" { if len(parts) >= 5 && parts[2] == "releases" && parts[3] == "tag" {
tag := parts[4] tag := parts[4]
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag) return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag)
} }
// default to latest // default to latest
return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
}
// archAliases returns common name variants for an architecture string
// so we can match release asset names like "x86_64" vs Go's "amd64".
// archAliases returns name variants for an architecture string.
// If `arch` is empty or matches the local runtime.GOARCH, prefer the
// compile-time architecture aliases provided by archAliasesForLocal
// (implemented per-architecture via build tags). For other `arch`
// values we use a small synonyms map.
func archAliases(arch string) []string {
a := strings.ToLower(arch)
if syns, ok := archSynonyms[a]; ok {
return syns
}
return []string{a}
}
var archSynonyms = map[string][]string{
"amd64": {"amd64", "x86_64", "x64"},
"x86_64": {"amd64", "x86_64", "x64"},
"x64": {"amd64", "x86_64", "x64"},
"386": {"386", "x86"},
"x86": {"386", "x86"},
"arm64": {"arm64", "aarch64"},
"aarch64": {"arm64", "aarch64"},
"arm": {"arm"},
} }
func extractArchive(archivePath, destDir string) error { func extractArchive(archivePath, destDir string) error {
lower := strings.ToLower(archivePath) lower := strings.ToLower(archivePath)
if strings.HasSuffix(lower, ".zip") { if strings.HasSuffix(lower, ".zip") {
return extractZip(archivePath, destDir) return extractZip(archivePath, destDir)
} }
// treat .tar.gz and .tgz as gzip+tar // treat .tar.gz and .tgz as gzip+tar
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") { if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
return extractTarGz(archivePath, destDir) return extractTarGz(archivePath, destDir)
} }
if strings.HasSuffix(lower, ".tar") { if strings.HasSuffix(lower, ".tar") {
return extractTar(archivePath, destDir) return extractTar(archivePath, destDir)
} }
// fallback: try tar.gz // fallback: try tar.gz
return extractTarGz(archivePath, destDir) return extractTarGz(archivePath, destDir)
} }
func extractZip(archivePath, destDir string) error { func extractZip(archivePath, destDir string) error {
r, err := zip.OpenReader(archivePath) r, err := zip.OpenReader(archivePath)
if err != nil { if err != nil {
return err return err
} }
defer r.Close() defer r.Close()
for _, f := range r.File { for _, f := range r.File {
fp := filepath.Join(destDir, f.Name) fp := filepath.Join(destDir, f.Name)
if f.FileInfo().IsDir() { if f.FileInfo().IsDir() {
_ = os.MkdirAll(fp, f.Mode()) _ = os.MkdirAll(fp, f.Mode())
continue continue
} }
if err := os.MkdirAll(filepath.Dir(fp), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(fp), 0o755); err != nil {
return err return err
} }
rc, err := f.Open() rc, err := f.Open()
if err != nil { if err != nil {
return err return err
} }
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil { if err != nil {
rc.Close() rc.Close()
return err return err
} }
if _, err := io.Copy(out, rc); err != nil { if _, err := io.Copy(out, rc); err != nil {
rc.Close() rc.Close()
out.Close() out.Close()
return err return err
} }
rc.Close() rc.Close()
out.Close() out.Close()
} }
return nil return nil
} }
func extractTarGz(archivePath, destDir string) error { func extractTarGz(archivePath, destDir string) error {
f, err := os.Open(archivePath) f, err := os.Open(archivePath)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
gzr, err := gzip.NewReader(f) gzr, err := gzip.NewReader(f)
if err != nil { if err != nil {
return err return err
} }
defer gzr.Close() defer gzr.Close()
tr := tar.NewReader(gzr) tr := tar.NewReader(gzr)
for { for {
hdr, err := tr.Next() hdr, err := tr.Next()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil { if err != nil {
return err return err
} }
target := filepath.Join(destDir, hdr.Name) target := filepath.Join(destDir, hdr.Name)
switch hdr.Typeflag { switch hdr.Typeflag {
case tar.TypeDir: case tar.TypeDir:
if err := os.MkdirAll(target, 0o755); err != nil { if err := os.MkdirAll(target, 0o755); err != nil {
return err return err
} }
case tar.TypeReg: case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err return err
} }
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode)) out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
if err != nil { if err != nil {
return err return err
} }
if _, err := io.Copy(out, tr); err != nil { if _, err := io.Copy(out, tr); err != nil {
out.Close() out.Close()
return err return err
} }
out.Close() out.Close()
} }
} }
return nil return nil
} }
func extractTar(archivePath, destDir string) error { func extractTar(archivePath, destDir string) error {
f, err := os.Open(archivePath) f, err := os.Open(archivePath)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
tr := tar.NewReader(f) tr := tar.NewReader(f)
for { for {
hdr, err := tr.Next() hdr, err := tr.Next()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil { if err != nil {
return err return err
} }
target := filepath.Join(destDir, hdr.Name) target := filepath.Join(destDir, hdr.Name)
switch hdr.Typeflag { switch hdr.Typeflag {
case tar.TypeDir: case tar.TypeDir:
if err := os.MkdirAll(target, 0o755); err != nil { if err := os.MkdirAll(target, 0o755); err != nil {
return err return err
} }
case tar.TypeReg: case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err return err
} }
out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode)) out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(hdr.Mode))
if err != nil { if err != nil {
return err return err
} }
if _, err := io.Copy(out, tr); err != nil { if _, err := io.Copy(out, tr); err != nil {
out.Close() out.Close()
return err return err
} }
out.Close() out.Close()
} }
} }
return nil return nil
} }
func findBinaryInDir(dir, programName string) (string, error) { func findBinaryInDir(dir, programName string) (string, error) {
wanted := []string{programName} wanted := []string{programName}
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
wanted = append([]string{programName + ".exe"}, wanted...) wanted = append([]string{programName + ".exe"}, wanted...)
} else { } else {
// also accept programs with .exe in archives targeting windows // also accept programs with .exe in archives targeting windows
wanted = append(wanted, programName+".exe") wanted = append(wanted, programName+".exe")
} }
var found string var found string
filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error { filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
if err != nil || found != "" { if err != nil || found != "" {
return err return err
} }
if d.IsDir() { if d.IsDir() {
return nil return nil
} }
base := filepath.Base(p) base := filepath.Base(p)
for _, w := range wanted { for _, w := range wanted {
if base == w { if base == w {
found = p found = p
return io.EOF // use EOF to stop walking early return io.EOF // use EOF to stop walking early
} }
} }
return nil return nil
}) })
if found == "" { if found == "" {
return "", fmt.Errorf("binary %q not found in archive", programName) return "", fmt.Errorf("binary %q not found in archive", programName)
} }
return found, nil return found, nil
} }
// NewUpdateCommand returns a cobra command that triggers UpdateSelfFromRelease. // NewUpdateCommand returns a cobra command that triggers UpdateSelfFromRelease.
func NewUpdateCommand(binaryName string) *cobra.Command { func NewUpdateCommand(binaryName string) *cobra.Command {
var urlStr, platform, arch string var urlStr, platform, arch string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update", Use: "update",
Short: "Check and apply updates from GitHub releases", Short: "Check and apply updates from GitHub releases",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if platform == "" { if platform == "" {
platform = runtime.GOOS platform = runtime.GOOS
} }
if arch == "" { if arch == "" {
arch = runtime.GOARCH arch = runtime.GOARCH
} }
fmt.Printf("Current version: %s\n", config.FormatVersion()) fmt.Printf("Current version: %s\n", config.FormatVersion())
if err := UpdateSelfFromRelease(urlStr, platform, arch, binaryName); err != nil { if err := UpdateSelfFromRelease(urlStr, platform, arch, binaryName); err != nil {
return err return err
} }
fmt.Println("Update applied; restart to use the new version.") fmt.Println("Update applied; restart to use the new version.")
return nil return nil
}, },
} }
cmd.Flags().StringVarP(&urlStr, "url", "u", "", "Direct URL to download release asset or release page") cmd.Flags().StringVarP(&urlStr, "url", "u", "", "Direct URL to download release asset or release page")
cmd.Flags().StringVar(&platform, "platform", "", "Target platform (default: runtime.GOOS)") cmd.Flags().StringVar(&platform, "platform", "", "Target platform (default: runtime.GOOS)")
cmd.Flags().StringVar(&arch, "arch", "", "Target arch (default: runtime.GOARCH)") cmd.Flags().StringVar(&arch, "arch", "", "Target arch (default: runtime.GOARCH)")
return cmd return cmd
} }