Skip to content

Commit 461e5f2

Browse files
feat: implement self-updating mechanism and installation scripts
Introduce a robust self-update system via GitHub Releases, add cross-platform installation scripts, and improve build metadata injection. - Implement `kairo update` command to download, verify, and swap binaries - Add `scripts/install.sh` and `scripts/install.ps1` for easy setup - Refactor build metadata injection using `internal/buildinfo` via GoReleaser - Update plugin menu to display keybinding legends - Update versioning and documentation to reflect 1.1.3 release
1 parent d968414 commit 461e5f2

20 files changed

Lines changed: 1019 additions & 159 deletions

File tree

.goreleaser.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ builds:
2828

2929
mod_timestamp: "{{ .CommitTimestamp }}"
3030

31-
ldflags:
32-
- -s -w
33-
- -X main.version={{ .Version }}
34-
- -X main.commit={{ .ShortCommit }}
35-
- -X main.date={{ .CommitDate }}
31+
ldflags:
32+
- -s -w
33+
- -X github.com/programmersd21/kairo/internal/buildinfo.Version={{ .Version }}
34+
- -X github.com/programmersd21/kairo/internal/buildinfo.Commit={{ .ShortCommit }}
35+
- -X github.com/programmersd21/kairo/internal/buildinfo.Date={{ .CommitDate }}
3636

3737
archives:
3838
- id: default
@@ -73,4 +73,4 @@ changelog:
7373
release:
7474
draft: false
7575
prerelease: auto
76-
76+

CHANGELOG.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,26 @@
33
All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7-
8-
## [1.1.2]
9-
10-
### Added
11-
- **Plugin Metadata Display**: Press `Enter` on a plugin in the menu to view full metadata including Name, Description, Author, and Version.
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.1.3]
9+
10+
### Added
11+
- **Self-updating binary updater**: `kairo update` now downloads the correct GitHub Release asset for your OS/arch, verifies it against `checksums.txt`, and performs a safe in-place binary swap (with `.old` backup/rollback).
12+
- **Cross-platform install scripts**: `scripts/install.sh` (Linux/macOS) and `scripts/install.ps1` (Windows) install into standard user locations and add the install directory to PATH when possible.
13+
- **Plugin menu keybind footer**: plugin manager overlay now shows a quick keybind legend (`enter`, `u`, `esc`, etc.).
14+
15+
### Changed
16+
- `kairo version` now prints build version + commit (when available).
17+
- GoReleaser now injects build metadata into `internal/buildinfo` (instead of `main.*`).
18+
19+
### Removed
20+
- `go install`-based updater flow (replaced by the GitHub Releases updater).
21+
22+
## [1.1.2]
23+
24+
### Added
25+
- **Plugin Metadata Display**: Press `Enter` on a plugin in the menu to view full metadata including Name, Description, Author, and Version.
1226
- **Uninstall Confirmation**: Added safety confirmation dialog before uninstalling plugins with `u` key.
1327

