Files
sanspie d436311f69 Stage 11i4: signed self-updates for Aego and exported games (D362)
update core: signed release.json (Ed25519) per channel, ETag check, resumable download with sha256, Auto/Ask/Off policy, appliers for the install.sh layout, AppImage, NSIS installer (keeps the replaced setup.exe for Restore) and .app, trial run with rollback.
Editor and launcher: banner, status bar, Help commands, Task Center download, install on restart.
CLI: aego update, aego release keygen|manifest, aego export --release-json.
Games: modules/update with update { url publicKey }, settings and the aego/update script API.
release.sh refuses to publish without AEGO_RELEASE_KEY and AEGO_UPDATE_KEY; install.sh lays out versions; make test-update runs a real v1 -> v2 update.
2026-10-26 19:36:15 +03:00

164 lines
5.1 KiB
Go

// Package prefs holds the editor preferences of the user, shared by every project and the launcher (D345).
package prefs
import (
"os"
"path/filepath"
"time"
"aego/core/errs"
"aego/editor/core/agentui"
"aego/editor/core/document"
"aego/engine/i18n"
"aego/format/aetext"
"aego/ui/theme"
"aego/update"
)
const (
File = "prefs.aetext"
version = 1
)
type Prefs struct {
Theme theme.Mode
// Language is auto, en or ru; auto follows the OS (D348).
Language string
// AgentFollow is the Follow button of the Agent panel; AgentResume is how many idle seconds resume following, 0 never (11b5b).
AgentFollow bool
AgentResume float64
// AgentBudgets cap what the agent's read tools return.
AgentBudgets agentui.Budgets
// ProxyConfirm makes changes from the CLI and outside agents wait in the Agent panel (editor.proxy.confirm).
ProxyConfirm bool
// Updates is how the editor updates itself: mode, channel, skipped versions and the last check (11i4).
Updates update.Policy
}
func Default() *Prefs {
return &Prefs{Theme: theme.ModeSystem, Language: i18n.Auto, AgentResume: 30, AgentBudgets: agentui.DefaultBudgets,
Updates: update.Policy{Mode: update.Auto, Channel: update.Stable}}
}
func Load(dir string) (*Prefs, error) {
p := Default()
if dir == "" {
return p, nil
}
data, err := os.ReadFile(filepath.Join(dir, File))
if os.IsNotExist(err) {
return p, nil
}
if err != nil {
return p, errs.Wrap(err, errs.EditorDocument, "cannot read the editor preferences")
}
doc, err := aetext.Parse(data, File)
if err != nil || doc.Head != "prefs" {
return p, err
}
if v, ok := doc.Field("theme"); ok {
name := v.Str
if name == "" {
name = v.Ident
}
p.Theme = theme.ParseMode(name)
}
if v, ok := doc.Field("language"); ok {
name := v.Str
if name == "" {
name = v.Ident
}
if name != "" {
p.Language = name
}
}
if v, ok := doc.Field("agentFollow"); ok {
p.AgentFollow = v.Bool
}
if v, ok := doc.Field("proxyConfirm"); ok {
p.ProxyConfirm = v.Bool
}
if u := doc.Block("updates", ""); u != nil {
p.Updates = readUpdates(u)
}
for _, f := range []struct {
name string
dst *int
}{{"agentTreeNodes", &p.AgentBudgets.TreeNodes}, {"agentTreeDepth", &p.AgentBudgets.TreeDepth}, {"agentDocResults", &p.AgentBudgets.DocResults}, {"agentScriptLines", &p.AgentBudgets.ScriptLines}} {
if v, ok := doc.Field(f.name); ok && v.Kind == aetext.VInt {
*f.dst = max(0, int(v.Int))
}
}
if v, ok := doc.Field("agentResume"); ok {
switch v.Kind {
case aetext.VFloat:
p.AgentResume = max(0, v.Float)
case aetext.VInt:
p.AgentResume = max(0, float64(v.Int))
}
}
return p, nil
}
func Save(dir string, p *Prefs) error {
if dir == "" {
return nil
}
root := &aetext.Node{Head: "prefs", Attrs: []aetext.Field{{Name: "version", Value: aetext.Int(version)}}}
root.Fields = append(root.Fields, aetext.Field{Name: "theme", Value: aetext.Ident(p.Theme.String())})
lang := p.Language
if lang == "" {
lang = i18n.Auto
}
root.Fields = append(root.Fields, aetext.Field{Name: "language", Value: aetext.Ident(lang)})
root.Fields = append(root.Fields, aetext.Field{Name: "agentFollow", Value: aetext.Bool(p.AgentFollow)})
root.Fields = append(root.Fields, aetext.Field{Name: "proxyConfirm", Value: aetext.Bool(p.ProxyConfirm)})
root.Blocks = append(root.Blocks, writeUpdates(p.Updates))
root.Fields = append(root.Fields, aetext.Field{Name: "agentResume", Value: aetext.Float64(p.AgentResume)})
b := p.AgentBudgets
root.Fields = append(root.Fields,
aetext.Field{Name: "agentTreeNodes", Value: aetext.Int(int64(b.TreeNodes))},
aetext.Field{Name: "agentTreeDepth", Value: aetext.Int(int64(b.TreeDepth))},
aetext.Field{Name: "agentDocResults", Value: aetext.Int(int64(b.DocResults))},
aetext.Field{Name: "agentScriptLines", Value: aetext.Int(int64(b.ScriptLines))})
return document.WriteAtomic(filepath.Join(dir, File), aetext.Write(root))
}
func readUpdates(n *aetext.Node) update.Policy {
pol := update.Policy{Mode: update.Auto, Channel: update.Stable}
if v, ok := n.Field("mode"); ok {
pol.Mode = update.ParseMode(v.Ident + v.Str)
}
if v, ok := n.Field("channel"); ok {
pol.Channel = update.ParseChannel(v.Ident + v.Str)
}
if v, ok := n.Field("skipped"); ok {
for _, e := range v.List {
if e.Str != "" {
pol.Skipped = append(pol.Skipped, e.Str)
}
}
}
if v, ok := n.Field("checked"); ok {
pol.LastCheck, _ = time.Parse(time.RFC3339, v.Str)
}
return pol
}
func writeUpdates(pol update.Policy) *aetext.Node {
n := &aetext.Node{Head: "updates"}
pol.Mode, pol.Channel = update.ParseMode(string(pol.Mode)), update.ParseChannel(string(pol.Channel))
skipped := make([]aetext.Value, 0, len(pol.Skipped))
for _, s := range pol.Skipped {
skipped = append(skipped, aetext.String(s))
}
n.Fields = append(n.Fields,
aetext.Field{Name: "mode", Value: aetext.Ident(string(pol.Mode))},
aetext.Field{Name: "channel", Value: aetext.Ident(string(pol.Channel))},
aetext.Field{Name: "skipped", Value: aetext.List(skipped...)})
if !pol.LastCheck.IsZero() {
n.Fields = append(n.Fields, aetext.Field{Name: "checked", Value: aetext.String(pol.LastCheck.UTC().Format(time.RFC3339))})
}
return n
}