feat(config): add ResetToDefaults and CLI config reset command
Export MakeBackup for external use, add ResetToDefaults function that backs up current config, creates defaults, and preserves security credentials. Add `picoclaw config reset` CLI command with --force flag.
This commit is contained in:
parent
cb5d33124c
commit
d61902d42a
4 changed files with 89 additions and 17 deletions
57
cmd/picoclaw/internal/config/command.go
Normal file
57
cmd/picoclaw/internal/config/command.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewConfigCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage configuration",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newResetCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResetCommand() *cobra.Command {
|
||||||
|
var force bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "reset",
|
||||||
|
Short: "Reset configuration to factory defaults",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Example: ` picoclaw config reset
|
||||||
|
picoclaw config reset --force`,
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
if !force {
|
||||||
|
fmt.Print("Reset config to factory defaults? API keys will be preserved. (y/n): ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||||
|
fmt.Println("Aborted.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := internal.GetConfigPath()
|
||||||
|
if err := config.ResetToDefaults(configPath); err != nil {
|
||||||
|
return fmt.Errorf("reset failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Configuration has been reset to factory defaults.")
|
||||||
|
fmt.Println("A backup of the previous config was created in the same directory.")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&force, "force", "f", false,
|
||||||
|
"Skip confirmation prompt")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
||||||
|
configcmd "github.com/sipeed/picoclaw/cmd/picoclaw/internal/config"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
||||||
|
|
@ -82,6 +83,7 @@ picoclaw --no-color status`,
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
|
configcmd.NewConfigCommand(),
|
||||||
onboard.NewOnboardCommand(),
|
onboard.NewOnboardCommand(),
|
||||||
agent.NewAgentCommand(),
|
agent.NewAgentCommand(),
|
||||||
auth.NewAuthCommand(),
|
auth.NewAuthCommand(),
|
||||||
|
|
|
||||||
|
|
@ -1239,7 +1239,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1293,7 +1293,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1345,7 +1345,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = makeBackup(path)
|
err = MakeBackup(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1496,7 +1496,7 @@ func applySkillsRegistryEnvCompat(cfg *Config) {
|
||||||
cfg.Tools.Skills.Registries.Set("github", githubCfg)
|
cfg.Tools.Skills.Registries.Set("github", githubCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBackup(path string) error {
|
func MakeBackup(path string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1624,6 +1624,19 @@ func (c *Config) SecurityCopyFrom(path string) error {
|
||||||
return loadSecurityConfig(c, securityPath(path))
|
return loadSecurityConfig(c, securityPath(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetToDefaults backs up the current config, creates a default config,
|
||||||
|
// preserves security credentials from the existing config, and saves it.
|
||||||
|
func ResetToDefaults(configPath string) error {
|
||||||
|
if err := MakeBackup(configPath); err != nil {
|
||||||
|
return fmt.Errorf("backup before reset: %w", err)
|
||||||
|
}
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if err := cfg.SecurityCopyFrom(configPath); err != nil {
|
||||||
|
logger.WarnF("could not preserve security config", map[string]any{"error": err})
|
||||||
|
}
|
||||||
|
return SaveConfig(configPath, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
|
||||||
var expanded []*ModelConfig
|
var expanded []*ModelConfig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2602,7 +2602,7 @@ func TestFilterSensitiveData_AllTokenTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// makeBackup tests
|
// MakeBackup tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// TestMakeBackup_WithDateSuffix verifies backup files include a date suffix.
|
// TestMakeBackup_WithDateSuffix verifies backup files include a date suffix.
|
||||||
|
|
@ -2613,8 +2613,8 @@ func TestMakeBackup_WithDateSuffix(t *testing.T) {
|
||||||
t.Fatalf("WriteFile: %v", err)
|
t.Fatalf("WriteFile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
|
|
@ -2653,8 +2653,8 @@ func TestMakeBackup_AlsoBacksSecurityFile(t *testing.T) {
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
os.WriteFile(secPath, []byte(`model_list:\n test:0:\n api_keys:\n - "sk-test"\n`), 0o600)
|
os.WriteFile(secPath, []byte(`model_list:\n test:0:\n api_keys:\n - "sk-test"\n`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
|
|
@ -2680,14 +2680,14 @@ func TestMakeBackup_AlsoBacksSecurityFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMakeBackup_NonexistentFileSkipsBackup verifies that makeBackup returns nil
|
// TestMakeBackup_NonexistentFileSkipsBackup verifies that MakeBackup returns nil
|
||||||
// when the config file does not exist (no error, no panic).
|
// when the config file does not exist (no error, no panic).
|
||||||
func TestMakeBackup_NonexistentFileSkipsBackup(t *testing.T) {
|
func TestMakeBackup_NonexistentFileSkipsBackup(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
configPath := filepath.Join(dir, "nonexistent.json")
|
configPath := filepath.Join(dir, "nonexistent.json")
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup on nonexistent file should return nil, got: %v", err)
|
t.Fatalf("MakeBackup on nonexistent file should return nil, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2698,8 +2698,8 @@ func TestMakeBackup_OnlyConfigNoSecurity(t *testing.T) {
|
||||||
configPath := filepath.Join(dir, "config.json")
|
configPath := filepath.Join(dir, "config.json")
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, _ := os.ReadDir(dir)
|
entries, _ := os.ReadDir(dir)
|
||||||
|
|
@ -2722,7 +2722,7 @@ func TestMakeBackup_OnlyConfigNoSecurity(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMakeBackup_SameDateSuffix verifies that config and security backups
|
// TestMakeBackup_SameDateSuffix verifies that config and security backups
|
||||||
// share the same date suffix (they are created in the same makeBackup call).
|
// share the same date suffix (they are created in the same MakeBackup call).
|
||||||
func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
configPath := filepath.Join(dir, "config.json")
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
|
@ -2731,8 +2731,8 @@ func TestMakeBackup_SameDateSuffix(t *testing.T) {
|
||||||
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
os.WriteFile(configPath, []byte(`{"version":2}`), 0o600)
|
||||||
os.WriteFile(secPath, []byte(`key: value`), 0o600)
|
os.WriteFile(secPath, []byte(`key: value`), 0o600)
|
||||||
|
|
||||||
if err := makeBackup(configPath); err != nil {
|
if err := MakeBackup(configPath); err != nil {
|
||||||
t.Fatalf("makeBackup: %v", err)
|
t.Fatalf("MakeBackup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, _ := os.ReadDir(dir)
|
entries, _ := os.ReadDir(dir)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue