Files
pmg/cmd/setup/setup.go
T
d112ded3da feat: Merge template config into existing user config during setup install (#189)
* docs: Add config merging design spec for #114

Defines the merge-during-setup-install approach for keeping user
configs up to date with new template keys while preserving all
existing values, comments, and formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: Add implementation plan for config merging

TDD-based plan with 6 tasks: dependency setup, failing tests,
core merge implementation, integration test, WriteTemplateConfig
integration, and full verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Merge template config into existing user config during setup install

Instead of skipping when a config file exists, WriteTemplateConfig() now
merges missing keys from the embedded template into the user's config
using YAML AST manipulation. Preserves all user values, comments, and
formatting. Only adds keys present in the template but absent in the
user's config.

Closes #114

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Graceful error handling for config loading and setup commands

Replace panics in loadViperConfig with error returns so the app falls
back to defaults instead of crashing on malformed config files. Add
SilenceUsage to setup install/remove commands so runtime errors don't
dump the full usage text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* add test cmd in readme

* update copy text

* refactor: Address review feedback on config merging

- Rename existing/template to dest/source for generic util naming
- Remove unnecessary code comments (Rule N references, obvious comments)
- Add AGENTS.md with dev guide and code style rules, symlink CLAUDE.md
- Add BenchmarkMergeYAML (~46μs/op on M4 Pro)
- Remove stale design spec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update `MergeYAML` to use from dry/utils

* update AGENTS.md

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 19:21:01 +05:30

95 lines
2.6 KiB
Go

package setup
import (
"fmt"
"os"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/alias"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/internal/version"
"github.com/spf13/cobra"
)
var (
setupRemoveConfigFile = false
)
func NewSetupCommand() *cobra.Command {
setupCmd := &cobra.Command{
Use: "setup",
Short: "Manage PMG shell aliases and integration",
Long: "Setup and manage PMG config, shell aliases that allow you to use package manager commands with security guardrails.",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
setupCmd.AddCommand(NewInstallCommand())
setupCmd.AddCommand(NewRemoveCommand())
setupCmd.AddCommand(NewInfoCommand())
return setupCmd
}
func NewInstallCommand() *cobra.Command {
return &cobra.Command{
Use: "install",
Short: "Setup PMG config and aliases for package managers (npm, pnpm, pip, and more)",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
cfg := alias.DefaultConfig()
rcFileManager, err := alias.NewDefaultRcFileManager(cfg.RcFileName)
if err != nil {
return fmt.Errorf("failed to create alias manager: %w", err)
}
aliasManager := alias.New(cfg, rcFileManager)
err = aliasManager.Install()
if err != nil {
return fmt.Errorf("failed to install aliases: %w", err)
}
if err := config.WriteTemplateConfig(); err != nil {
return fmt.Errorf("failed to write template config: %w", err)
}
ui.PrintSetupInstallCmdInfo(aliasManager.GetRcPath(), config.Get().ConfigDir())
return nil
},
}
}
func NewRemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Short: "Removes pmg aliases from the user's shell config file.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Print(ui.GeneratePMGBanner(version.Version, version.Commit))
// We remove the config file only if explicitly asked to do so.
if setupRemoveConfigFile {
config := config.Get()
if err := os.Remove(config.ConfigFilePath()); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove config file %q: %w", config.ConfigFilePath(), err)
}
}
cfg := alias.DefaultConfig()
rcFileManager, err := alias.NewDefaultRcFileManager(cfg.RcFileName)
if err != nil {
return err
}
aliasManager := alias.New(cfg, rcFileManager)
return aliasManager.Remove()
},
}
cmd.Flags().BoolVar(&setupRemoveConfigFile, "config-file", false, "Remove the config file")
return cmd
}