From d77ca4c384016f85f843b8af739dfacbadf0dda7 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Sun, 29 Jun 2025 21:26:00 +0300 Subject: [PATCH] feat: config command --- assets/coverage-badge.svg | 4 +- config.go | 227 ++++++++++++++++++++++ e2e-tests/addrepo_test.go | 2 +- e2e-tests/issue_95_config_command_test.go | 47 +++++ go.mod | 25 ++- go.sum | 62 ++++-- internal/config/config.go | 162 ++++++--------- internal/config/env_config.go | 76 ++++++++ internal/config/paths.go | 11 +- internal/config/system_config.go | 144 ++++++++++++++ internal/translations/default.pot | 48 ++++- internal/translations/po/ru/default.po | 53 ++++- main.go | 1 + pkg/types/config.go | 22 +-- repo.go | 14 +- 15 files changed, 738 insertions(+), 160 deletions(-) create mode 100644 config.go create mode 100644 e2e-tests/issue_95_config_command_test.go create mode 100644 internal/config/env_config.go create mode 100644 internal/config/system_config.go diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index 4257307..2a6facc 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.8% - 19.8% + 19.2% + 19.2% diff --git a/config.go b/config.go new file mode 100644 index 0000000..9eb0713 --- /dev/null +++ b/config.go @@ -0,0 +1,227 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "fmt" + "strconv" + "strings" + + "github.com/goccy/go-yaml" + "github.com/leonelquinteros/gotext" + "github.com/urfave/cli/v2" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" + appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" +) + +func ConfigCmd() *cli.Command { + return &cli.Command{ + Name: "config", + Usage: gotext.Get("Manage config"), + Subcommands: []*cli.Command{ + ShowCmd(), + SetConfig(), + GetConfig(), + }, + } +} + +func ShowCmd() *cli.Command { + return &cli.Command{ + Name: "show", + Usage: gotext.Get("Show config"), + BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { + return nil + }), + Action: func(c *cli.Context) error { + deps, err := appbuilder. + New(c.Context). + WithConfig(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + content, err := deps.Cfg.ToYAML() + if err != nil { + return err + } + fmt.Println(content) + return nil + }, + } +} + +var configKeys = []string{ + "rootCmd", + "useRootCmd", + "pagerStyle", + "autoPull", + "logLevel", + "ignorePkgUpdates", +} + +func SetConfig() *cli.Command { + return &cli.Command{ + Name: "set", + Usage: gotext.Get("Set config value"), + ArgsUsage: gotext.Get(" "), + BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { + if c.Args().Len() == 0 { + for _, key := range configKeys { + fmt.Println(key) + } + return nil + } + return nil + }), + Action: utils.RootNeededAction(func(c *cli.Context) error { + if c.Args().Len() < 2 { + return cliutils.FormatCliExit("missing args", nil) + } + + key := c.Args().Get(0) + value := c.Args().Get(1) + + deps, err := appbuilder. + New(c.Context). + WithConfig(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + switch key { + case "rootCmd": + deps.Cfg.System.SetRootCmd(value) + case "useRootCmd": + boolValue, err := strconv.ParseBool(value) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err) + } + deps.Cfg.System.SetUseRootCmd(boolValue) + case "pagerStyle": + deps.Cfg.System.SetPagerStyle(value) + case "autoPull": + boolValue, err := strconv.ParseBool(value) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err) + } + deps.Cfg.System.SetAutoPull(boolValue) + case "logLevel": + deps.Cfg.System.SetLogLevel(value) + case "ignorePkgUpdates": + var updates []string + if value != "" { + updates = strings.Split(value, ",") + for i, update := range updates { + updates[i] = strings.TrimSpace(update) + } + } + deps.Cfg.System.SetIgnorePkgUpdates(updates) + case "repo", "repos": + return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil) + default: + return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil) + } + + if err := deps.Cfg.System.Save(); err != nil { + return cliutils.FormatCliExit(gotext.Get("failed to save config"), err) + } + + fmt.Println(gotext.Get("Successfully set %s = %s", key, value)) + return nil + }), + } +} + +func GetConfig() *cli.Command { + return &cli.Command{ + Name: "get", + Usage: gotext.Get("Get config value"), + ArgsUsage: gotext.Get(""), + BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { + if c.Args().Len() == 0 { + for _, key := range configKeys { + fmt.Println(key) + } + return nil + } + return nil + }), + Action: func(c *cli.Context) error { + deps, err := appbuilder. + New(c.Context). + WithConfig(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + if c.Args().Len() == 0 { + content, err := deps.Cfg.ToYAML() + if err != nil { + return cliutils.FormatCliExit("failed to serialize config", err) + } + fmt.Print(content) + return nil + } + + key := c.Args().Get(0) + + switch key { + case "rootCmd": + fmt.Println(deps.Cfg.RootCmd()) + case "useRootCmd": + fmt.Println(deps.Cfg.UseRootCmd()) + case "pagerStyle": + fmt.Println(deps.Cfg.PagerStyle()) + case "autoPull": + fmt.Println(deps.Cfg.AutoPull()) + case "logLevel": + fmt.Println(deps.Cfg.LogLevel()) + case "ignorePkgUpdates": + updates := deps.Cfg.IgnorePkgUpdates() + if len(updates) == 0 { + fmt.Println("[]") + } else { + fmt.Println(strings.Join(updates, ", ")) + } + case "repo", "repos": + repos := deps.Cfg.Repos() + if len(repos) == 0 { + fmt.Println("[]") + } else { + repoData, err := yaml.Marshal(repos) + if err != nil { + return cliutils.FormatCliExit("failed to serialize repos", err) + } + fmt.Print(string(repoData)) + } + default: + return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil) + } + + return nil + }, + } +} diff --git a/e2e-tests/addrepo_test.go b/e2e-tests/addrepo_test.go index d0404e2..065b92b 100644 --- a/e2e-tests/addrepo_test.go +++ b/e2e-tests/addrepo_test.go @@ -66,7 +66,7 @@ func TestE2EAlrAddRepo(t *testing.T) { "cat /etc/alr/alr.toml", ), e2e.WithExecOptionStdout(&buf)) assert.NoError(t, err) - assert.Contains(t, buf.String(), "rootCmd") + assert.Contains(t, buf.String(), "repo = []") }, ) } diff --git a/e2e-tests/issue_95_config_command_test.go b/e2e-tests/issue_95_config_command_test.go new file mode 100644 index 0000000..c45fd23 --- /dev/null +++ b/e2e-tests/issue_95_config_command_test.go @@ -0,0 +1,47 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//go:build e2e + +package e2etests_test + +import ( + "testing" + + "github.com/efficientgo/e2e" +) + +func TestE2EIssue95ConfigCommand(t *testing.T) { + dockerMultipleRun( + t, + "issue-95-config-command", + COMMON_SYSTEMS, + func(t *testing.T, r e2e.Runnable) { + defaultPrepare(t, r) + execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: true\"") + execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: true\"") + execShouldError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull\"") + execShouldNoError(t, r, "alr", "config", "get", "autoPull") + execShouldError(t, r, "alr", "config", "set", "autoPull") + execShouldNoError(t, r, "sudo", "alr", "config", "set", "autoPull", "false") + execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: false\"") + execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: false\"") + execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = false\"") + execShouldNoError(t, r, "alr", "config", "set", "autoPull", "true") + execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = true\"") + }, + ) +} diff --git a/go.mod b/go.mod index 52c4f46..661010e 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/reflow v0.3.0 - github.com/pelletier/go-toml/v2 v2.1.0 + github.com/pelletier/go-toml/v2 v2.2.4 github.com/stretchr/testify v1.10.0 github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 github.com/urfave/cli/v2 v2.25.7 @@ -37,7 +37,7 @@ require ( go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 golang.org/x/crypto v0.36.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/sys v0.31.0 + golang.org/x/sys v0.33.0 golang.org/x/text v0.23.0 modernc.org/sqlite v1.25.0 mvdan.cc/sh/v3 v3.10.0 @@ -75,8 +75,11 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.7.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.8.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -84,7 +87,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/goreleaser/chglog v0.6.1 // indirect github.com/goreleaser/fileglob v1.3.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect @@ -98,6 +101,15 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/parsers/json v1.0.0 // indirect + github.com/knadh/koanf/parsers/toml/v2 v2.2.0 // indirect + github.com/knadh/koanf/providers/confmap v1.0.0 // indirect + github.com/knadh/koanf/providers/env v1.1.0 // indirect + github.com/knadh/koanf/providers/file v1.2.0 // indirect + github.com/knadh/koanf/providers/rawbytes v1.0.0 // indirect + github.com/knadh/koanf/providers/structs v1.0.0 // indirect + github.com/knadh/koanf/v2 v2.2.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -121,7 +133,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/skeema/knownhosts v1.3.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/ulikunitz/xz v0.5.12 // indirect @@ -132,11 +144,12 @@ require ( go4.org v0.0.0-20200411211856-f5505b9728dd // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/grpc v1.58.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect + google.golang.org/grpc v1.67.3 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 61b5bb2..b414072 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTli github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= @@ -139,9 +139,13 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -160,6 +164,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= @@ -212,8 +218,8 @@ github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -273,6 +279,24 @@ github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o= +github.com/knadh/koanf/parsers/json v1.0.0/go.mod h1:zb5WtibRdpxSoSJfXysqGbVxvbszdlroWDHGdDkkEYU= +github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A= +github.com/knadh/koanf/parsers/toml/v2 v2.2.0/go.mod h1:JpjTeK1Ge1hVX0wbof5DMCuDBriR8bWgeQP98eeOZpI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= +github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= +github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= +github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/zsJyc/8qaSnzcdNI= +github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo= +github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4= +github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w= +github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE= +github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -341,8 +365,10 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -388,19 +414,15 @@ github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -500,8 +522,8 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -541,6 +563,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -601,8 +625,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -616,8 +638,8 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -625,8 +647,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= diff --git a/internal/config/config.go b/internal/config/config.go index b62ca2d..902b07d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,13 +20,12 @@ package config import ( - "log/slog" - "os" + "fmt" "path/filepath" - "reflect" - "github.com/caarlos0/env" - "github.com/pelletier/go-toml/v2" + "github.com/goccy/go-yaml" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/v2" "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" @@ -35,74 +34,65 @@ import ( type ALRConfig struct { cfg *types.Config paths *Paths -} -var defaultConfig = &types.Config{ - RootCmd: "sudo", - UseRootCmd: true, - PagerStyle: "native", - IgnorePkgUpdates: []string{}, - AutoPull: true, - Repos: []types.Repo{}, + System *SystemConfig + env *EnvConfig } func New() *ALRConfig { - return &ALRConfig{} + return &ALRConfig{ + System: NewSystemConfig(), + env: NewEnvConfig(), + } } -func readConfig(path string) (*types.Config, error) { - file, err := os.Open(path) - if err != nil { - return nil, err +func defaultConfigKoanf() *koanf.Koanf { + k := koanf.New(".") + defaults := map[string]interface{}{ + "rootCmd": "sudo", + "useRootCmd": true, + "pagerStyle": "native", + "ignorePkgUpdates": []string{}, + "logLevel": "info", + "autoPull": true, + "repos": []types.Repo{}, } - defer file.Close() - - config := types.Config{} - - if err := toml.NewDecoder(file).Decode(&config); err != nil { - return nil, err - } - - return &config, nil -} - -func mergeStructs(dst, src interface{}) { - srcVal := reflect.ValueOf(src) - if srcVal.IsNil() { - return - } - srcVal = srcVal.Elem() - dstVal := reflect.ValueOf(dst).Elem() - - for i := range srcVal.NumField() { - srcField := srcVal.Field(i) - srcFieldName := srcVal.Type().Field(i).Name - - dstField := dstVal.FieldByName(srcFieldName) - if dstField.IsValid() && dstField.CanSet() { - dstField.Set(srcField) - } + if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { + panic(k) } + return k } func (c *ALRConfig) Load() error { - systemConfig, err := readConfig( - constants.SystemConfigPath, - ) - if err != nil { - slog.Debug("Cannot read system config", "err", err) + config := types.Config{} + + merged := koanf.New(".") + + if err := c.System.Load(); err != nil { + return fmt.Errorf("failed to load system config: %w", err) } - config := &types.Config{} - - mergeStructs(config, defaultConfig) - mergeStructs(config, systemConfig) - err = env.Parse(config) - if err != nil { - return err + if err := c.env.Load(); err != nil { + return fmt.Errorf("failed to load env config: %w", err) } - c.cfg = config + systemK := c.System.koanf() + envK := c.env.koanf() + + if err := merged.Merge(defaultConfigKoanf()); err != nil { + return fmt.Errorf("failed to merge default config: %w", err) + } + if err := merged.Merge(systemK); err != nil { + return fmt.Errorf("failed to merge system config: %w", err) + } + if err := merged.Merge(envK); err != nil { + return fmt.Errorf("failed to merge env config: %w", err) + } + if err := merged.Unmarshal("", &config); err != nil { + return fmt.Errorf("failed to unmarshal merged config: %w", err) + } + + c.cfg = &config c.paths = &Paths{} c.paths.UserConfigPath = constants.SystemConfigPath @@ -110,52 +100,24 @@ func (c *ALRConfig) Load() error { c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") - // c.initPaths() return nil } -func (c *ALRConfig) RootCmd() string { - return c.cfg.RootCmd -} - -func (c *ALRConfig) PagerStyle() string { - return c.cfg.PagerStyle -} - -func (c *ALRConfig) AutoPull() bool { - return c.cfg.AutoPull -} - -func (c *ALRConfig) Repos() []types.Repo { - return c.cfg.Repos -} - -func (c *ALRConfig) SetRepos(repos []types.Repo) { - c.cfg.Repos = repos -} - -func (c *ALRConfig) IgnorePkgUpdates() []string { - return c.cfg.IgnorePkgUpdates -} - -func (c *ALRConfig) LogLevel() string { - return c.cfg.LogLevel -} - -func (c *ALRConfig) UseRootCmd() bool { - return c.cfg.UseRootCmd -} - -func (c *ALRConfig) GetPaths() *Paths { - return c.paths -} - -func (c *ALRConfig) SaveUserConfig() error { - f, err := os.Create(c.paths.UserConfigPath) +func (c *ALRConfig) ToYAML() (string, error) { + data, err := yaml.Marshal(c.cfg) if err != nil { - return err + return "", err } - - return toml.NewEncoder(f).Encode(c.cfg) + return string(data), nil } + +func (c *ALRConfig) RootCmd() string { return c.cfg.RootCmd } +func (c *ALRConfig) PagerStyle() string { return c.cfg.PagerStyle } +func (c *ALRConfig) AutoPull() bool { return c.cfg.AutoPull } +func (c *ALRConfig) Repos() []types.Repo { return c.cfg.Repos } +func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) } +func (c *ALRConfig) IgnorePkgUpdates() []string { return c.cfg.IgnorePkgUpdates } +func (c *ALRConfig) LogLevel() string { return c.cfg.LogLevel } +func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd } +func (c *ALRConfig) GetPaths() *Paths { return c.paths } diff --git a/internal/config/env_config.go b/internal/config/env_config.go new file mode 100644 index 0000000..7255d5f --- /dev/null +++ b/internal/config/env_config.go @@ -0,0 +1,76 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "strings" + + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/v2" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type EnvConfig struct { + k *koanf.Koanf +} + +func NewEnvConfig() *EnvConfig { + return &EnvConfig{ + k: koanf.New("."), + } +} + +func (c *EnvConfig) koanf() *koanf.Koanf { + return c.k +} + +func (c *EnvConfig) Load() error { + allowedKeys := map[string]struct{}{ + "ALR_LOG_LEVEL": {}, + "ALR_PAGER_STYLE": {}, + "ALR_AUTO_PULL": {}, + } + err := c.k.Load(env.Provider("ALR_", ".", func(s string) string { + _, ok := allowedKeys[s] + if !ok { + return "" + } + withoutPrefix := strings.TrimPrefix(s, "ALR_") + lowered := strings.ToLower(withoutPrefix) + dotted := strings.ReplaceAll(lowered, "__", ".") + parts := strings.Split(dotted, ".") + for i, part := range parts { + if strings.Contains(part, "_") { + parts[i] = toCamelCase(part) + } + } + return strings.Join(parts, ".") + }), nil) + + return err +} + +func toCamelCase(s string) string { + parts := strings.Split(s, "_") + for i := 1; i < len(parts); i++ { + if len(parts[i]) > 0 { + parts[i] = cases.Title(language.Und, cases.NoLower).String(parts[i]) + } + } + return strings.Join(parts, "") +} diff --git a/internal/config/paths.go b/internal/config/paths.go index b5a11bd..13ce051 100644 --- a/internal/config/paths.go +++ b/internal/config/paths.go @@ -21,9 +21,10 @@ package config // Paths contains various paths used by ALR type Paths struct { - UserConfigPath string - CacheDir string - RepoDir string - PkgsDir string - DBPath string + SystemConfigPath string + UserConfigPath string + CacheDir string + RepoDir string + PkgsDir string + DBPath string } diff --git a/internal/config/system_config.go b/internal/config/system_config.go new file mode 100644 index 0000000..393dd96 --- /dev/null +++ b/internal/config/system_config.go @@ -0,0 +1,144 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + ktoml "github.com/knadh/koanf/parsers/toml/v2" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" +) + +type SystemConfig struct { + k *koanf.Koanf + cfg *types.Config +} + +func NewSystemConfig() *SystemConfig { + return &SystemConfig{ + k: koanf.New("."), + cfg: &types.Config{}, + } +} + +func (c *SystemConfig) koanf() *koanf.Koanf { + return c.k +} + +func (c *SystemConfig) Load() error { + if _, err := os.Stat(constants.SystemConfigPath); errors.Is(err, os.ErrNotExist) { + return nil + } + + if err := c.k.Load(file.Provider(constants.SystemConfigPath), ktoml.Parser()); err != nil { + return err + } + + return c.k.Unmarshal("", c.cfg) +} + +func (c *SystemConfig) Save() error { + bytes, err := c.k.Marshal(ktoml.Parser()) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + file, err := os.Create(constants.SystemConfigPath) + if err != nil { + return fmt.Errorf("failed to create config file: %w", err) + } + defer func() { + if cerr := file.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + if _, err := file.Write(bytes); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + if err := file.Sync(); err != nil { + return fmt.Errorf("failed to sync config: %w", err) + } + + return nil +} + +func (c *SystemConfig) SetRootCmd(v string) { + err := c.k.Set("rootCmd", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetUseRootCmd(v bool) { + err := c.k.Set("useRootCmd", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetPagerStyle(v string) { + err := c.k.Set("pagerStyle", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetIgnorePkgUpdates(v []string) { + err := c.k.Set("ignorePkgUpdates", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetAutoPull(v bool) { + err := c.k.Set("autoPull", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetLogLevel(v string) { + err := c.k.Set("logLevel", v) + if err != nil { + panic(err) + } +} + +func (c *SystemConfig) SetRepos(v []types.Repo) { + b, err := json.Marshal(v) + if err != nil { + panic(err) + } + var m []interface{} + err = json.Unmarshal(b, &m) + if err != nil { + panic(err) + } + err = c.k.Set("repo", m) + if err != nil { + panic(err) + } +} diff --git a/internal/translations/default.pot b/internal/translations/default.pot index deb25fe..6e04003 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -58,6 +58,50 @@ msgstr "" msgid "Done" msgstr "" +#: config.go:36 +msgid "Manage config" +msgstr "" + +#: config.go:48 +msgid "Show config" +msgstr "" + +#: config.go:84 +msgid "Set config value" +msgstr "" + +#: config.go:85 +msgid " " +msgstr "" + +#: config.go:118 config.go:126 +msgid "invalid boolean value for %s: %s" +msgstr "" + +#: config.go:141 +msgid "use 'repo add/remove' commands to manage repositories" +msgstr "" + +#: config.go:143 config.go:221 +msgid "unknown config key: %s" +msgstr "" + +#: config.go:147 +msgid "failed to save config" +msgstr "" + +#: config.go:150 +msgid "Successfully set %s = %s" +msgstr "" + +#: config.go:159 +msgid "Get config value" +msgstr "" + +#: config.go:160 +msgid "" +msgstr "" + #: fix.go:39 msgid "Attempt to fix problems with ALR" msgstr "" @@ -437,11 +481,11 @@ msgstr "" msgid "Enable interactive questions and prompts" msgstr "" -#: main.go:146 +#: main.go:147 msgid "Show help" msgstr "" -#: main.go:150 +#: main.go:151 msgid "Error while running app" msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index 69d242f..866e41d 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: unnamed project\n" -"PO-Revision-Date: 2025-06-19 18:54+0300\n" +"PO-Revision-Date: 2025-06-29 21:05+0300\n" "Last-Translator: Maxim Slipenko \n" "Language-Team: Russian\n" "Language: ru\n" @@ -65,6 +65,50 @@ msgstr "Ошибка при перемещении пакета" msgid "Done" msgstr "Сделано" +#: config.go:36 +msgid "Manage config" +msgstr "Управление конфигурацией" + +#: config.go:48 +msgid "Show config" +msgstr "Показать конфигурацию" + +#: config.go:84 +msgid "Set config value" +msgstr "Установить значение в конфигурации" + +#: config.go:85 +msgid " " +msgstr "<ключ> <значение>" + +#: config.go:118 config.go:126 +msgid "invalid boolean value for %s: %s" +msgstr "неверное булево значение для %s: %s" + +#: config.go:141 +msgid "use 'repo add/remove' commands to manage repositories" +msgstr "используйте команды 'repo add/remove' для управления репозиториями" + +#: config.go:143 config.go:221 +msgid "unknown config key: %s" +msgstr "неизвестный ключ конфигурации: %s" + +#: config.go:147 +msgid "failed to save config" +msgstr "не удалось сохранить конфигурацию" + +#: config.go:150 +msgid "Successfully set %s = %s" +msgstr "Успешно установлено %s = %s" + +#: config.go:159 +msgid "Get config value" +msgstr "Получить значение из конфигурации" + +#: config.go:160 +msgid "" +msgstr "<ключ>" + #: fix.go:39 msgid "Attempt to fix problems with ALR" msgstr "Попытка устранить проблемы с ALR" @@ -453,11 +497,11 @@ msgstr "Аргументы, которые будут переданы мене msgid "Enable interactive questions and prompts" msgstr "Включение интерактивных вопросов и запросов" -#: main.go:146 +#: main.go:147 msgid "Show help" msgstr "Показать справку" -#: main.go:150 +#: main.go:151 msgid "Error while running app" msgstr "Ошибка при запуске приложения" @@ -683,9 +727,6 @@ msgstr "Здесь нечего делать." #~ msgid "Unable to detect user config directory" #~ msgstr "Не удалось обнаружить каталог конфигурации пользователя" -#~ msgid "Unable to create ALR config file" -#~ msgstr "Не удалось создать конфигурационный файл ALR" - #~ msgid "Error encoding default configuration" #~ msgstr "Ошибка кодирования конфигурации по умолчанию" diff --git a/main.go b/main.go index 106c459..af84885 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,7 @@ func GetApp() *cli.App { VersionCmd(), SearchCmd(), RepoCmd(), + ConfigCmd(), // Internal commands InternalBuildCmd(), InternalInstallCmd(), diff --git a/pkg/types/config.go b/pkg/types/config.go index 617df87..f11cc68 100644 --- a/pkg/types/config.go +++ b/pkg/types/config.go @@ -21,19 +21,19 @@ package types // Config represents the ALR configuration file type Config struct { - RootCmd string `toml:"rootCmd" env:"ALR_ROOT_CMD"` - UseRootCmd bool `toml:"useRootCmd"` - PagerStyle string `toml:"pagerStyle" env:"ALR_PAGER_STYLE"` - IgnorePkgUpdates []string `toml:"ignorePkgUpdates"` - Repos []Repo `toml:"repo"` - AutoPull bool `toml:"autoPull" env:"ALR_AUTOPULL"` - LogLevel string `toml:"logLevel" env:"ALR_LOG_LEVEL"` + RootCmd string `json:"rootCmd" koanf:"rootCmd"` + UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"` + PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"` + IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"` + Repos []Repo `json:"repo" koanf:"repo"` + AutoPull bool `json:"autoPull" koanf:"autoPull"` + LogLevel string `json:"logLevel" koanf:"logLevel"` } // Repo represents a ALR repo within a configuration file type Repo struct { - Name string `toml:"name"` - URL string `toml:"url"` - Ref string `toml:"ref"` - Mirrors []string `toml:"mirrors"` + Name string `json:"name" koanf:"name"` + URL string `json:"url" koanf:"url"` + Ref string `json:"ref" koanf:"ref"` + Mirrors []string `json:"mirrors" koanf:"mirrors"` } diff --git a/repo.go b/repo.go index a1d7ea2..7fc9e4b 100644 --- a/repo.go +++ b/repo.go @@ -108,7 +108,7 @@ func RemoveRepoCmd() *cli.Command { if err != nil { return cliutils.FormatCliExit(gotext.Get("Error removing repo directory"), err) } - err = cfg.SaveUserConfig() + err = cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -175,7 +175,7 @@ func AddRepoCmd() *cli.Command { }) cfg.SetRepos(reposSlice) - err = cfg.SaveUserConfig() + err = cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -248,7 +248,7 @@ func SetRepoRefCmd() *cli.Command { newRepos = append(newRepos, repo) } deps.Cfg.SetRepos(newRepos) - err = deps.Cfg.SaveUserConfig() + err = deps.Cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -311,7 +311,7 @@ func SetUrlCmd() *cli.Command { newRepos = append(newRepos, repo) } deps.Cfg.SetRepos(newRepos) - err = deps.Cfg.SaveUserConfig() + err = deps.Cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -384,7 +384,7 @@ func AddMirror() *cli.Command { } } deps.Cfg.SetRepos(repos) - err = deps.Cfg.SaveUserConfig() + err = deps.Cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -499,7 +499,7 @@ func RemoveMirror() *cli.Command { } deps.Cfg.SetRepos(reposSlice) - err = deps.Cfg.SaveUserConfig() + err = deps.Cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } @@ -571,7 +571,7 @@ func ClearMirrors() *cli.Command { reposSlice[repoIndex].Mirrors = []string{} deps.Cfg.SetRepos(reposSlice) - err = deps.Cfg.SaveUserConfig() + err = deps.Cfg.System.Save() if err != nil { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) }