Skip to content

Commit a221962

Browse files
committed
Add update command and auto-update check
Introduce an internal update checker and a new CLI command. Adds internal/update package that queries GitHub releases (lnbotdev/cli) with a 3s timeout and 24h cache interval, storing results at ~/.config/lnbot/.update-check and honoring LNBOT_NO_UPDATE_CHECK. Adds cmd/update.go (cobra command) to show current/latest versions and platform-specific upgrade instructions. Integrates the update check into root.PersistentPostRun to notify users when an update is available and registers the update command in the command groups.
1 parent 7364bb7 commit a221962

3 files changed

Lines changed: 145 additions & 1 deletion

File tree

cmd/root.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
lnbot "github.com/lnbotdev/go-sdk"
1212

1313
"github.com/lnbotdev/cli/internal/config"
14+
"github.com/lnbotdev/cli/internal/update"
1415
)
1516

1617
var (
@@ -39,6 +40,12 @@ var rootCmd = &cobra.Command{
3940
cfg, err = config.Load()
4041
return err
4142
},
43+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
44+
if latest, ok := update.CheckForUpdate(version); ok {
45+
fmt.Fprintf(os.Stderr, "\nUpdate available: %s → %s\n", version, latest)
46+
fmt.Fprintf(os.Stderr, "Run: curl -fsSL https://ln.bot/install.sh | bash\n")
47+
}
48+
},
4249
}
4350

4451
const rootHelpTmpl = `{{.Long}}
@@ -133,6 +140,7 @@ func init() {
133140
webhookCmd.GroupID = "integrations"
134141
mcpCmd.GroupID = "integrations"
135142

143+
updateCmd.GroupID = "other"
136144
completionCmd.GroupID = "other"
137145
versionCmd.GroupID = "other"
138146

@@ -151,6 +159,7 @@ func init() {
151159
rootCmd.AddCommand(addressCmd)
152160
rootCmd.AddCommand(webhookCmd)
153161
rootCmd.AddCommand(mcpCmd)
162+
rootCmd.AddCommand(updateCmd)
154163
rootCmd.AddCommand(versionCmd)
155164

156165
cobra.AddTemplateFuncs(template.FuncMap{
@@ -174,7 +183,7 @@ func init() {
174183
// Leaf commands: use default cobra template (don't inherit root's grouped template)
175184
leafCmds := []*cobra.Command{
176185
initCmd, balanceCmd, statusCmd, whoamiCmd, payCmd, transactionsCmd,
177-
completionCmd, versionCmd,
186+
updateCmd, completionCmd, versionCmd,
178187
}
179188
for _, cmd := range leafCmds {
180189
cmd.SetHelpTemplate(leafHelpTmpl)

cmd/update.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"runtime"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/lnbotdev/cli/internal/update"
10+
)
11+
12+
var updateCmd = &cobra.Command{
13+
Use: "update",
14+
Short: "Check for updates or show upgrade instructions",
15+
Long: `Check if a newer version of lnbot is available and show how to upgrade.`,
16+
Example: ` lnbot update`,
17+
Run: func(cmd *cobra.Command, args []string) {
18+
fmt.Printf("Current version: %s\n", version)
19+
20+
latest, available := update.CheckForUpdate(version)
21+
if !available {
22+
fmt.Println("You're up to date.")
23+
return
24+
}
25+
26+
fmt.Printf("Latest version: %s\n", latest)
27+
fmt.Println()
28+
fmt.Println("To update:")
29+
30+
switch runtime.GOOS {
31+
case "windows":
32+
fmt.Println(" PowerShell: iwr -useb https://ln.bot/install.ps1 | iex")
33+
fmt.Println(" CMD: curl -fsSL https://ln.bot/install.cmd -o install.cmd && install.cmd && del install.cmd")
34+
default:
35+
fmt.Println(" curl -fsSL https://ln.bot/install.sh | bash")
36+
}
37+
},
38+
}

internal/update/update.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package update
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"path/filepath"
9+
"time"
10+
)
11+
12+
const (
13+
repo = "lnbotdev/cli"
14+
checkTimeout = 3 * time.Second
15+
checkInterval = 24 * time.Hour
16+
)
17+
18+
type cachedCheck struct {
19+
Latest string `json:"latest"`
20+
CheckedAt int64 `json:"checked_at"`
21+
}
22+
23+
func cacheFile() string {
24+
home, _ := os.UserHomeDir()
25+
return filepath.Join(home, ".config", "lnbot", ".update-check")
26+
}
27+
28+
func CheckForUpdate(current string) (latest string, available bool) {
29+
if os.Getenv("LNBOT_NO_UPDATE_CHECK") != "" {
30+
return "", false
31+
}
32+
33+
// Check cache first
34+
if data, err := os.ReadFile(cacheFile()); err == nil {
35+
var cached cachedCheck
36+
if json.Unmarshal(data, &cached) == nil {
37+
if time.Since(time.Unix(cached.CheckedAt, 0)) < checkInterval {
38+
if cached.Latest != "" && cached.Latest != current {
39+
return cached.Latest, true
40+
}
41+
return "", false
42+
}
43+
}
44+
}
45+
46+
// Fetch latest release from GitHub
47+
latest, err := fetchLatest()
48+
if err != nil {
49+
return "", false
50+
}
51+
52+
// Cache the result
53+
saveCache(latest)
54+
55+
if latest != current && latest != "" {
56+
return latest, true
57+
}
58+
return "", false
59+
}
60+
61+
func fetchLatest() (string, error) {
62+
client := &http.Client{Timeout: checkTimeout}
63+
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
64+
65+
resp, err := client.Get(url)
66+
if err != nil {
67+
return "", err
68+
}
69+
defer resp.Body.Close()
70+
71+
if resp.StatusCode != 200 {
72+
return "", fmt.Errorf("status %d", resp.StatusCode)
73+
}
74+
75+
var release struct {
76+
TagName string `json:"tag_name"`
77+
}
78+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
79+
return "", err
80+
}
81+
82+
tag := release.TagName
83+
if len(tag) > 0 && tag[0] == 'v' {
84+
tag = tag[1:]
85+
}
86+
return tag, nil
87+
}
88+
89+
func saveCache(latest string) {
90+
data, _ := json.Marshal(cachedCheck{
91+
Latest: latest,
92+
CheckedAt: time.Now().Unix(),
93+
})
94+
p := cacheFile()
95+
os.MkdirAll(filepath.Dir(p), 0o700)
96+
os.WriteFile(p, data, 0o600)
97+
}

0 commit comments

Comments
 (0)