Skip to content

Commit 124e573

Browse files
TeoSlayerteovl
andauthored
Add pilotctl auto-update control surface (off by default) (#304)
Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
1 parent eeee27e commit 124e573

6 files changed

Lines changed: 149 additions & 10 deletions

File tree

cmd/pilotctl/main.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,13 +1148,19 @@ Common keys:
11481148
11491149
Print the pilotctl build version string.
11501150
`,
1151-
"update": `Usage: pilotctl update [flags]
1151+
"update": `Usage: pilotctl update [subcommand|flags]
11521152
1153-
Run the updater once — check for new releases and install if available.
1154-
In manual mode (daemon not running), re-runs skill install so newly
1155-
installed binaries have matching skill definitions.
1153+
Automatic updates are OFF by default. Control them with:
1154+
pilotctl update status show whether auto-update is on and the current version
1155+
pilotctl update enable turn automatic updates ON
1156+
pilotctl update disable turn automatic updates OFF (default)
11561157
1157-
Flags:
1158+
With no subcommand, runs the updater ONCE — a manual check that installs the
1159+
latest release if available, regardless of the auto-update setting. In manual
1160+
mode (daemon not running), re-runs skill install so newly installed binaries
1161+
have matching skill definitions.
1162+
1163+
Flags (one-shot mode):
11581164
--repo <name> GitHub owner/repo for releases (default: pilot-protocol/pilotprotocol)
11591165
--pin <tag> pin to a specific release tag (e.g. v1.10.5)
11601166
`,

cmd/pilotctl/updates.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package main
44

55
import (
6+
"encoding/json"
67
"encoding/xml"
78
"fmt"
89
"io"
@@ -17,6 +18,76 @@ import (
1718
"github.com/pilot-protocol/updater"
1819
)
1920

21+
// autoUpdateStatePath is the JSON control file ({"enabled": bool}) shared with
22+
// the pilot-updater loop (passed as its --state-path). Automatic updates are
23+
// OFF by default: when the file is absent the updater applies nothing.
24+
func autoUpdateStatePath() string { return configDir() + "/auto-update.json" }
25+
26+
// autoUpdateEnabled reports the persisted auto-update setting (default off).
27+
func autoUpdateEnabled() bool {
28+
data, err := os.ReadFile(autoUpdateStatePath())
29+
if err != nil {
30+
return false
31+
}
32+
var s struct {
33+
Enabled bool `json:"enabled"`
34+
}
35+
if err := json.Unmarshal(data, &s); err != nil {
36+
return false
37+
}
38+
return s.Enabled
39+
}
40+
41+
// cmdAutoUpdateSet turns automatic updates on or off (`pilotctl update
42+
// enable|disable`). The pilot-updater re-reads the file each tick, so this
43+
// takes effect without restarting it.
44+
func cmdAutoUpdateSet(on bool) {
45+
path := autoUpdateStatePath()
46+
_ = os.MkdirAll(configDir(), 0o755)
47+
data, _ := json.MarshalIndent(map[string]bool{"enabled": on}, "", " ")
48+
if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil {
49+
fatalCode("internal", "write %s: %v", path, err)
50+
}
51+
if jsonOutput {
52+
outputOK(map[string]interface{}{"auto_update": on})
53+
return
54+
}
55+
if on {
56+
fmt.Println("Automatic updates ENABLED. The updater will install new stable releases on its check interval.")
57+
fmt.Println("Disable any time with: pilotctl update disable")
58+
} else {
59+
fmt.Println("Automatic updates DISABLED. Nothing will be installed automatically.")
60+
fmt.Println("Run a one-time manual update with: pilotctl update")
61+
}
62+
}
63+
64+
// cmdAutoUpdateStatus shows whether automatic updates are on and the current
65+
// version (`pilotctl update status`).
66+
func cmdAutoUpdateStatus() {
67+
on := autoUpdateEnabled()
68+
if jsonOutput {
69+
outputOK(map[string]interface{}{
70+
"auto_update": on,
71+
"current_version": version,
72+
"state_file": autoUpdateStatePath(),
73+
})
74+
return
75+
}
76+
state := "disabled"
77+
if on {
78+
state = "enabled"
79+
}
80+
fmt.Printf("Automatic updates: %s\n", state)
81+
fmt.Printf("Current version: %s\n", version)
82+
fmt.Printf("State file: %s\n", autoUpdateStatePath())
83+
if on {
84+
fmt.Println("\nTurn off with: pilotctl update disable")
85+
} else {
86+
fmt.Println("\nTurn on with: pilotctl update enable")
87+
fmt.Println("One-time check: pilotctl update")
88+
}
89+
}
90+
2091
// changelogFeedURL is the canonical RSS 2.0 feed for the public Pilot
2192
// Protocol changelog. Hosted on GitHub Pages from the pilot-changelog
2293
// repo (per `pilot-changelog/README.md`). RSS chosen over feed.json so
@@ -218,6 +289,22 @@ func collapseWhitespace(s string) string {
218289
// --pin <tag> : pin to a specific release tag (e.g. v1.10.5)
219290
// (global) --json : emit machine-readable JSON
220291
func cmdUpdate(args []string) {
292+
// Auto-update control surface: `pilotctl update status|enable|disable`.
293+
// Bare `pilotctl update` (or with --repo/--pin flags) runs a one-shot
294+
// manual update, which works regardless of the auto-update setting.
295+
if len(args) >= 1 {
296+
switch args[0] {
297+
case "status":
298+
cmdAutoUpdateStatus()
299+
return
300+
case "enable", "on":
301+
cmdAutoUpdateSet(true)
302+
return
303+
case "disable", "off":
304+
cmdAutoUpdateSet(false)
305+
return
306+
}
307+
}
221308
flags, _ := parseFlags(args)
222309
repo := flagString(flags, "repo", "pilot-protocol/pilotprotocol")
223310
pin := flagString(flags, "pin", "")

cmd/pilotctl/zz_autoupdate_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
package main
4+
5+
import (
6+
"os"
7+
"testing"
8+
)
9+
10+
// TestAutoUpdateControl pins the enable/disable/default-off control surface.
11+
func TestAutoUpdateControl(t *testing.T) {
12+
t.Setenv("HOME", t.TempDir()) // configDir() -> $HOME/.pilot
13+
14+
if autoUpdateEnabled() {
15+
t.Fatal("auto-update must be OFF by default (no state file)")
16+
}
17+
18+
prev := jsonOutput
19+
defer func() { jsonOutput = prev }()
20+
jsonOutput = true
21+
22+
_ = captureStdout(t, func() { cmdAutoUpdateSet(true) })
23+
if !autoUpdateEnabled() {
24+
t.Fatal("enable did not persist")
25+
}
26+
if _, err := os.Stat(autoUpdateStatePath()); err != nil {
27+
t.Fatalf("state file not written: %v", err)
28+
}
29+
30+
_ = captureStdout(t, func() { cmdAutoUpdateSet(false) })
31+
if autoUpdateEnabled() {
32+
t.Fatal("disable did not persist")
33+
}
34+
}

cmd/updater/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ import (
1616

1717
var version = "dev"
1818

19+
// defaultStatePath returns the auto-update control file, matching pilotctl's
20+
// ~/.pilot/auto-update.json so `pilotctl update enable/disable` and this loop
21+
// share one source of truth. Empty if the home dir can't be resolved (the
22+
// updater then treats auto-update as disabled — opt-in).
23+
func defaultStatePath() string {
24+
home, err := os.UserHomeDir()
25+
if err != nil || home == "" {
26+
return ""
27+
}
28+
return home + "/.pilot/auto-update.json"
29+
}
30+
1931
func main() {
2032
installDir := flag.String("install-dir", "", "directory containing pilot binaries (required)")
2133
repo := flag.String("repo", "pilot-protocol/pilotprotocol", "GitHub owner/repo for releases")
@@ -24,6 +36,7 @@ func main() {
2436
logLevel := flag.String("log-level", "info", "log level (debug, info, warn, error)")
2537
logFormat := flag.String("log-format", "text", "log format (text, json)")
2638
showVersion := flag.Bool("version", false, "print version and exit")
39+
statePath := flag.String("state-path", defaultStatePath(), "JSON control file {\"enabled\":bool} for automatic updates; auto-update is OFF until enabled (e.g. via `pilotctl update enable`)")
2740
flag.Parse()
2841

2942
if *showVersion {
@@ -44,6 +57,7 @@ func main() {
4457
InstallDir: *installDir,
4558
Version: version,
4659
PinnedVersion: *pin,
60+
StatePath: *statePath,
4761
})
4862

4963
u.Start()

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/pilot-protocol/runtime v0.3.1
1717
github.com/pilot-protocol/skillinject v0.2.3
1818
github.com/pilot-protocol/trustedagents v0.2.3
19-
github.com/pilot-protocol/updater v0.2.2-0.20260616131353-92a3a30a235e
19+
github.com/pilot-protocol/updater v0.2.2
2020
github.com/pilot-protocol/webhook v0.2.0
2121
golang.org/x/sys v0.46.0
2222
)

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260616142430-8edfed7efa72
88
github.com/pilot-protocol/app-store v1.0.1-beta.1.0.20260616142430-8edfed7efa72/go.mod h1:leZPtX43gE2JB7xeljexXri81g6qhdZfYExLtzI+bhg=
99
github.com/pilot-protocol/beacon v0.2.6 h1:grxwaVyPRUT0W6coyjYfNkO0rpzOIrwrKn94S21DuVE=
1010
github.com/pilot-protocol/beacon v0.2.6/go.mod h1:I/UhEv097g1z/qtAVDZbEhf3R5tzM0Dp71vGHah52A4=
11-
github.com/pilot-protocol/common v0.5.3 h1:CsBBmzuQn75G1MKVvKdLp77G9nf6fC7YGLZh8DVeZEI=
12-
github.com/pilot-protocol/common v0.5.3/go.mod h1:yrAwPXGVMbXU+SADvOCmbdXjK/wJ3uA0KshyLvRlej4=
1311
github.com/pilot-protocol/common v0.5.5 h1:mnv3q84alVaotGD+Qxfo4ECFEquqsUwrI3mjKIGUKFY=
1412
github.com/pilot-protocol/common v0.5.5/go.mod h1:yrAwPXGVMbXU+SADvOCmbdXjK/wJ3uA0KshyLvRlej4=
1513
github.com/pilot-protocol/dataexchange v0.2.1-beta.1.0.20260615113607-fac933edea98 h1:Bqgnf4CZC7aZJyDzz/E7agwXotArJg2FvFlNDqouhLo=
@@ -30,8 +28,8 @@ github.com/pilot-protocol/skillinject v0.2.3 h1:Bf0tqRe7tqYY27X5RGCOf4LGjtWpyQvN
3028
github.com/pilot-protocol/skillinject v0.2.3/go.mod h1:fCzivA/bjkXRgGjp6yd7nqfaIETtU+lQRocBu0J/O9g=
3129
github.com/pilot-protocol/trustedagents v0.2.3 h1:QQJHYqzPrECJwkCev0xIDBMjd92uhtcxcCMc2aOrRHc=
3230
github.com/pilot-protocol/trustedagents v0.2.3/go.mod h1:gDgEOC9lHmXSS9v45h80XxlmUS861soIrA0AsbXiSV4=
33-
github.com/pilot-protocol/updater v0.2.2-0.20260616131353-92a3a30a235e h1:vFzuw5dUVi0igwI2PdVzDY8OnY6FDLzM05wzI75zUZ8=
34-
github.com/pilot-protocol/updater v0.2.2-0.20260616131353-92a3a30a235e/go.mod h1:/I0uhVk1SljAOEYmjTdI/6CP7UmemmV4WB22ai1FxUw=
31+
github.com/pilot-protocol/updater v0.2.2 h1:uA+Gmbs3/sMoumtjwCMXUHo3TAKg51VRch88m0wUtZA=
32+
github.com/pilot-protocol/updater v0.2.2/go.mod h1:wn+HkjgChZ1QCCkOHBolAol42mxyyW/iz7oOFJLciT4=
3533
github.com/pilot-protocol/webhook v0.2.0 h1:3UFU9X2yBb0iKlPbzVcism+Z6yCrBBaOgdo9+vd4Wf4=
3634
github.com/pilot-protocol/webhook v0.2.0/go.mod h1:WVXhHFg+o0pHHk+4nXMCh1zl/ZAyZ3AXrtx6mNuZS6g=
3735
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=

0 commit comments

Comments
 (0)