-
Notifications
You must be signed in to change notification settings - Fork 5
Add update notification prompt on command start #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ func setDefaults() { | |
| "port": "4566", | ||
| }, | ||
| }) | ||
| viper.SetDefault("update_prompt", true) | ||
| } | ||
|
|
||
| func loadConfig(path string) error { | ||
|
|
@@ -90,6 +91,19 @@ func resolvedConfigPath() string { | |
| return viper.ConfigFileUsed() | ||
| } | ||
|
|
||
| func Set(key string, value any) error { | ||
| viper.Set(key, value) | ||
| return viper.WriteConfig() | ||
| } | ||
|
|
||
| func IsUpdatePromptEnabled() bool { | ||
| return viper.GetBool("update_prompt") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: shouldn't this go as a new field in the Config struct instead?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes 😬 |
||
| } | ||
|
|
||
| func DisableUpdatePrompt() error { | ||
| return Set("update_prompt", false) | ||
| } | ||
|
|
||
| func Get() (*Config, error) { | ||
| var cfg Config | ||
| if err := viper.Unmarshal(&cfg); err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package update | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/localstack/lstk/internal/output" | ||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/localstack/lstk/internal/version" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type versionFetcher func(ctx context.Context, token string) (string, error) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type NotifyOptions struct { | ||||||||||||||||||||||||||||||||||||||||||||||
| GitHubToken string | ||||||||||||||||||||||||||||||||||||||||||||||
| UpdatePrompt bool | ||||||||||||||||||||||||||||||||||||||||||||||
| PersistDisable func() error | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const checkTimeout = 500 * time.Millisecond | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func CheckQuietly(ctx context.Context, githubToken string) (current, latest string, available bool) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return checkQuietlyWithVersion(ctx, githubToken, version.Version(), fetchLatestVersion) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func checkQuietlyWithVersion(ctx context.Context, githubToken string, currentVersion string, fetch versionFetcher) (current, latest string, available bool) { | ||||||||||||||||||||||||||||||||||||||||||||||
| current = currentVersion | ||||||||||||||||||||||||||||||||||||||||||||||
| if current == "dev" { | ||||||||||||||||||||||||||||||||||||||||||||||
| return current, "", false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| ctx, cancel := context.WithTimeout(ctx, checkTimeout) | ||||||||||||||||||||||||||||||||||||||||||||||
| defer cancel() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| latestVer, err := fetch(ctx, githubToken) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return current, "", false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if normalizeVersion(current) == normalizeVersion(latestVer) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return current, latestVer, false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return current, latestVer, true | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func UpdateCommandHint(info InstallInfo) string { | ||||||||||||||||||||||||||||||||||||||||||||||
| switch info.Method { | ||||||||||||||||||||||||||||||||||||||||||||||
| case InstallHomebrew: | ||||||||||||||||||||||||||||||||||||||||||||||
| return "brew upgrade localstack/tap/lstk" | ||||||||||||||||||||||||||||||||||||||||||||||
| case InstallNPM: | ||||||||||||||||||||||||||||||||||||||||||||||
| return "npm install -g @localstack/lstk@latest" | ||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||
| return "lstk update" | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Differentiate local and global npm installs in the hint.
💡 Suggested fix func UpdateCommandHint(info InstallInfo) string {
switch info.Method {
case InstallHomebrew:
return "brew upgrade localstack/tap/lstk"
case InstallNPM:
- return "npm install -g `@localstack/lstk`@latest"
+ if npmProjectDir(info.ResolvedPath) != "" {
+ return "npm install `@localstack/lstk`@latest"
+ }
+ return "npm install -g `@localstack/lstk`@latest"
default:
return "lstk update"
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func NotifyUpdate(ctx context.Context, sink output.Sink, opts NotifyOptions) (exitAfter bool) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return notifyUpdateWithVersion(ctx, sink, opts, version.Version(), fetchLatestVersion) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func notifyUpdateWithVersion(ctx context.Context, sink output.Sink, opts NotifyOptions, currentVersion string, fetch versionFetcher) (exitAfter bool) { | ||||||||||||||||||||||||||||||||||||||||||||||
| current, latest, available := checkQuietlyWithVersion(ctx, opts.GitHubToken, currentVersion, fetch) | ||||||||||||||||||||||||||||||||||||||||||||||
| if !available { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if !opts.UpdatePrompt { | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitNote(sink, fmt.Sprintf("Update available: %s → %s (run %s)", current, latest, UpdateCommandHint(DetectInstallMethod()))) | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return promptAndUpdate(ctx, sink, opts.GitHubToken, current, latest, opts.PersistDisable) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func promptAndUpdate(ctx context.Context, sink output.Sink, githubToken string, current, latest string, persistDisable func() error) (exitAfter bool) { | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitWarning(sink, fmt.Sprintf("Update available: %s → %s", current, latest)) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| responseCh := make(chan output.InputResponse, 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitUserInputRequest(sink, output.UserInputRequestEvent{ | ||||||||||||||||||||||||||||||||||||||||||||||
| Prompt: "A new version is available", | ||||||||||||||||||||||||||||||||||||||||||||||
| Options: []output.InputOption{ | ||||||||||||||||||||||||||||||||||||||||||||||
| {Key: "u", Label: "Update"}, | ||||||||||||||||||||||||||||||||||||||||||||||
| {Key: "s", Label: "SKIP"}, | ||||||||||||||||||||||||||||||||||||||||||||||
| {Key: "n", Label: "Never ask again"}, | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ResponseCh: responseCh, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| var resp output.InputResponse | ||||||||||||||||||||||||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||||||||||||||||||||||||
| case resp = <-responseCh: | ||||||||||||||||||||||||||||||||||||||||||||||
| case <-ctx.Done(): | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if resp.Cancelled { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| switch resp.SelectedKey { | ||||||||||||||||||||||||||||||||||||||||||||||
| case "u": | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := applyUpdate(ctx, sink, latest, githubToken); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitWarning(sink, fmt.Sprintf("Update failed: %v", err)) | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitSuccess(sink, fmt.Sprintf("Updated to %s — please re-run your command.", latest)) | ||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||
| case "n": | ||||||||||||||||||||||||||||||||||||||||||||||
| if persistDisable != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := persistDisable(); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| output.EmitWarning(sink, fmt.Sprintf("Failed to save preference: %v", err)) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: we're adding latency to all commands with that check, so I am wondering if we should do the check asynchronously or just less often in the future (say once per 24h). Just a note for the future, no action required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created an issue already for this after noticing. What other tools do is do the check in the background, write it down and on next start show it.
For now I've set the timeout to 500ms. If it fails because of the timeout we won't show it.