Skip to content

Commit f906bbf

Browse files
authored
Merge pull request #312 from lets-cli/codex/adapt-cli-update-plan-to
Add background update notifier
2 parents c73fc07 + 7bc34a6 commit f906bbf

14 files changed

Lines changed: 744 additions & 30 deletions

File tree

.golangci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ run:
55
linters:
66
default: all
77
disable:
8+
- goconst
89
- containedctx
910
- copyloopvar
1011
- cyclop

.zed/debug.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Project-local debug tasks
2+
//
3+
// For more documentation on how to configure debug tasks,
4+
// see: https://zed.dev/docs/debugger
5+
[
6+
{
7+
"label": "Go (Delve)",
8+
"adapter": "Delve",
9+
"program": "$ZED_FILE",
10+
"request": "launch",
11+
"mode": "debug",
12+
},
13+
{
14+
"label": "Run fmt",
15+
"adapter": "Delve",
16+
"request": "launch",
17+
"mode": "debug",
18+
// For Delve, the program can be a package name
19+
"program": "./cmd/lets",
20+
"args": ["fmt"],
21+
// "buildFlags": [],
22+
},
23+
]

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ services:
2525
test-bats:
2626
<<: *base
2727
environment:
28+
LETS_CHECK_UPDATE: 0
2829
NO_COLOR: 1
2930
BATS_UTILS_PATH: /bats
3031
command:

