From 5ad29767c97cd6123e6f64d3501dedc13b5e82bb Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sun, 3 May 2026 12:30:00 +0200 Subject: [PATCH] Add safety check for package list validation --- README.md | 3 ++- cmd/declpac/main.go | 15 ++++++++++++--- pkg/merge/merge.go | 11 +++++++++-- pkg/output/output.go | 6 +++--- pkg/pacman/pacman.go | 13 ++++++++++++- pkg/pacman/read/read.go | 19 +++++++++++++++++++ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e004ce4..a6001f3 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ docker | Flag | Alias | Description | |------|-------|-------------| -| `--state` | `-s` | State file to read package list from (can be used multiple times) | +| `--state` | `-s` | State file(s) to read package list from (can be used multiple times) | +| `--nocheck` | | Skip safety check (allow significant package count reductions) | `--dry-run` | | Preview changes without applying them | | `--verbose` | `-v` | Enable verbose output | | `--help` | `-h` | Show help message | diff --git a/cmd/declpac/main.go b/cmd/declpac/main.go index 76b6f2f..15ed9da 100644 --- a/cmd/declpac/main.go +++ b/cmd/declpac/main.go @@ -18,7 +18,7 @@ import ( type Config struct { StateFiles []string - NoConfirm bool + NoCheck bool DryRun bool Verbose bool } @@ -36,6 +36,11 @@ func main() { Usage: "State file(s) to read package list from", Destination: &cfg.StateFiles, }, + &cli.BoolFlag{ + Name: "nocheck", + Usage: "Skip safety check", + Destination: &cfg.NoCheck, + }, &cli.BoolFlag{ Name: "dry-run", Usage: "Simulate the sync without making changes", @@ -70,7 +75,11 @@ func run(cfg *Config) error { } log.Debug("run: packages read (%.2fs)", time.Since(start).Seconds()) - merged := merge.Merge(packages) + merged, err := merge.Merge(packages) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + return err + } if cfg.DryRun { result, err := read.DryRun(merged) @@ -89,7 +98,7 @@ func run(cfg *Config) error { } defer log.Close() - result, err := pacman.Sync(merged) + result, err := pacman.Sync(merged, cfg.NoCheck) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return err diff --git a/pkg/merge/merge.go b/pkg/merge/merge.go index 2499c65..638c295 100644 --- a/pkg/merge/merge.go +++ b/pkg/merge/merge.go @@ -1,9 +1,16 @@ package merge -func Merge(packages map[string]bool) []string { +import "errors" + +var ErrEmptyList = errors.New("package list is empty") + +func Merge(packages map[string]bool) ([]string, error) { result := make([]string, 0, len(packages)) for name := range packages { result = append(result, name) } - return result + if len(result) == 0 { + return nil, ErrEmptyList + } + return result, nil } diff --git a/pkg/output/output.go b/pkg/output/output.go index fd7fd10..e5400c2 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -14,13 +14,13 @@ type Result struct { func Format(r *Result) string { var b strings.Builder - b.WriteString(fmt.Sprintf("Installed %d packages, removed %d packages", r.Installed, r.Removed)) + b.WriteString(fmt.Sprintf("installed %d packages, removed %d packages", r.Installed, r.Removed)) if len(r.ToInstall) > 0 { - b.WriteString("\nWould install: ") + b.WriteString("\nwould install: ") b.WriteString(strings.Join(r.ToInstall, ", ")) } if len(r.ToRemove) > 0 { - b.WriteString("\nWould remove: ") + b.WriteString("\nwould remove: ") b.WriteString(strings.Join(r.ToRemove, ", ")) } return b.String() diff --git a/pkg/pacman/pacman.go b/pkg/pacman/pacman.go index d3cf809..2d05981 100644 --- a/pkg/pacman/pacman.go +++ b/pkg/pacman/pacman.go @@ -13,10 +13,21 @@ import ( "github.com/Riyyi/declpac/pkg/pacman/sync" ) -func Sync(packages []string) (*output.Result, error) { +func Sync(packages []string, noCheck bool) (*output.Result, error) { start := time.Now() log.Debug("Sync: starting...") + explicitList, err := read.ExplicitList() + if err != nil { + return nil, err + } + explicitCount := len(explicitList) + + if !noCheck && len(packages) < explicitCount/2 { + errMsg := "safety check: state packages (%d) less than half of explicitly installed (%d), override with --nocheck" + return nil, fmt.Errorf(errMsg, len(packages), explicitCount) + } + list, err := read.List() if err != nil { return nil, err diff --git a/pkg/pacman/read/read.go b/pkg/pacman/read/read.go index 9ce707f..a50d2f1 100644 --- a/pkg/pacman/read/read.go +++ b/pkg/pacman/read/read.go @@ -35,6 +35,25 @@ func List() ([]string, error) { return list, nil } +func ExplicitList() ([]string, error) { + start := time.Now() + log.Debug("ExplicitList: starting...") + + cmd := exec.Command("pacman", "-Qqe") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + list := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(list) > 0 && list[0] == "" { + list = nil + } + + log.Debug("ExplicitList: done (%.2fs)", time.Since(start).Seconds()) + return list, nil +} + func ListOrphans() ([]string, error) { start := time.Now() log.Debug("ListOrphans: starting...")