66 "os"
77 "os/exec"
88 "path/filepath"
9+ "regexp"
910 "strings"
1011
1112 "github.com/Masterminds/semver/v3"
@@ -25,6 +26,27 @@ type updateResponse struct {
2526 Message string `json:"message"`
2627}
2728
29+ type updateCheckResponse struct {
30+ Current string `json:"current"`
31+ Latest string `json:"latest"`
32+ UpdateAvailable bool `json:"update_available"`
33+ ReleaseBuild bool `json:"release_build"`
34+ InstallMethod string `json:"install_method"`
35+ Channel string `json:"channel"`
36+ Message string `json:"message"`
37+ }
38+
39+ // releaseTaggedVersion matches a stable tag or a dev prerelease. The dev suffix
40+ // stays loose on purpose: dev-release.yml stamps a timestamp while the fixtures
41+ // below carry a timestamp plus a sha, so the marker is what identifies the
42+ // channel, not the stamp's shape.
43+ //
44+ // It is a shape check rather than a semver parse because git-describe is what it
45+ // must reject, and "v0.8.1-6-gabc1234-dirty" parses fine as semver while
46+ // ordering BELOW v0.8.1. Accepting it makes the newest release compare as newer,
47+ // so an "update" installs older code than the build already running.
48+ var releaseTaggedVersion = regexp .MustCompile (`^v\d+\.\d+\.\d+(-dev\.[0-9A-Za-z.]+)?$` )
49+
2850type updateRelease struct {
2951 Version string
3052 raw * selfupdate.Release
@@ -97,13 +119,77 @@ func newUpdateCmd(ctx *cmdContext) *cobra.Command {
97119 Annotations : map [string ]string {"skipAuth" : "true" },
98120 RunE : func (cmd * cobra.Command , args []string ) error {
99121 targetVersion , _ := cmd .Flags ().GetString ("version" )
122+ check , _ := cmd .Flags ().GetBool ("check" )
123+ if check {
124+ if cmd .Flags ().Changed ("version" ) {
125+ return clierrors .NewUsage ("--check reports what an update would do; it cannot be combined with --version" )
126+ }
127+ return runUpdateCheck (ctx )
128+ }
100129 return runUpdate (ctx , targetVersion )
101130 },
102131 }
103132 cmd .Flags ().String ("version" , "" , "Update to a specific version (e.g., v0.1.0)" )
133+ cmd .Flags ().Bool ("check" , false , "Report whether an update is available without installing it" )
104134 return cmd
105135}
106136
137+ // runUpdateCheck answers "what would heygen update do", without doing it.
138+ //
139+ // It reports rather than refuses, which is the whole difference from runUpdate:
140+ // a non-release build and a package-manager install are both terminal errors
141+ // there, but here they are the answer, so a caller can branch on the fields
142+ // instead of parsing an error.
143+ func runUpdateCheck (ctx * cmdContext ) error {
144+ raw := updateBuildVersion (ctx )
145+ resp := updateCheckResponse {Current : raw }
146+
147+ // Best-effort: an install method we cannot determine still permits the
148+ // version comparison, which is the part the caller asked for.
149+ if method , _ , err := detectInstallMethod (); err == nil {
150+ resp .InstallMethod = method
151+ }
152+
153+ current , err := validateCurrentVersion (raw )
154+ if err != nil {
155+ resp .Message = "not a release build, so no update can be offered"
156+ return emitUpdateCheck (ctx , resp )
157+ }
158+ resp .Current = current
159+ resp .ReleaseBuild = true
160+ resp .Channel = updateChannel (current )
161+
162+ updater , err := newReleaseUpdater (resp .Channel == "dev" )
163+ if err != nil {
164+ return err
165+ }
166+ rel , found , err := updater .DetectLatest (context .Background ())
167+ if err != nil {
168+ return clierrors .New (fmt .Sprintf ("failed to check for updates: %v" , err ))
169+ }
170+ if ! found {
171+ resp .Message = fmt .Sprintf ("no %s release found for this platform" , resp .Channel )
172+ return emitUpdateCheck (ctx , resp )
173+ }
174+
175+ resp .Latest = rel .Version
176+ resp .UpdateAvailable = isVersionGreater (rel .Version , current )
177+ if resp .UpdateAvailable {
178+ resp .Message = fmt .Sprintf ("heygen %s is available; you have %s" , rel .Version , current )
179+ } else {
180+ resp .Message = fmt .Sprintf ("heygen is up to date at %s" , current )
181+ }
182+ return emitUpdateCheck (ctx , resp )
183+ }
184+
185+ func emitUpdateCheck (ctx * cmdContext , resp updateCheckResponse ) error {
186+ data , err := marshalData (resp )
187+ if err != nil {
188+ return err
189+ }
190+ return ctx .formatter .Data (data , "" , nil )
191+ }
192+
107193func runUpdate (ctx * cmdContext , targetVersion string ) error {
108194 current , err := validateCurrentVersion (updateBuildVersion (ctx ))
109195 if err != nil {
@@ -138,10 +224,7 @@ func runUpdate(ctx *cmdContext, targetVersion string) error {
138224 return clierrors .New (fmt .Sprintf ("failed to resolve executable path: %v" , err ))
139225 }
140226
141- // Auto-detect update channel from current version: dev builds track
142- // dev prereleases, stable builds track stable releases only.
143- isDevBuild := strings .Contains (current , "-dev." )
144- updater , err := newReleaseUpdater (isDevBuild )
227+ updater , err := newReleaseUpdater (updateChannel (current ) == "dev" )
145228 if err != nil {
146229 return err
147230 }
@@ -190,16 +273,25 @@ func runUpdate(ctx *cmdContext, targetVersion string) error {
190273}
191274
192275func validateCurrentVersion (raw string ) (string , error ) {
193- if raw == "" || raw == "dev" || raw == "test" {
276+ version := canonicalVersion (raw )
277+ if ! releaseTaggedVersion .MatchString (version ) {
194278 return "" , clierrors .New ("current build version is not release-tagged; reinstall from a release build to use heygen update" )
195279 }
196- version := canonicalVersion (raw )
197280 if _ , err := semver .NewVersion (strings .TrimPrefix (version , "v" )); err != nil {
198281 return "" , clierrors .New (fmt .Sprintf ("current build version %q is not a valid semantic version" , raw ))
199282 }
200283 return version , nil
201284}
202285
286+ // updateChannel selects the release track, which becomes the updater's
287+ // prerelease flag: a dev build sees dev prereleases, a stable build never does.
288+ func updateChannel (version string ) string {
289+ if strings .Contains (version , "-dev." ) {
290+ return "dev"
291+ }
292+ return "stable"
293+ }
294+
203295func validateTargetVersion (raw string ) error {
204296 if ! strings .HasPrefix (raw , "v" ) {
205297 return clierrors .NewUsage ("version must include the leading v (for example: v0.1.0)" )
0 commit comments