docs/docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ title: Changelog
1515
* `[Changed]` Migrate the LSP YAML parser from the CGO-based tree-sitter bindings to pure-Go [`gotreesitter`](https://github.com/odvcencio/gotreesitter), removing the C toolchain requirement from normal builds and release packaging.
1616
* `[Refactoring]` Move CLI startup flow from `cmd/lets/main.go` into `internal/cli/cli.go`, keeping `main.go` as a thin launcher.
1717
* `[Added]` Add `lets self doc` command to open the online documentation in a browser.
18+
* `[Added]` Show background update notifications for interactive sessions, with Homebrew-aware guidance and `LETS_CHECK_UPDATE` opt-out.
1819

1920
## [0.0.59](https://github.com/lets-cli/lets/releases/tag/v0.0.59)
2021

docs/docs/env.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ title: Environment
1010
* `LETS_DEBUG` - enable debug messages
1111
* `LETS_CONFIG` - changes default `lets.yaml` file path (e.g. LETS_CONFIG=lets.my.yaml)
1212
* `LETS_CONFIG_DIR` - changes path to dir where `lets.yaml` file placed
13+
* `LETS_CHECK_UPDATE` - disables background update checks and notifications
1314
* `NO_COLOR` - disables colored output. See https://no-color.org/
1415

1516
### Environment variables available at command runtime

internal/cli/cli.go

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package cli
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
78
"os/signal"
89
"strings"
910
"syscall"
11+
"time"
1012

1113
"github.com/lets-cli/lets/internal/cmd"
1214
"github.com/lets-cli/lets/internal/config"
@@ -17,10 +19,18 @@ import (
1719
"github.com/lets-cli/lets/internal/upgrade"
1820
"github.com/lets-cli/lets/internal/upgrade/registry"
1921
"github.com/lets-cli/lets/internal/workdir"
22+
"github.com/mattn/go-isatty"
2023
log "github.com/sirupsen/logrus"
2124
"github.com/spf13/cobra"
2225
)
2326

27+
const updateCheckTimeout = 3 * time.Second
28+
29+
type updateCheckResult struct {
30+
notifier *upgrade.UpdateNotifier
31+
notice *upgrade.UpdateNotice
32+
}
33+
2434
func Main(version string, buildDate string) int {
2535
ctx := getContext()
2636

@@ -94,9 +104,9 @@ func Main(version string, buildDate string) int {
94104
}
95105

96106
if rootFlags.upgrade {
97-
upgrader, err := upgrade.NewBinaryUpgrader(registry.NewGithubRegistry(ctx), version)
107+
upgrader, err := upgrade.NewBinaryUpgrader(registry.NewGithubRegistry(), version)
98108
if err == nil {
99-
err = upgrader.Upgrade()
109+
err = upgrader.Upgrade(ctx)
100110
}
101111

102112
if err != nil {
@@ -118,16 +128,22 @@ func Main(version string, buildDate string) int {
118128
return 0
119129
}
120130

131+
updateCh, cancelUpdateCheck := maybeStartUpdateCheck(ctx, version, command)
132+
defer cancelUpdateCheck()
133+
121134
if err := rootCmd.ExecuteContext(ctx); err != nil {
122135
var depErr *executor.DependencyError
123136
if errors.As(err, &depErr) {
124137
executor.PrintDependencyTree(depErr, os.Stderr)
125138
}
126139

127140
log.Errorf("lets: %s", err.Error())
141+
128142
return getExitCode(err, 1)
129143
}
130144

145+
printUpdateNotice(updateCh)
146+
131147
return 0
132148
}
133149

@@ -186,6 +202,82 @@ func allowsMissingConfig(current *cobra.Command) bool {
186202
return false
187203
}
188204

205+
func maybeStartUpdateCheck(
206+
ctx context.Context,
207+
version string,
208+
command *cobra.Command,
209+
) (<-chan updateCheckResult, context.CancelFunc) {
210+
if !shouldCheckForUpdate(command.Name(), isInteractiveStderr()) {
211+
return nil, func() {}
212+
}
213+
214+
log.Debugf("lets: start update check")
215+
216+
notifier, err := upgrade.NewUpdateNotifier(registry.NewGithubRegistry())
217+
if err != nil {
218+
return nil, func() {}
219+
}
220+
221+
ch := make(chan updateCheckResult, 1)
222+
checkCtx, cancel := context.WithTimeout(ctx, updateCheckTimeout)
223+
224+
go func() {
225+
notice, err := notifier.Check(checkCtx, version)
226+
if err != nil {
227+
upgrade.LogUpdateCheckError(err)
228+
}
229+
230+
log.Debugf("lets: update check done")
231+
232+
ch <- updateCheckResult{
233+
notifier: notifier,
234+
notice: notice,
235+
}
236+
}()
237+
238+
return ch, cancel
239+
}
240+
241+
func printUpdateNotice(updateCh <-chan updateCheckResult) {
242+
if updateCh == nil {
243+
return
244+
}
245+
246+
select {
247+
case result := <-updateCh:
248+
if result.notice == nil {
249+
return
250+
}
251+
252+
if _, err := fmt.Fprintln(os.Stderr, result.notice.Message()); err != nil {
253+
return
254+
}
255+
256+
if err := result.notifier.MarkNotified(result.notice); err != nil {
257+
upgrade.LogUpdateCheckError(err)
258+
}
259+
default:
260+
}
261+
}
262+
263+
func shouldCheckForUpdate(commandName string, interactive bool) bool {
264+
if !interactive || os.Getenv("CI") != "" || os.Getenv("LETS_CHECK_UPDATE") != "" {
265+
return false
266+
}
267+
268+
switch commandName {
269+
case "completion", "help", "lsp", "self":
270+
return false
271+
default:
272+
return true
273+
}
274+
}
275+
276+
func isInteractiveStderr() bool {
277+
fd := os.Stderr.Fd()
278+
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
279+
}
280+
189281
type flags struct {
190282
config string
191283
debug int

internal/cli/cli_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,42 @@ func TestAllowsMissingConfig(t *testing.T) {
5757
}
5858
})
5959
}
60+
61+
func TestShouldCheckForUpdate(t *testing.T) {
62+
t.Run("should allow normal interactive commands", func(t *testing.T) {
63+
t.Setenv("CI", "")
64+
t.Setenv("LETS_CHECK_UPDATE", "")
65+
66+
if !shouldCheckForUpdate("lets", true) {
67+
t.Fatal("expected update check to be enabled")
68+
}
69+
})
70+
71+
t.Run("should skip non interactive sessions", func(t *testing.T) {
72+
if shouldCheckForUpdate("lets", false) {
73+
t.Fatal("expected non-interactive session to skip update check")
74+
}
75+
})
76+
77+
t.Run("should skip when CI is set", func(t *testing.T) {
78+
t.Setenv("CI", "1")
79+
if shouldCheckForUpdate("lets", true) {
80+
t.Fatal("expected CI to skip update check")
81+
}
82+
})
83+
84+
t.Run("should skip when notifier disabled", func(t *testing.T) {
85+
t.Setenv("LETS_CHECK_UPDATE", "1")
86+
if shouldCheckForUpdate("lets", true) {
87+
t.Fatal("expected opt-out env to skip update check")
88+
}
89+
})
90+
91+
t.Run("should skip internal commands", func(t *testing.T) {
92+
for _, name := range []string{"completion", "help", "lsp", "self"} {
93+
if shouldCheckForUpdate(name, true) {
94+
t.Fatalf("expected %q to skip update check", name)
95+
}
96+
}
97+
})
98+
}

0 commit comments

Comments
 (0)