1428
## [1.1.1]

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,35 @@ Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) (TUI framewo
5454

5555
---
5656

57+
## 📦 Installation
58+
59+
### Linux / macOS
60+
61+
```bash
62+
curl -fsSL https://raw.githubusercontent.com/programmersd21/kairo/main/scripts/install.sh | bash
63+
```
64+
65+
Installs to `$HOME/.local/bin/kairo` (fallback: `/usr/local/bin/kairo`) and attempts to persist the PATH update in your shell profile when needed.
66+
67+
### Windows (PowerShell)
68+
69+
```powershell
70+
iwr -useb https://raw.githubusercontent.com/programmersd21/kairo/main/scripts/install.ps1 | iex
71+
```
72+
73+
Installs to `%USERPROFILE%\\AppData\\Local\\Programs\\kairo\\kairo.exe` and adds the install directory to your user PATH.
74+
75+
### Updates
76+
77+
```bash
78+
kairo update
79+
```
80+
81+
Downloads the latest GitHub Release for your OS/arch, verifies it against `checksums.txt`, and safely replaces the installed binary.
82+
On Windows, the update is applied after `kairo update` exits; run `kairo` again once it completes.
83+
84+
---
85+
5786
## 🤖 Automation & CLI API
5887

5988
Kairo provides a stable CLI API for external automation. Every operation available in the TUI can be performed via the `api` subcommand.
@@ -293,6 +322,8 @@ kairo/
293322
│ ├── app
294323
│ │ ├── model.go
295324
│ │ └── msg.go
325+
│ ├── buildinfo
326+
│ │ └── buildinfo.go
296327
│ ├── config
297328
│ │ ├── config.go
298329
│ │ └── config_test.go
@@ -350,6 +381,13 @@ kairo/
350381
│ │ │ └── theme.go
351382
│ │ └── theme_menu
352383
│ │ └── model.go
384+
│ ├── updater
385+
│ │ ├── checksums.go
386+
│ │ ├── download.go
387+
│ │ ├── extract.go
388+
│ │ ├── github.go
389+
│ │ ├── updater.go
390+
│ │ └── windows_helper.go
353391
│ └── util
354392
│ ├── paths.go
355393
│ └── util_test.go
@@ -363,6 +401,9 @@ kairo/
363401
├── README.md
364402
├── screenshots
365403
│ └── thumbnail.png
404+
├── scripts
405+
│ ├── install.ps1
406+
│ └── install.sh
366407
├── SECURITY.md
367408
└── VERSION.txt
368409
```

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.2
1+
1.1.3

cmd/kairo/main.go

Lines changed: 17 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
"io"
9-
"net/http"
108
"os"
11-
"os/exec"
129
"os/signal"
1310
"path/filepath"
1411
"strings"
@@ -18,16 +15,26 @@ import (
1815

1916
"github.com/programmersd21/kairo/internal/api"
2017
"github.com/programmersd21/kairo/internal/app"
18+
"github.com/programmersd21/kairo/internal/buildinfo"
2119
"github.com/programmersd21/kairo/internal/config"
2220
"github.com/programmersd21/kairo/internal/core"
2321
"github.com/programmersd21/kairo/internal/core/codec"
2422
"github.com/programmersd21/kairo/internal/hooks"
2523
"github.com/programmersd21/kairo/internal/service"
2624
"github.com/programmersd21/kairo/internal/storage"
2725
ksync "github.com/programmersd21/kairo/internal/sync"
26+
"github.com/programmersd21/kairo/internal/updater"
2827
)
2928

3029
func main() {
30+
if handled, err := updater.MaybeRunWindowsApply(os.Stdout, os.Stderr); handled {
31+
if err != nil {
32+
fmt.Fprintln(os.Stderr, "kairo update:", err)
33+
os.Exit(2)
34+
}
35+
return
36+
}
37+
3138
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
3239
defer cancel()
3340

@@ -245,144 +252,14 @@ func runImport(ctx context.Context, repo *storage.Repository, args []string) err
245252
}
246253

247254
func runVersion() {
248-
version := getCurrentVersion()
249-
fmt.Printf("kairo version %s\n", version)
250-
}
251-
252-
func getCurrentVersion() string {
253-
// Try current working directory first (most common for development/running)
254-
if data, err := os.ReadFile("VERSION.txt"); err == nil {
255-
return strings.TrimSpace(string(data))
256-
}
257-
258-
// Try repository root relative to binary location
259-
if ex, err := os.Executable(); err == nil {
260-
versionPath := filepath.Join(filepath.Dir(ex), "VERSION.txt")
261-
if data, err := os.ReadFile(versionPath); err == nil {
262-
return strings.TrimSpace(string(data))
263-
}
264-
265-
// Try going up from the binary directory (for development builds)
266-
versionPath = filepath.Join(filepath.Dir(ex), "..", "VERSION.txt")
267-
if data, err := os.ReadFile(versionPath); err == nil {
268-
return strings.TrimSpace(string(data))
269-
}
270-
}
271-
272-
// If not found in expected locations, try home config directory
273-
homeDir, _ := os.UserHomeDir()
274-
altPath := filepath.Join(homeDir, ".config", "kairo", "VERSION.txt")
275-
if data, err := os.ReadFile(altPath); err == nil {
276-
return strings.TrimSpace(string(data))
277-
}
278-
279-
// Fallback to default
280-
return "1.1.2"
281-
}
282-
283-
type GitHubRelease struct {
284-
TagName string `json:"tag_name"`
285-
Draft bool `json:"draft"`
286-
Prerelease bool `json:"prerelease"`
287-
}
288-
289-
func getLatestGitHubRelease(ctx context.Context) (string, error) {
290-
// Query GitHub API for the latest release
291-
url := "https://api.github.com/repos/programmersd21/kairo/releases/latest"
292-
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
293-
if err != nil {
294-
return "", err
295-
}
296-
297-
req.Header.Set("Accept", "application/vnd.github.v3+json")
298-
299-
client := &http.Client{}
300-
resp, err := client.Do(req)
301-
if err != nil {
302-
return "", err
303-
}
304-
defer func() {
305-
_ = resp.Body.Close()
306-
}()
307-
308-
if resp.StatusCode != http.StatusOK {
309-
return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
310-
}
311-
312-
body, err := io.ReadAll(resp.Body)
313-
if err != nil {
314-
return "", err
315-
}
316-
317-
var release GitHubRelease
318-
if err := json.Unmarshal(body, &release); err != nil {
319-
return "", err
320-
}
321-
322-
// Remove 'v' prefix if present
323-
version := strings.TrimPrefix(release.TagName, "v")
324-
return version, nil
325-
}
326-
327-
func compareVersions(current, latest string) int {
328-
// Parse semantic versions: x.y.z
329-
currentParts := strings.Split(strings.TrimPrefix(current, "v"), ".")
330-
latestParts := strings.Split(strings.TrimPrefix(latest, "v"), ".")
331-
332-
// Pad with zeros if needed
333-
for len(currentParts) < 3 {
334-
currentParts = append(currentParts, "0")
335-
}
336-
for len(latestParts) < 3 {
337-
latestParts = append(latestParts, "0")
338-
}
339-
340-
// Simple numeric comparison
341-
for i := 0; i < 3; i++ {
342-
var curr, last int
343-
_, _ = fmt.Sscanf(currentParts[i], "%d", &curr)
344-
_, _ = fmt.Sscanf(latestParts[i], "%d", &last)
345-
346-
if last > curr {
347-
return -1 // latest is newer
348-
}
349-
if last < curr {
350-
return 1 // current is newer
351-
}
352-
}
353-
return 0 // equal
255+
fmt.Printf("kairo %s\n", buildinfo.VersionWithCommit())
354256
}
355257

356258
func runUpdate(ctx context.Context) error {
357-
currentVersion := getCurrentVersion()
358-
fmt.Printf("Checking for updates (current: %s)...\n", currentVersion)
359-
360-
latestVersion, err := getLatestGitHubRelease(ctx)
361-
if err != nil {
362-
return fmt.Errorf("failed to fetch latest release: %w", err)
363-
}
364-
365-
cmp := compareVersions(currentVersion, latestVersion)
366-
if cmp >= 0 {
367-
// Already on latest or newer version
368-
fmt.Printf("✓ Updated to latest version %s\n", currentVersion)
369-
return nil
370-
}
371-
372-
// There's a newer version available
373-
fmt.Printf("Found newer version: %s\n", latestVersion)
374-
fmt.Println("Updating kairo...")
375-
376-
// Execute: go install github.com/programmersd21/kairo/cmd/kairo@latest
377-
cmd := exec.CommandContext(ctx, "go", "install", "github.com/programmersd21/kairo/cmd/kairo@latest")
378-
cmd.Stdout = os.Stdout
379-
cmd.Stderr = os.Stderr
380-
cmd.Stdin = os.Stdin
381-
382-
if err := cmd.Run(); err != nil {
383-
return fmt.Errorf("failed to update: %w", err)
384-
}
385-
386-
fmt.Printf("✓ Kairo updated to version %s successfully!\n", latestVersion)
387-
return nil
259+
cfg := updater.DefaultConfig()
260+
return cfg.Update(ctx, updater.UpdateOptions{
261+
CurrentVersion: buildinfo.EffectiveVersion(),
262+
Stdout: os.Stdout,
263+
Stderr: os.Stderr,
264+
})
388265
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ require (
1515
)
1616

1717
require (
18+
aead.dev/minisign v0.2.0 // indirect
1819
github.com/AlekSi/pointer v1.0.0 // indirect
20+
github.com/Masterminds/semver/v3 v3.4.0 // indirect
1921
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
2022
github.com/atotto/clipboard v0.1.4 // indirect
2123
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -35,6 +37,7 @@ require (
3537
github.com/mattn/go-localereader v0.0.1 // indirect
3638
github.com/mattn/go-runewidth v0.0.16 // indirect
3739
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
40+
github.com/minio/selfupdate v0.6.0 // indirect
3841
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
3942
github.com/muesli/cancelreader v0.2.2 // indirect
4043
github.com/muesli/reflow v0.3.0 // indirect
@@ -46,6 +49,7 @@ require (
4649
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4750
github.com/yuin/goldmark v1.7.8 // indirect
4851
github.com/yuin/goldmark-emoji v1.0.5 // indirect
52+
golang.org/x/crypto v0.31.0 // indirect
4953
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
5054
golang.org/x/net v0.33.0 // indirect
5155
golang.org/x/sync v0.15.0 // indirect

0 commit comments

Comments
 (0)