diff --git a/.gitignore b/.gitignore index cde0123..c203e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ +github_test/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index b27b1cc..2f22d5a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,19 +1,16 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com - -# The lines below are called `modelines`. See `:help modeline` -# Feel free to remove those if you don't want/need to use them. # yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj version: 2 +env: + - GITHUB_TOKEN={{ .Env.CODEBERG_TOKEN }} + before: hooks: - # You may remove this if you don't use go modules. - go mod tidy - # you may remove this if you don't need go generate - go generate ./... + - go test ./... -v builds: - env: @@ -25,7 +22,6 @@ builds: archives: - format: tar.gz - # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ @@ -33,7 +29,6 @@ archives: {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives format_overrides: - goos: windows format: zip diff --git a/changelog.md b/changelog.md index a35e1ff..ac3aeeb 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,106 @@ All notable changes to this project will be documented in this file. -## [unreleased] +## [0.6.0] - 2025-02-04 + +### 🚀 Features + +- *(cli)* Add check command + +### 🚜 Refactor + +- *(packagemanager,cli,tui)* Move packages in specific subfolders + +### 📚 Documentation + +- *(cli)* Update version + +## [0.5.0] - 2025-01-24 + +### 🚀 Features + +- *(cli)* Add concurrent execution of search and update to improve performance +- *(cli)* Add aliases to commands +- *(config)* Add creation of default config if no config exists +- *(cli)* Add history command and logging for actions +- *(cli)* Add max history length + +### 🐛 Bug Fixes + +- *(packagemanager)* Reimplement getPackagemanager to enable installing buildtools on linux + +### 🚜 Refactor + +- *(cli)* Perform code clean up + +## [0.4.0] - 2025-01-20 + +### 🚀 Features + +- *(cli)* Add update and update-all command +- *(packagemanager)* Added packagemanager definitions for winget and chocolatey +- *(cli,packagemanager)* Implemented first working windows and macos versions + +### 🐛 Bug Fixes + +- *(packagemanager)* Added missing build tag for macos + +### 📚 Documentation + +- Update changelog.md +- Update changelog.md + +## [0.3.1] - 2025-01-19 + +### 🐛 Bug Fixes + +- Fix nil pointer execution if no proper packagemanager selected + +## [0.3.0] - 2025-01-18 + +### 🚀 Features + +- *(cli)* Implement search argument +- *(cli)* Add first implementation for install command +- *(cli)* Cleanup install command and add remove and search command + +### 🐛 Bug Fixes + +- *(cli)* Add missing yes flag in flatpak remove cmd + +### 🚜 Refactor + +- Seperate files, function and structs into seperate packages +- Move main.go back to project root + +### 📚 Documentation + +- Update changelog.md + +## [0.2.0] - 2025-01-16 + +### 🚀 Features + +- Update config.toml +- *(cli)* Add packages command and various subcommands + +### 🚜 Refactor + +- Change package management for different software sources +- Add progressbar and implement package interface +- Implement a simple progressbar instead of bubbletea +- Implement mocking functions and tests + +### 📚 Documentation + +- Update changelog.md +- Update changelog.md + +### ⚙️ Miscellaneous Tasks + +- Update gitignore and goreleaser.yaml + +## [0.1.0] - 2024-12-28 ### 🚀 Features @@ -13,6 +112,7 @@ All notable changes to this project will be documented in this file. - *(cli)* Add checks for pacman - *(cli,config)* Add flatpak and dotfiles support - *(cli)* Add a box display +- Prepare initial release ### 🐛 Bug Fixes diff --git a/cmd.go b/cmd.go deleted file mode 100644 index 658b3b8..0000000 --- a/cmd.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var rootCmd = &cobra.Command{ - Use: "package-installer", - Short: "Installiert Pakete basierend auf TOML-Konfiguration", - Run: run, -} - -func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringP("config", "c", "", "Pfad zur Konfigurationsdatei") - viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) -} - -func initConfig() { - if cfgFile := viper.GetString("config"); cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - viper.SetConfigName("config") - viper.AddConfigPath(".") - } - - if err := viper.ReadInConfig(); err != nil { - fmt.Println("Fehler beim Lesen der Konfigurationsdatei:", err) - os.Exit(1) - } -} diff --git a/cmd/check_cmd.go b/cmd/check_cmd.go new file mode 100644 index 0000000..1c91319 --- /dev/null +++ b/cmd/check_cmd.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "fmt" + "log" + + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/spf13/cobra" +) + +var checkCmd = &cobra.Command{ + Use: "check [package-name]", + Short: "Check if the specified package is installed", + Args: cobra.ExactArgs(1), + Aliases: []string{"c"}, + Run: func(cmd *cobra.Command, args []string) { + packageName := args[0] + managerName, _ := cmd.Flags().GetString("manager") + var manager pm.PackageManager + switch managerName { + case "os": + managerName = "OS Package Manager" + case "brew": + managerName = "homebrew" + } + switch managerName { + case "OS Package Manager": + manager = pm.NewOSManager("") + case "homebrew": + manager = &pm.HomebrewManager{} + case "cargo": + manager = &pm.CargoManager{} + case "pipx": + manager = &pm.PipxManager{} + case "flatpak": + manager = &pm.FlatpakManager{} + default: + fmt.Println("No PackageManager found") + } + if err := manager.Check(packageName); err != nil { + log.Printf("error: %v\n", err) + } + fmt.Printf("Package: %s installed\n", packageName) + if err := utils.LogToHistory("checked", packageName, manager.Name()); err != nil { + fmt.Println("error logging action:", err) + } + }, +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..e3f2e0b --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,141 @@ +package cmd + +import ( + "fmt" + "log" + "log/slog" + "os" + "path/filepath" + + "codeberg.org/Pata1704/system_setup_tool/internal/tui" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var RootCmd = &cobra.Command{ + Use: "system_setup_tool", + Short: "Installs packages based on TOML configuration", + Version: "0.6.0", + Run: tui.Run, +} + +func init() { + cobra.OnInitialize(initConfig) + RootCmd.PersistentFlags().StringP("config", "c", "", "Path to the configuration file") + err := viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config")) + if err != nil { + slog.Error("could not bind config flag") + } + + addCmd.Flags().StringP("name", "n", "", "The name of the package you want to add") + addCmd.Flags().StringP("manager", "m", "", "The package manager you want to add the package to (homebrew|cargo|flatpak|pipx|go)") + addCmd.Flags().Bool("system", false, "Add as a system package") + addCmd.Flags().Bool("headless", false, "Add as a headless system package (only used with --system)") + + deleteCmd.Flags().StringP("name", "n", "", "The name of the package you want to delete") + deleteCmd.Flags().StringP("manager", "m", "", "The package manager you want to delete the package from (homebrew|cargo|flatpak|pipx|go)") + deleteCmd.Flags().Bool("system", false, "Delete from system packages") + deleteCmd.Flags().Bool("headless", false, "Delete from headless system packages (only used with --system)") + + enableCmd.Flags().Bool("value", true, "Set to true to enable, false to disable") + + installCmd.Flags().StringP("manager", "m", "os", "The package manager you want to install a package with. (Options: os|homebrew|pipx|flatpak|cargo)") + + removeCmd.Flags().StringP("manager", "m", "os", "The package manager you want to remove a package with. (Options: os|homebrew|pipx|flatpak|cargo)") + + searchCmd.Flags().StringP("manager", "m", "", "The package manager you want to search a package with. (Options: os|homebrew|flatpak)") + + updateCmd.Flags().StringP("manager", "m", "os", "The package manager you want to update packages with. (Options: os|homebrew|pipx|flatpak)") + + checkCmd.Flags().StringP("manager", "m", "os", "The package manager you want to check for a packages with. (Options: os|homebrew|pipx|flatpak)") + + packageCmd.AddCommand( + addCmd, + deleteCmd, + showCmd, + enableCmd, + ) + RootCmd.AddCommand( + packageCmd, + searchCmd, + installCmd, + removeCmd, + updateCmd, + updateAllCmd, + historyCmd, + checkCmd, + ) +} + +func initConfig() { + if cfgFile := viper.GetString("config"); cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.SetConfigName("config") + viper.SetConfigType("toml") + dirname, err := os.UserConfigDir() + if err != nil { + log.Println("cant obtain config dir") + } else { + viper.AddConfigPath(filepath.Join(dirname, "sst")) + } + } + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + fmt.Println("no config file found, creating a new one...") + if err = createDefaultConfig(); err != nil { + slog.Error(fmt.Sprintf("error creating default config: %v\n", err)) + os.Exit(1) + } + } else { + log.Printf("error reading config file: %v\n", err) + os.Exit(1) + } + } +} + +func createDefaultConfig() error { + v := viper.New() + + v.SetDefault("config", "") + v.SetDefault("headless", true) + + v.SetDefault("dotfiles.enable", false) + v.SetDefault("dotfiles.git_repo", "") + + v.SetDefault("package_managers.cargo.enable", false) + v.SetDefault("package_managers.cargo.packages", []string{}) + + v.SetDefault("package_managers.flatpak.enable", false) + v.SetDefault("package_managers.flatpak.packages", []string{}) + + v.SetDefault("package_managers.go.enable", false) + v.SetDefault("package_managers.go.packages", []string{}) + + v.SetDefault("package_managers.homebrew.enable", false) + v.SetDefault("package_managers.homebrew.packages", []string{}) + + v.SetDefault("package_managers.pipx.enable", false) + v.SetDefault("package_managers.pipx.packages", []string{}) + + v.SetDefault("package_managers.os.enable", true) + v.SetDefault("package_managers.os.packages", []string{}) + + configDir := filepath.Join(os.Getenv("HOME"), ".config", "sst") + if err := os.MkdirAll(configDir, 0o755); err != nil { + return fmt.Errorf("error creating config dir: %w", err) + } + + configPath := filepath.Join(configDir, "config.toml") + v.SetConfigFile(configPath) + v.SetConfigType("toml") + + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("error writing default config: %w", err) + } + + fmt.Println("default config created:", configPath) + return nil +} diff --git a/cmd/history_cmd.go b/cmd/history_cmd.go new file mode 100644 index 0000000..2b1fedb --- /dev/null +++ b/cmd/history_cmd.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var historyCmd = &cobra.Command{ + Use: "history", + Short: "show command history", + Long: "Shows history of the last 2000 commands", + Run: func(cmd *cobra.Command, args []string) { + dirname, err := os.UserConfigDir() + if err != nil { + log.Printf("error getting user config dir: %v\n", err) + } + historyFile := filepath.Join(dirname, "sst", "sst_history") + content, err := os.ReadFile(historyFile) + if err != nil { + fmt.Println("error reading history: ", err) + return + } + fmt.Println(string(content)) + }, +} diff --git a/cmd/install_cmd.go b/cmd/install_cmd.go new file mode 100644 index 0000000..5895509 --- /dev/null +++ b/cmd/install_cmd.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "fmt" + "log" + "runtime" + + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install [package_name]", + Short: "Install a package with the specified package manager", + Args: cobra.ExactArgs(1), + Aliases: []string{"i"}, + Run: func(cmd *cobra.Command, args []string) { + packageName := args[0] + managerName, _ := cmd.Flags().GetString("manager") + var manager pm.PackageManager + switch managerName { + case "os": + managerName = "OS Package Manager" + case "brew": + managerName = "homebrew" + } + switch managerName { + case "OS Package Manager": + if runtime.GOOS != "windows" { + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + manager = pm.NewOSManager(sudoPassword) + } else { + manager = pm.NewOSManager("") + } + case "homebrew": + manager = &pm.HomebrewManager{} + case "cargo": + manager = &pm.CargoManager{} + case "pipx": + manager = &pm.PipxManager{} + case "flatpak": + manager = &pm.FlatpakManager{} + default: + fmt.Println("No PackageManager found") + return + } + if err := manager.Install([]string{packageName}); err != nil { + log.Printf("error: %v\n", err) + } + if err := utils.LogToHistory("installed", packageName, manager.Name()); err != nil { + fmt.Println("error logging action:", err) + } + }, +} diff --git a/cmd/package_commands.go b/cmd/package_commands.go new file mode 100644 index 0000000..7c8b44c --- /dev/null +++ b/cmd/package_commands.go @@ -0,0 +1,200 @@ +package cmd + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + "fmt" + "os" + "slices" + "sort" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var packageCmd = &cobra.Command{ + Use: "package", + Short: "Manage packages in the configuration", +} + +var addCmd = &cobra.Command{ + Use: "add", + Short: "Adds a package to the config.toml", + Run: func(cmd *cobra.Command, args []string) { + name, _ := cmd.Flags().GetString("name") + manager, _ := cmd.Flags().GetString("manager") + isSystem, _ := cmd.Flags().GetBool("system") + isHeadless, _ := cmd.Flags().GetBool("headless") + + if isSystem { + var packages []string + if isHeadless { + packages = viper.GetStringSlice("packages.headless") + } else { + packages = viper.GetStringSlice("packages.non_headless") + } + + packages = append(packages, name) + + if isHeadless { + viper.Set("packages.headless", packages) + } else { + viper.Set("packages.non_headless", packages) + } + } else { + packages := viper.GetStringSlice(fmt.Sprintf("package_managers.%s.packages", manager)) + + if slices.Contains(packages, name) { + fmt.Printf("Package %s is already present in the configuration for %s\n", name, manager) + return + } + + packages = append(packages, name) + viper.Set(fmt.Sprintf("package_managers.%s.packages", manager), packages) + + } + if err := viper.WriteConfig(); err != nil { + fmt.Printf("Error writing configuration: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Package %s has been added to the configuration for %s\n", name, manager) + if err := utils.LogToHistory("added", name, manager); err != nil { + fmt.Println("error logging action:", err) + } + }, +} + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Deletes a package from the config.toml", + Run: func(cmd *cobra.Command, args []string) { + name, _ := cmd.Flags().GetString("name") + manager, _ := cmd.Flags().GetString("manager") + isSystem, _ := cmd.Flags().GetBool("system") + isHeadless, _ := cmd.Flags().GetBool("headless") + + if isSystem { + var packages []string + var packageType string + if isHeadless { + packages = viper.GetStringSlice("packages.headless") + packageType = "headless" + } else { + packages = viper.GetStringSlice("packages.non_headless") + packageType = "non-headless" + } + + for i, pkg := range packages { + if pkg == name { + packages = append(packages[:i], packages[i+1:]...) + if isHeadless { + viper.Set("packages.headless", packages) + } else { + viper.Set("packages.non_headless", packages) + } + + if err := viper.WriteConfig(); err != nil { + fmt.Printf("Error writing configuration: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Package %s has been deleted from the %s system packages\n", name, packageType) + return + } + } + + fmt.Printf("Package %s was not found in the %s system packages\n", name, packageType) + } else { + packages := viper.GetStringSlice(fmt.Sprintf("package_managers.%s.packages", manager)) + + for i, pkg := range packages { + if pkg == name { + packages = append(packages[:i], packages[i+1:]...) + viper.Set(fmt.Sprintf("package_managers.%s.packages", manager), packages) + + if err := viper.WriteConfig(); err != nil { + fmt.Printf("Error writing configuration: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Package %s has been deleted from the configuration for %s\n", name, manager) + return + } + } + + fmt.Printf("Package %s was not found in the configuration for %s\n", name, manager) + if err := utils.LogToHistory("removed", name, manager); err != nil { + fmt.Println("error logging action:", err) + } + } + }, +} + +var showCmd = &cobra.Command{ + Use: "show", + Short: "Shows the current package configuration", + Run: func(cmd *cobra.Command, args []string) { + packageManagers := viper.GetStringMap("package_managers") + + if len(packageManagers) == 0 { + fmt.Println("No package managers configured.") + return + } + + var managers []string + for manager := range packageManagers { + managers = append(managers, manager) + } + sort.Strings(managers) + + for _, manager := range managers { + fmt.Printf("Package Manager: %s\n", manager) + packages := viper.GetStringSlice(fmt.Sprintf("package_managers.%s.packages", manager)) + sort.Strings(packages) + for _, pkg := range packages { + fmt.Printf(" - %s\n", pkg) + } + fmt.Println() + } + fmt.Println("System Packages:") + headless := viper.GetStringSlice("packages.headless") + nonHeadless := viper.GetStringSlice("packages.non_headless") + + fmt.Println(" Headless:") + for _, pkg := range headless { + fmt.Printf(" - %s\n", pkg) + } + + fmt.Println(" Non-Headless:") + for _, pkg := range nonHeadless { + fmt.Printf(" - %s\n", pkg) + } + }, +} + +var enableCmd = &cobra.Command{ + Use: "enable [package_manager]", + Short: "Enable or disable a package manager (Options: homebrew|go|cargo|pipx|flatpak)", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + manager := args[0] + enable, _ := cmd.Flags().GetBool("value") + + if !viper.IsSet(fmt.Sprintf("package_managers.%s", manager)) { + fmt.Printf("Package manager %s not found in configuration\n", manager) + return + } + + viper.Set(fmt.Sprintf("package_managers.%s.enable", manager), enable) + + if err := viper.WriteConfig(); err != nil { + fmt.Printf("Error writing configuration: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Package manager %s has been %s\n", manager, map[bool]string{true: "enabled", false: "disabled"}[enable]) + if err := utils.LogToHistory("enabled", "", manager); err != nil { + fmt.Println("error logging action:", err) + } + }, +} diff --git a/cmd/remove_cmd.go b/cmd/remove_cmd.go new file mode 100644 index 0000000..c898268 --- /dev/null +++ b/cmd/remove_cmd.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "fmt" + "log" + "runtime" + + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/spf13/cobra" +) + +var removeCmd = &cobra.Command{ + Use: "remove [package_name]", + Short: "Remove a package with the specified package manager", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + packageName := args[0] + managerName, _ := cmd.Flags().GetString("manager") + var manager pm.PackageManager + switch managerName { + case "os": + managerName = "OS Package Manager" + case "brew": + managerName = "homebrew" + } + switch managerName { + case "OS Package Manager": + if runtime.GOOS != "windows" { + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + manager = pm.NewOSManager(sudoPassword) + } else { + manager = pm.NewOSManager("") + } + case "homebrew": + manager = &pm.HomebrewManager{} + case "cargo": + manager = &pm.CargoManager{} + case "pipx": + manager = &pm.PipxManager{} + case "flatpak": + manager = &pm.FlatpakManager{} + default: + fmt.Println("No PackageManager found") + return + } + if err := manager.RemovePackage(packageName); err != nil { + log.Printf("error: %v\n", err) + } + if err := utils.LogToHistory("removed", packageName, manager.Name()); err != nil { + fmt.Println("error logging action:", err) + } + }, +} diff --git a/cmd/search_cmd.go b/cmd/search_cmd.go new file mode 100644 index 0000000..8aaf02d --- /dev/null +++ b/cmd/search_cmd.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "fmt" + "log" + "runtime" + "strings" + "sync" + + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/spf13/cobra" +) + +var searchCmd = &cobra.Command{ + Use: "search [package_name]", + Short: "Search for a package across package managers (will search in os|flatpak|homebrew)", + Args: cobra.ExactArgs(1), + Aliases: []string{"s"}, + Run: func(cmd *cobra.Command, args []string) { + packageName := args[0] + managerName, _ := cmd.Flags().GetString("manager") + switch managerName { + case "os": + managerName = "OS Package Manager" + case "brew": + managerName = "homebrew" + } + + var managers []pm.PackageManager + switch runtime.GOOS { + case "linux": + managers = []pm.PackageManager{ + pm.NewOSManager(""), + &pm.HomebrewManager{}, + &pm.FlatpakManager{}, + } + case "windows": + managers = []pm.PackageManager{ + pm.NewOSManager(""), + &pm.WingetManager{}, + &pm.ChocoManager{}, + } + case "darwin": + managers = []pm.PackageManager{ + pm.NewOSManager(""), + } + default: + log.Println("No Package Managers found") + } + + if managerName != "" { + for _, m := range managers { + if strings.EqualFold(m.Name(), managerName) { + searchAndDisplayResults(m, packageName) + return + } + } + fmt.Printf("Not supported for '%s'\n", managerName) + return + } + + searchConcurrently(managers, packageName) + }, +} + +func displayResults(results []string, manager string) { + if len(results) == 0 { + fmt.Printf("No results found in %s\n", manager) + return + } + for _, result := range results { + fmt.Printf("- %s\n", result) + } +} + +func searchAndDisplayResults(manager pm.PackageManager, packageName string) { + fmt.Printf("Searching in %s:\n", manager.Name()) + results := manager.SearchPackage(packageName) + displayResults(results, manager.Name()) + fmt.Println() +} + +func searchConcurrently(managers []pm.PackageManager, packageName string) { + var wg sync.WaitGroup + results := make(chan struct { + manager pm.PackageManager + results []string + }, len(managers)) + + for _, manager := range managers { + wg.Add(1) + go func(m pm.PackageManager) { + defer wg.Done() + searchResults := m.SearchPackage(packageName) + results <- struct { + manager pm.PackageManager + results []string + }{m, searchResults} + }(manager) + } + + go func() { + wg.Wait() + close(results) + }() + + for result := range results { + fmt.Printf("Results from %s:\n", result.manager.Name()) + displayResults(result.results, result.manager.Name()) + fmt.Println() + if err := utils.LogToHistory("searched", packageName, result.manager.Name()); err != nil { + fmt.Println("error logging action:", err) + } + } +} diff --git a/cmd/update_command.go b/cmd/update_command.go new file mode 100644 index 0000000..69bee95 --- /dev/null +++ b/cmd/update_command.go @@ -0,0 +1,129 @@ +package cmd + +import ( + "fmt" + "log" + "runtime" + "sync" + + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/spf13/cobra" +) + +var updateCmd = &cobra.Command{ + Use: "update [package_name]", + Short: "update a package with the specified package manager.", + Long: "update a package with the specified package manager.\nIf no package specified, all packages for the package manager will be updated", + Args: cobra.ExactArgs(1), + Aliases: []string{"u"}, + Run: func(cmd *cobra.Command, args []string) { + packageName := args[0] + managerName, _ := cmd.Flags().GetString("manager") + var manager pm.PackageManager + switch managerName { + case "os": + managerName = "OS Package Manager" + case "brew": + managerName = "homebrew" + } + + switch managerName { + case "OS Package Manager": + if runtime.GOOS != "windows" { + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + manager = pm.NewOSManager(sudoPassword) + } else { + manager = pm.NewOSManager("") + } + case "homebrew": + manager = &pm.HomebrewManager{} + case "cargo": + manager = &pm.CargoManager{} + case "pipx": + manager = &pm.PipxManager{} + case "flatpak": + manager = &pm.FlatpakManager{} + default: + fmt.Println("No PackageManager found") + return + } + if packageName != "" { + if err := manager.UpdatePackage(packageName); err != nil { + log.Printf("error: %v\n", err) + } else { + if err := utils.LogToHistory("updated", packageName, manager.Name()); err != nil { + fmt.Println("error logging action:", err) + } + } + } else { + if err := manager.UpdateAllPackages(); err != nil { + log.Printf("error: %v\n", err) + } else { + if err := utils.LogToHistory("updated", "all", "all"); err != nil { + fmt.Println("error logging action:", err) + } + } + } + }, +} + +var updateAllCmd = &cobra.Command{ + Use: "update-all", + Short: "update all packages with all package managers.", + Run: func(cmd *cobra.Command, args []string) { + var managers []pm.PackageManager + switch runtime.GOOS { + case "linux": + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + managers = []pm.PackageManager{ + pm.NewOSManager(sudoPassword), + &pm.HomebrewManager{}, + &pm.FlatpakManager{}, + &pm.PipxManager{}, + } + case "windows": + managers = []pm.PackageManager{ + pm.NewOSManager(""), + &pm.WingetManager{}, + &pm.ChocoManager{}, + &pm.PipxManager{}, + } + case "darwin": + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + managers = []pm.PackageManager{ + pm.NewOSManager(sudoPassword), + &pm.PipxManager{}, + } + default: + log.Println("No Package Managers found") + } + updateAllConcurrently(managers) + }, +} + +func updateAllConcurrently(managers []pm.PackageManager) { + var wg sync.WaitGroup + for _, m := range managers { + wg.Add(1) + go func(manager pm.PackageManager) { + defer wg.Done() + fmt.Printf("Updating %s-Packages\n", manager.Name()) + err := manager.UpdateAllPackages() + if err != nil { + log.Printf("Error updating %s: %v\n", manager.Name(), err) + } + }(m) + } + wg.Wait() +} diff --git a/config.go b/config.go deleted file mode 100644 index 35ec318..0000000 --- a/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -type Config struct { - Headless bool `mapstructure:"headless"` - Packages Packages `mapstructure:"packages"` - SpecialPackages SpecialPackages `mapstructure:"special_packages"` - Flatpak FlatpakConfig `mapstructure:"flatpak"` - Dotfiles DotfilesConfig `mapstructure:"dotfiles"` -} diff --git a/config.toml b/config.toml deleted file mode 100644 index 85d85aa..0000000 --- a/config.toml +++ /dev/null @@ -1,103 +0,0 @@ -headless = false - -[packages] -headless = [ - "git", - "curl", - "wget", - "ripgrep", - "bottom", - "bat", - "biber", - "bear", - "docker", - "docker-compose", - "fd", - "bat", - "lsd", - "zsh", - "jq", - "neofetch", - "neovim", - "helix", - "npm", - "stow", - "task", - "timew", - "typst-lsp", - "tree-sitter-cli", - "tmux", - "zoxide", - "python3-pip", - "pipx", - "golang", - "rustup", - "latexmk", - "luarocks", -] - -non_headless = [ - "discord", - "flameshot", - "fuzzel", - "google-chrome", - "hyprcursor", - "hypridle", - "hyprland", - "hyprland-qtutils", - "hyprlock", - "hyprpaper", - "hyprutils", - "kitty", - "mako", - "mpv", - "neovide", - "pidgin", - "remmina", - "spotify", - "thunderbird", - "thunderbird-i18n-de", - "virt-manager", - "vlc", - "waybar", - "xdg-desktop-portal-hyprland", - "zathura", - "zathura-pdf-mupdf", - "zoom", - "zotero", - "android-studio", -] - -[special_packages] -go = [ - "github.com/jesseduffield/lazygit", - "github.com/stefanlogue/meteor", - "golang.org/x/tools/gopls", - "github.com/go-delve/delve/cmd/dlv", - "github.com/air-verse/air", - "go.senan.xyz/cliphist", - "github.com/goreleaser/goreleaser/v2", -] - -cargo = ["git-cliff", "bottom", "typst-cli"] - -pipx = ["harlequin", "posting", "euporie"] - -[flatpak] -enable = true -remotes = [ - { name = "flathub", url = "https://flathub.org/repo/flathub.flatpakrepo" }, -] -packages = [ - "com.spotify.Client", - "us.zoom.Zoom", - "org.zotero.Zotero", - "com.google.AndroidStudio", - "io.freetubeapp.FreeTube", - "com.discordapp.Discord", - "com.nextcloud.desktopclient.nextcloud", -] - -[dotfiles] -enable = true -git_repo = "https://codeberg.org/Pata1704/dotfiles.git" diff --git a/dotfiles.go b/dotfiles.go deleted file mode 100644 index 08f3d52..0000000 --- a/dotfiles.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/exec" - "path/filepath" -) - -type DotfilesConfig struct { - Enable bool `mapstructure:"enable"` - GitRepo string `mapstructure:"git_repo"` -} - -func setupDotfiles(config DotfilesConfig) error { - if _, err := exec.LookPath("git"); err != nil { - return fmt.Errorf("git ist nicht installiert") - } - - if _, err := exec.LookPath("stow"); err != nil { - return fmt.Errorf("gnu stow ist nicht installiert") - } - - dotfilesDir := filepath.Join(os.Getenv("HOME"), "dotfiles") - - // cmd := exec.Command("git", "clone", config.GitRepo) - cmd := exec.Command("git", "clone", config.GitRepo, dotfilesDir) - if err := cmd.Run(); err != nil { - return fmt.Errorf("fehler beim Klonen der Dotfiles: %v", err) - } - - if err := os.Chdir(dotfilesDir); err != nil { - return fmt.Errorf("fehler beim Wechseln in das Dotfiles-Verzeichnis: %v", err) - } - - cmd = exec.Command("stow", ".", "--override='*'") - if err := cmd.Run(); err != nil { - log.Printf("Fehler beim Linken: %v", err) - } - fmt.Printf("Alles erfolgreich verlinkt\n") - - return nil -} diff --git a/flatpak.go b/flatpak.go deleted file mode 100644 index 4295b01..0000000 --- a/flatpak.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os/exec" - - "github.com/charmbracelet/bubbles/progress" -) - -type FlatpakConfig struct { - Enable bool `mapstructure:"enable"` - Remotes []Remote `mapstructure:"remotes"` - Packages []string `mapstructure:"packages"` -} - -type Remote struct { - Name string `mapstructure:"name"` - URL string `mapstructure:"url"` -} - -func installFlatpak(os *OS, sudoPassword string) error { - var command string - switch os.PackageManager { - case "pacman": - command = "pacman -S --noconfirm --needed flatpak" - case "apt": - command = "apt install -y flatpak" - case "dnf": - command = "dnf install -y flatpak" - default: - return fmt.Errorf("keine Flatpak-Installation für OS %s definiert", os.ID) - } - - if err := installPackage(command, "", sudoPassword); err != nil { - return fmt.Errorf("fehler bei der Flatpak-Installation: %v", err) - } - return nil -} - -func addFlatpakRemotes(remotes []Remote) error { - for _, remote := range remotes { - cmd := exec.Command("flatpak", "remote-add", "--if-not-exists", remote.Name, remote.URL) - if err := cmd.Run(); err != nil { - return fmt.Errorf("fehler beim Hinzufügen des Remotes %s: %v", remote.Name, err) - } - } - return nil -} - -func installFlatpakPackages(packages []string) error { - if len(packages) == 0 { - return nil - } - - fmt.Println("\nInstalliere Flatpak-Pakete...") - p := progress.New( - progress.WithDefaultGradient(), - progress.WithWidth(40), - ) - - for i, pkg := range packages { - p.SetPercent(float64(i) / float64(len(packages))) - fmt.Printf("Installiere Flatpak: %s\n", pkg) - - cmd := exec.Command("flatpak", "install", "-y", pkg) - if err := cmd.Run(); err != nil { - log.Printf("Fehler bei der Installation von %s: %v", pkg, err) - continue - } - } - return nil -} diff --git a/go.mod b/go.mod index 2afa679..2870739 100644 --- a/go.mod +++ b/go.mod @@ -1,60 +1,61 @@ -module system_setup +module codeberg.org/Pata1704/system_setup_tool -go 1.23.4 +go 1.25.5 require ( - github.com/Delta456/box-cli-maker/v2 v2.3.0 - github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.4 - github.com/charmbracelet/huh v0.6.0 - github.com/charmbracelet/lipgloss v1.0.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 + github.com/charmbracelet/huh v0.8.0 + github.com/magefile/mage v1.15.0 + github.com/mitchellh/mapstructure v1.5.0 + github.com/schollz/progressbar/v3 v3.19.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 + golang.org/x/sys v0.40.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/catppuccin/go v0.2.0 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.4.5 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20260109001716-2fbdffcb221f // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.6.2 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gookit/color v1.5.2 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/huandu/xstrings v1.3.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.0.0-20221017184919-83659145692c // indirect - golang.org/x/text v0.18.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9525042..d016915 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,84 @@ -github.com/Delta456/box-cli-maker/v2 v2.3.0 h1:rGdoK/Qt3shdT1uqRMGgPqrhtisGD7PamTW8vY5MyCA= -github.com/Delta456/box-cli-maker/v2 v2.3.0/go.mod h1:Uv/kSX95LuNQn3C8wWazEIETE6MunPuYN+/knckbPQc= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= -github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -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= -github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= -github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= -github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= -github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= -github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= -github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI= +github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= +github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20250623112707-45752038d08d h1:fEsdno3tFo//l8pS4hlzH5hj6zGSaMzQcbyDLfToeJg= +github.com/charmbracelet/x/exp/strings v0.0.0-20250623112707-45752038d08d/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k= +github.com/charmbracelet/x/exp/strings v0.0.0-20260109001716-2fbdffcb221f h1:c0cKImYFPrOEEzMsYss56Q7Q69HD7H4ss3Yu9Mw9vqQ= +github.com/charmbracelet/x/exp/strings v0.0.0-20260109001716-2fbdffcb221f/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= -github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +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/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -58,16 +90,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -76,100 +112,82 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= +github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= +github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -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/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.0.0-20221017184919-83659145692c h1:dveknrit5futqEmXAvd2I1BbZIDhxRijsyWHM86NlcA= -golang.org/x/term v0.0.0-20221017184919-83659145692c/go.mod h1:VTIZ7TEbF0BS9Sv9lPTvGbtW8i4z6GGbJBCM37uMCzY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..680de28 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,18 @@ +package config + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/dotfiles" + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" +) + +type PackageManagerConfig struct { + Enable bool `mapstructure:"enable"` + Packages []string `mapstructure:"packages"` +} + +type Config struct { + Headless bool `mapstructure:"headless"` + Packages pm.Packages `mapstructure:"packages"` + PackageManagers map[string]PackageManagerConfig `mapstructure:"package_managers"` + Dotfiles dotfiles.DotfilesConfig `mapstructure:"dotfiles"` +} diff --git a/internal/dotfiles/dotfiles.go b/internal/dotfiles/dotfiles.go new file mode 100644 index 0000000..c5d09f3 --- /dev/null +++ b/internal/dotfiles/dotfiles.go @@ -0,0 +1,43 @@ +package dotfiles + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "os" + "path/filepath" +) + +type DotfilesConfig struct { + Enable bool `mapstructure:"enable"` + GitRepo string `mapstructure:"git_repo"` +} + +func SetupDotfiles(config DotfilesConfig) error { + if _, err := shell.ExecLookPath("git"); err != nil { + return fmt.Errorf("git ist nicht installiert") + } + + if _, err := shell.ExecLookPath("stow"); err != nil { + return fmt.Errorf("gnu stow not installed") + } + + dotfilesDir := filepath.Join(os.Getenv("HOME"), "dotfiles") + + cmd := shell.ExecCommand("git", "clone", config.GitRepo, dotfilesDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error cloning dotfiles: %v", err) + } + + if err := os.Chdir(dotfilesDir); err != nil { + return fmt.Errorf("error changing into dotfiles directory: %v", err) + } + + cmd = shell.ExecCommand("stow", ".", "--override='*'") + if err := cmd.Run(); err != nil { + log.Printf("error creating links: %v", err) + } + fmt.Printf("all linked properly\n") + + return nil +} diff --git a/internal/shell/shell.go b/internal/shell/shell.go new file mode 100644 index 0000000..5b7a8b7 --- /dev/null +++ b/internal/shell/shell.go @@ -0,0 +1,23 @@ +package shell + +import ( + "fmt" + "os" + "os/exec" +) + +var ( + ExecCommand = exec.Command + ExecLookPath = exec.LookPath +) + +func ExecuteShellCommand(command string, env string) error { + cmd := ExecCommand("bash", "-c", command) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, env) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("befehl fehlgeschlagen: %v\nAusgabe: %s", err, output) + } + return nil +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go new file mode 100644 index 0000000..df3a39f --- /dev/null +++ b/internal/tui/tui.go @@ -0,0 +1,115 @@ +package tui + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/config" + "codeberg.org/Pata1704/system_setup_tool/internal/dotfiles" + "codeberg.org/Pata1704/system_setup_tool/internal/utils" + "fmt" + "log" + + pm "codeberg.org/Pata1704/system_setup_tool/pkg/packagemanager" + + "github.com/charmbracelet/huh" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type installedPkgMsg string + +func Run(cmd *cobra.Command, args []string) { + sudoPassword, err := utils.GetSudoPassword() + if err != nil { + log.Fatal(err) + } + + var cfg config.Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatalf("Fehler beim Lesen der Konfiguration: %v", err) + } + + // form := huh.NewForm( + // huh.NewGroup( + // huh.NewConfirm(). + // Title("Möchten Sie eine headless Installation durchführen?"). + // Value(&cfg.Headless), + // ), + // ).WithTheme(huh.ThemeCatppuccin()) + // + // if err := form.Run(); err != nil { + // log.Fatalf("Fehler bei der Benutzerabfrage: %v", err) + // } + // + // osManager := pm.NewOSManager(sudoPassword) + // + // if err := osManager.Install(cfg.Packages.Headless); err != nil { + // log.Printf("Warnung bei der Installation der Headless-Pakete: %v", err) + // } + // if !cfg.Headless { + // if err := osManager.Install(cfg.Packages.NonHeadless); err != nil { + // log.Printf("Warnung bei der Installation der Non-Headless-Pakete: %v", err) + // } + // } + // + + for name, config := range cfg.PackageManagers { + if config.Enable { + var manager pm.PackageManager + switch name { + case "os": + manager = pm.NewOSManager(sudoPassword) + case "homebrew": + manager = &pm.HomebrewManager{} + case "go": + manager = &pm.GolangManager{} + case "cargo": + manager = &pm.CargoManager{} + case "pipx": + manager = &pm.PipxManager{} + case "flatpak": + flatpakConfig := pm.FlatpakConfig{} + if err := mapstructure.Decode(config, &flatpakConfig); err != nil { + log.Printf("error decoding flatpak config: %v", err) + continue + } + manager = pm.NewFlatpakManager(sudoPassword, flatpakConfig) + default: + log.Printf("unknown packagemanager: %s", name) + continue + } + if err := manager.InstallManager(); err != nil { + log.Printf("warning packagemanager %s not installed and could not be installed: %v", manager.Name(), err) + } + + if err := manager.Install(config.Packages); err != nil { + log.Printf("warning at %s-packages: %v", manager.Name(), err) + } + } + } + osManager := pm.NewOSManager(sudoPassword) + var installBuild bool + form := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Möchten Sie die Build Essentials installieren?"). + Value(&installBuild), + ), + ).WithTheme(huh.ThemeCatppuccin()) + + err = form.Run() + if err != nil { + log.Fatal(err) + } + + if installBuild { + if err := osManager.InstallBuildEssentials(); err != nil { + log.Printf("Warnung: %v", err) + } + } + if cfg.Dotfiles.Enable { + fmt.Println("\nconfiguring dotfiles...") + if err := dotfiles.SetupDotfiles(cfg.Dotfiles); err != nil { + log.Printf("Warnung bei Dotfiles-Setup: %v", err) + } + } +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..33d87e5 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,80 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/charmbracelet/huh" +) + +func GetSudoPassword() (string, error) { + var password string + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter your password"). + EchoMode(huh.EchoModePassword). + Value(&password), + ), + ).WithTheme(huh.ThemeCatppuccin()) + + err := form.Run() + if err != nil { + return "", fmt.Errorf("error at password-dialog: %v", err) + } + return password, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +const ( + maxHistoryLines = 2000 + historyFileName = "sst_history" +) + +func LogToHistory(action, pkg, manager string) error { + dirname, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("cant obtain config dir: %v", err) + } + historyFile := filepath.Join(dirname, "sst", historyFileName) + + lines, err := readHistoryLines(historyFile) + if err != nil { + return err + } + + timestamp := time.Now().Format("2006-01-02 15:04:05") + logEntry := fmt.Sprintf("%s: action: [%s] - package: [%s] - packagemanager: [%s]", timestamp, action, pkg, manager) + lines = append(lines, logEntry) + + if len(lines) > maxHistoryLines { + lines = lines[len(lines)-maxHistoryLines:] + } + + return writeHistoryLines(historyFile, lines) +} + +func readHistoryLines(filePath string) ([]string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + if os.IsNotExist(err) { + return []string{}, nil + } + return nil, err + } + return strings.Split(string(content), "\n"), nil +} + +func writeHistoryLines(filePath string, lines []string) error { + content := strings.Join(lines, "\n") + return os.WriteFile(filePath, []byte(content), 0644) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..df66fce --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,40 @@ +package utils + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestMax(t *testing.T) { + tests := []struct { + a, b, want int + }{ + {1, 2, 2}, + {5, 3, 5}, + {0, 0, 0}, + {-1, -5, -1}, + } + + for _, tt := range tests { + got := max(tt.a, tt.b) + if got != tt.want { + t.Errorf("max(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want) + } + } +} + +func TestExecuteShellCommand(t *testing.T) { + // Mock execCommand + oldExecCommand := shell.ExecCommand + defer func() { shell.ExecCommand = oldExecCommand }() + + shell.ExecCommand = func(command string, args ...string) *exec.Cmd { + return exec.Command("echo", "mocked command") + } + + err := shell.ExecuteShellCommand("test command", "TEST_ENV=value") + if err != nil { + t.Errorf("executeShellCommand() error = %v; want nil", err) + } +} diff --git a/magefile.go b/magefile.go new file mode 100644 index 0000000..b0be26b --- /dev/null +++ b/magefile.go @@ -0,0 +1,56 @@ +//go:build mage + +package main + +import ( + "fmt" + "os" + "os/exec" + + "github.com/magefile/mage/mg" // mg contains helpful utility functions, like Deps +) + +// Default target to run when none is specified +// If not set, running mage will list available targets +// var Default = Build + +// A build step that requires additional params, or platform specific steps for example +func Build() error { + fmt.Println("Building...") + cmd := exec.Command("go", "build", "-o", "sst", ".") + return cmd.Run() +} + +// A custom install step if you need your bin someplace other than go/bin +func Install() error { + mg.Deps(Build) + fmt.Println("Installing...") + return os.Rename("./sst", "~/.local/bin/sst") +} + +// Clean up after yourself +func Clean() { + fmt.Println("Cleaning...") + os.RemoveAll("system_setup_tool") +} + +// Use gofmt to format your code +func Format() error { + fmt.Println("Checking Format") + cmd := exec.Command("gofmt", "./") + return cmd.Run() +} + +// get linting information from golangci-lint +func Lint() error { + fmt.Println("Linting...") + cmd := exec.Command("golangci-lint", "./...") + return cmd.Run() +} + +// perform all modul tests +func Test() error { + fmt.Println("Testing...") + cmd := exec.Command("go", "test", "./...") + return cmd.Run() +} diff --git a/main.go b/main.go index b7eb66a..0a40171 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,15 @@ package main import ( - "fmt" + "log" "os" - "github.com/Delta456/box-cli-maker/v2" + "codeberg.org/Pata1704/system_setup_tool/cmd" ) func main() { - Box := box.New(box.Config{Px: 2, Py: 5, Type: "Single"}) - Box.Print("System Setup", "Configure and install your predefined system") - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + if err := cmd.RootCmd.Execute(); err != nil { + log.Println(err) os.Exit(1) } } diff --git a/model.go b/model.go deleted file mode 100644 index bd184c4..0000000 --- a/model.go +++ /dev/null @@ -1,163 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os/exec" - "runtime" - "strings" - - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type model struct { - packages []string - index int - width int - height int - spinner spinner.Model - progress progress.Model - done bool - sudoPassword string - os OS -} - -func (m model) installSpecialSoftware() error { - if _, err := exec.LookPath("oh-my-posh"); err == nil { - fmt.Println("Oh-my-posh ist bereits installiert") - } else { - poshCommand := "curl -s https://ohmyposh.dev/install.sh | bash -s" - if err := installPackage(poshCommand, "", ""); err != nil { - return err - } - } - if _, err := exec.LookPath("go"); err == nil { - fmt.Println("Go ist bereits installiert.") - } else { - golangVersion := "1.23.4" - if err := downloadGolang(golangVersion); err != nil { - return fmt.Errorf("fehler beim Herunterladen von Go: %v", err) - } - golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH) - if err := installPackage(golangCommand, "", m.sudoPassword); err != nil { - return err - } - } - - if _, err := exec.LookPath("rustc"); err == nil { - fmt.Println("Rust ist bereits installiert.") - } else { - rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y" - if err := installPackage(rustupCommand, "", ""); err != nil { - return err - } - } - - // if _, err := exec.LookPath("pipx"); err == nil { - // fmt.Println("Pipx ist bereits installiert.") - // } else { - // pipXCommand := "python3 -m pip install --user pipx && python3 -m pipx ensurepath" - // if err := installPackage(pipXCommand, "", ""); err != nil { - // return err - // } - // } - - return nil -} - -func newModel(packages []string, sudoPassword string, os *OS) model { - p := progress.New( - progress.WithDefaultGradient(), - progress.WithWidth(40), - progress.WithoutPercentage(), - ) - s := spinner.New() - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) - return model{ - packages: packages, - spinner: s, - progress: p, - sudoPassword: sudoPassword, - os: *os, - } -} - -func (m model) Init() tea.Cmd { - return tea.Batch(m.installPackageCmd(m.packages[m.index]), m.spinner.Tick) -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width, m.height = msg.Width, msg.Height - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "esc", "q": - return m, tea.Quit - } - case installedPkgMsg: - pkg := m.packages[m.index] - if m.index >= len(m.packages)-1 { - m.done = true - return m, tea.Sequence( - tea.Printf("%s %s", checkMark, pkg), - tea.Quit, - ) - } - - m.index++ - progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages))) - - return m, tea.Batch( - progressCmd, - tea.Printf("%s %s", checkMark, pkg), - m.installPackageCmd(m.packages[m.index]), - ) - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - case progress.FrameMsg: - newModel, cmd := m.progress.Update(msg) - if newModel, ok := newModel.(progress.Model); ok { - m.progress = newModel - } - return m, cmd - } - return m, nil -} - -func (m model) View() string { - n := len(m.packages) - w := lipgloss.Width(fmt.Sprintf("%d", n)) - - if m.done { - return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)) - } - - pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n) - - spin := m.spinner.View() + " " - prog := m.progress.View() - cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) - - pkgName := currentPkgNameStyle.Render(m.packages[m.index]) - info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) - - cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) - gap := strings.Repeat(" ", cellsRemaining) - - return spin + info + gap + prog + pkgCount -} - -func (m model) installPackageCmd(pkg string) tea.Cmd { - return func() tea.Msg { - if err := installPackage(m.os.InstallCommand, pkg, m.sudoPassword); err != nil { - log.Printf("Fehler beim Installieren von %s: %v", pkg, err) - } - return installedPkgMsg(pkg) - } -} diff --git a/osinfo.go b/osinfo.go deleted file mode 100644 index 519b9af..0000000 --- a/osinfo.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/exec" - "strings" -) - -type OS struct { - ID string - Name string - Version string - PackageManager string - InstallCommand string -} - -func parseOsRelease(osRelease string) *OS { - var result OS - result.ID = "Unknown" - result.Name = "Unknown" - result.Version = "Unknown" - result.PackageManager = "Unkown" - result.InstallCommand = "Unkown" - - lines := strings.Split(osRelease, "\n") - - for _, line := range lines { - splitLine := strings.SplitN(line, "=", 2) - if len(splitLine) != 2 { - continue - } - switch splitLine[0] { - case "ID": - result.ID = strings.ToLower(strings.Trim(splitLine[1], "\"")) - case "NAME": - result.Name = strings.Trim(splitLine[1], "\"") - case "VERSION_ID": - result.Version = strings.Trim(splitLine[1], "\"") - } - } - err := result.getPackageManager() - if err != nil { - log.Fatal(err) - } - err = result.getInstallCommand() - if err != nil { - log.Fatal(err) - } - return &result -} - -func getLinuxDistribution() (*OS, error) { - _, err := os.Stat("/etc/os-release") - if os.IsNotExist(err) { - return nil, fmt.Errorf("unable to read system information") - } - - osRelease, _ := os.ReadFile("/etc/os-release") - return parseOsRelease(string(osRelease)), nil -} - -func (os *OS) getPackageManager() error { - switch os.ID { - case "debian", "ubuntu": - os.PackageManager = "apt" - return nil - case "arch": - os.PackageManager = "pacman" - return nil - case "fedora": - os.PackageManager = "dnf" - return nil - default: - pmcommands := []string{ - "apt", - "dnf", - "pacman", - } - for _, pmname := range pmcommands { - _, err := exec.LookPath(pmname) - if err == nil { - os.PackageManager = pmname - return nil - } - } - return fmt.Errorf("no packagemanager found for os: %s", os) - } -} - -func (os *OS) getInstallCommand() error { - switch os.PackageManager { - case "apt": - os.InstallCommand = "apt install -y" - return nil - case "pacman": - os.InstallCommand = "pacman -S --noconfirm --needed" - return nil - case "dnf": - os.InstallCommand = "dnf install -y --best" - return nil - default: - return fmt.Errorf("no install command found for package manager: %s", os.ID) - } -} - -func installBuildEssentials(os *OS, sudoPassword string) error { - var command string - switch os.PackageManager { - case "pacman": - command = "pacman -S --noconfirm --needed base-devel" - case "apt": - command = "apt install -y build-essential" - case "dnf": - command = "dnf install -y @development-tools" - default: - return fmt.Errorf("keine Build Essentials für OS %s definiert", os.ID) - } - - fmt.Printf("Installiere Build Essentials für %s...\n", os.Name) - if err := installPackage(command, "", sudoPassword); err != nil { - return fmt.Errorf("fehler bei der Installation der Build Essentials: %v", err) - } - return nil -} diff --git a/package.go b/package.go deleted file mode 100644 index 752528b..0000000 --- a/package.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "fmt" - "os/exec" - "strings" -) - -type Packages struct { - Headless []string `mapstructure:"headless"` - NonHeadless []string `mapstructure:"non_headless"` -} - -type SpecialPackages struct { - Go []string `mapstructure:"go"` - Cargo []string `mapstructure:"cargo"` - Pipx []string `mapstructure:"pipx"` -} - -func installPackage(cmd, pkg, sudoPassword string) error { - fullCmd := fmt.Sprintf("%s %s", cmd, pkg) - command := exec.Command("sudo", "-S", "sh", "-c", fullCmd) - command.Stdin = strings.NewReader(sudoPassword + "\n") - output, err := command.CombinedOutput() - if err != nil { - if strings.Contains(string(output), "not found") || strings.Contains(string(output), "no matching package") || strings.Contains(string(output), "Keine Übereinstimmung") || strings.Contains(string(output), "Ziel nicht gefunden") { - unavailablePackages = append(unavailablePackages, pkg) - return nil - } - return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output)) - } - return nil -} - -func installSpecialPackages(sp SpecialPackages) error { - for _, pkg := range sp.Go { - cmd := exec.Command("go", "install", pkg+"@latest") - if err := cmd.Run(); err != nil { - return fmt.Errorf("fehler bei der Installation von %s: %v", pkg, err) - } - } - - for _, pkg := range sp.Cargo { - cmd := exec.Command("cargo", "install", pkg) - if err := cmd.Run(); err != nil { - return fmt.Errorf("fehler bei der Installation von %s: %v", pkg, err) - } - } - - for _, pkg := range sp.Pipx { - cmd := exec.Command("pipx", "install", pkg) - if err := cmd.Run(); err != nil { - return fmt.Errorf("fehler bei der Installation von %s: %v", pkg, err) - } - } - - return nil -} - -func printUnavailablePackages() { - if len(unavailablePackages) > 0 { - fmt.Println("\nFolgende Pakete waren nicht verfügbar:") - for _, pkg := range unavailablePackages { - fmt.Printf("- %s\n", pkg) - } - } -} diff --git a/pkg/packagemanager/apt.go b/pkg/packagemanager/apt.go new file mode 100644 index 0000000..3c748c0 --- /dev/null +++ b/pkg/packagemanager/apt.go @@ -0,0 +1,81 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type AptManager struct { + SudoPassword string +} + +func (a *AptManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(a, packages) + if err != nil { + return err + } + return nil +} + +func (a *AptManager) Name() string { + return "OS Home Manager" +} + +func (a *AptManager) InstallManager() error { + return nil +} + +func (a *AptManager) InstallPackage(pkg string) error { + cmd := "apt install -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(a.SudoPassword + "\n") + return command.Run() +} + +func (a *AptManager) RemovePackage(pkg string) error { + cmd := "apt remove -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(a.SudoPassword + "\n") + return command.Run() +} + +func (a *AptManager) SearchPackage(pkg string) []string { + cmd := "apt search -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(a.SudoPassword + "\n") + packages, err := command.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", a.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (a *AptManager) UpdatePackage(pkg string) error { + cmd := "apt update -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(a.SudoPassword + "\n") + return command.Run() +} + +func (a *AptManager) UpdateAllPackages() error { + cmd := "apt update -y" + fullCmd := fmt.Sprintf("%s", cmd) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(a.SudoPassword + "\n") + return command.Run() +} + +func (a *AptManager) Check(pkg string) error { + cmd := shell.ExecCommand("/bin/sh", "-c", fmt.Sprintf("dpkg -l | grep %s", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/cargo.go b/pkg/packagemanager/cargo.go new file mode 100644 index 0000000..6ebc89b --- /dev/null +++ b/pkg/packagemanager/cargo.go @@ -0,0 +1,69 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "os" + "path/filepath" +) + +type CargoManager struct{} + +func (c *CargoManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(c, packages) + if err != nil { + return err + } + return nil +} + +func (c *CargoManager) Name() string { + return "Cargo" +} + +func (c *CargoManager) InstallManager() error { + if _, err := shell.ExecLookPath("brew"); err != nil { + installHomebrew() + } + + if _, err := shell.ExecLookPath("cargo"); err == nil { + return nil + } + + cmd := shell.ExecCommand("brew", "install", "rust") + return cmd.Run() +} + +func (c *CargoManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("cargo", "install", pkg) + return cmd.Run() +} + +func (c *CargoManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("cargo", "uninstall", pkg) + return cmd.Run() +} + +func (c *CargoManager) SearchPackage(pkg string) []string { + return []string{} +} + +func (c *CargoManager) UpdatePackage(pkg string) error { + return fmt.Errorf("update not supported") +} + +func (c *CargoManager) UpdateAllPackages() error { + return fmt.Errorf("update not supported") +} + +func (c *CargoManager) Check(pkg string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + cmd := shell.ExecCommand("ls", filepath.Join(home, ".cargo", "bin", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/cargo_test.go b/pkg/packagemanager/cargo_test.go new file mode 100644 index 0000000..5cb383c --- /dev/null +++ b/pkg/packagemanager/cargo_test.go @@ -0,0 +1,46 @@ +// cargo_test.go +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestCargoManager_Name(t *testing.T) { + cm := &CargoManager{} + if name := cm.Name(); name != "Cargo" { + t.Errorf("Expected name to be 'Cargo', got %s", name) + } +} + +func TestCargoManager_InstallManager(t *testing.T) { + cm := &CargoManager{} + + // Mock exec.LookPath + shell.ExecLookPath = func(file string) (string, error) { + if file == "cargo" { + return "/usr/bin/cargo", nil + } + return "", exec.ErrNotFound + } + defer func() { shell.ExecLookPath = exec.LookPath }() + + if err := cm.InstallManager(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestCargoManager_InstallPackage(t *testing.T) { + cm := &CargoManager{} + + // Mock exec.Command + shell.ExecCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command("echo", "mocked cargo install") + } + defer func() { shell.ExecCommand = exec.Command }() + + if err := cm.InstallPackage("test-package"); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} diff --git a/pkg/packagemanager/choco.go b/pkg/packagemanager/choco.go new file mode 100644 index 0000000..ae286cd --- /dev/null +++ b/pkg/packagemanager/choco.go @@ -0,0 +1,71 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "log" + "strings" +) + +type ChocoManager struct{} + +func (c *ChocoManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(c, packages) + if err != nil { + return err + } + return nil +} + +func (c *ChocoManager) Name() string { + return "Chocolatey" +} + +func (c *ChocoManager) InstallManager() error { + if _, err := shell.ExecLookPath("choco"); err == nil { + return nil + } + + return installHomebrew() +} + +func installChoco() error { + return nil +} + +func (c *ChocoManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("choco", "install", pkg) + return cmd.Run() +} + +func (c *ChocoManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("choco", "uninstall", pkg) + return cmd.Run() +} + +func (c *ChocoManager) SearchPackage(pkg string) []string { + cmd := shell.ExecCommand("choco", "search", pkg) + packages, err := cmd.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", c.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (c *ChocoManager) UpdatePackage(pkg string) error { + cmd := shell.ExecCommand("choco", "update", pkg) + return cmd.Run() +} + +func (c *ChocoManager) UpdateAllPackages() error { + cmd := shell.ExecCommand("choco", "update") + return cmd.Run() +} + +func (c *ChocoManager) Check(pkg string) error { + cmd := shell.ExecCommand("choco", "search", "--local-only", pkg) + return cmd.Run() +} diff --git a/pkg/packagemanager/dnf.go b/pkg/packagemanager/dnf.go new file mode 100644 index 0000000..526d2fe --- /dev/null +++ b/pkg/packagemanager/dnf.go @@ -0,0 +1,78 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type DnfManager struct { + SudoPassword string +} + +func (d *DnfManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(d, packages) + if err != nil { + return err + } + return nil +} + +func (d *DnfManager) Name() string { + return "OS Home Manager" +} + +func (d *DnfManager) InstallManager() error { + return nil +} + +func (d *DnfManager) InstallPackage(pkg string) error { + cmd := "dnf install -y --best" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(d.SudoPassword + "\n") + return command.Run() +} + +func (d *DnfManager) RemovePackage(pkg string) error { + cmd := "dnf remove -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(d.SudoPassword + "\n") + return command.Run() +} + +func (d *DnfManager) SearchPackage(pkg string) []string { + cmd := shell.ExecCommand("dnf", "search", pkg) + packages, err := cmd.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", d.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (d *DnfManager) UpdatePackage(pkg string) error { + cmd := "dnf update -y" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(d.SudoPassword + "\n") + return command.Run() +} + +func (d *DnfManager) UpdateAllPackages() error { + cmd := "dnf update -y" + fullCmd := fmt.Sprintf("%s", cmd) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(d.SudoPassword + "\n") + return command.Run() +} + +func (d *DnfManager) Check(pkg string) error { + cmd := shell.ExecCommand("/bin/sh", "-c", fmt.Sprintf("dnf list installed | grep %s", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/flatpak.go b/pkg/packagemanager/flatpak.go new file mode 100644 index 0000000..0e38ba4 --- /dev/null +++ b/pkg/packagemanager/flatpak.go @@ -0,0 +1,133 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type FlatpakManager struct { + OS *OS + SudoPassword string + Config FlatpakConfig +} + +func NewFlatpakManager(sudoPassword string, config FlatpakConfig) *FlatpakManager { + os, err := GetLinuxDistribution() + if err != nil { + log.Fatalf("error geting os information: %v", err) + } + return &FlatpakManager{ + OS: os, + SudoPassword: sudoPassword, + Config: config, + } +} + +func (f *FlatpakManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(f, packages) + if err != nil { + return err + } + return nil +} + +func (f *FlatpakManager) Name() string { + return "Flatpak" +} + +func (f *FlatpakManager) InstallManager() error { + if _, err := shell.ExecLookPath("flatpak"); err == nil { + return nil + } + + err := installFlatpak(f.OS, f.SudoPassword) + if err != nil { + return err + } + err = addFlatpakRemotes(f.Config.Remotes) + if err != nil { + return err + } + + return nil +} + +type FlatpakConfig struct { + Enable bool `mapstructure:"enable"` + Remotes []Remote `mapstructure:"remotes"` + Packages []string `mapstructure:"packages"` +} + +type Remote struct { + Name string `mapstructure:"name"` + URL string `mapstructure:"url"` +} + +func installFlatpak(os *OS, sudoPassword string) error { + var command string + switch os.PackageManager { + case "pacman": + command = "pacman -S --noconfirm --needed flatpak" + case "apt": + command = "apt install -y flatpak" + case "dnf": + command = "dnf install -y flatpak" + default: + return fmt.Errorf("keine Flatpak-Installation für OS %s definiert", os.ID) + } + + if err := InstallPackage(command, "", sudoPassword); err != nil { + return fmt.Errorf("fehler bei der Flatpak-Installation: %v", err) + } + return nil +} + +func addFlatpakRemotes(remotes []Remote) error { + for _, remote := range remotes { + cmd := shell.ExecCommand("flatpak", "remote-add", "--if-not-exists", remote.Name, remote.URL) + if err := cmd.Run(); err != nil { + return fmt.Errorf("fehler beim Hinzufügen des Remotes %s: %v", remote.Name, err) + } + } + return nil +} + +func (f *FlatpakManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("flatpak", "install", "-y", pkg) + return cmd.Run() +} + +func (f *FlatpakManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("flatpak", "uninstall", "-y", pkg) + return cmd.Run() +} + +func (f *FlatpakManager) SearchPackage(pkg string) []string { + cmd := shell.ExecCommand("flatpak", "search", pkg) + packages, err := cmd.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", f.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (f *FlatpakManager) UpdatePackage(pkg string) error { + cmd := shell.ExecCommand("flatpak", "update", "-y", pkg) + return cmd.Run() +} + +func (f *FlatpakManager) UpdateAllPackages() error { + cmd := shell.ExecCommand("flatpak", "update", "-y") + return cmd.Run() +} + +func (f *FlatpakManager) Check(pkg string) error { + cmd := shell.ExecCommand("/bin/sh", "-c", fmt.Sprintf("flatpak list | grep %s", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/flatpak_test.go b/pkg/packagemanager/flatpak_test.go new file mode 100644 index 0000000..dbf0b98 --- /dev/null +++ b/pkg/packagemanager/flatpak_test.go @@ -0,0 +1,44 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestFlatpakManager_Name(t *testing.T) { + fm := &FlatpakManager{} + if name := fm.Name(); name != "Flatpak" { + t.Errorf("Expected name to be 'Flatpak', got %s", name) + } +} + +func TestFlatpakManager_InstallManager(t *testing.T) { + fm := &FlatpakManager{ + OS: &OS{PackageManager: "apt"}, + SudoPassword: "testpass", + Config: FlatpakConfig{ + Remotes: []Remote{{Name: "test", URL: "http://test.com"}}, + }, + } + + // Mock exec.LookPath and other functions as needed + + if err := fm.InstallManager(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestFlatpakManager_InstallPackage(t *testing.T) { + fm := &FlatpakManager{} + + // Mock exec.Command + shell.ExecCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command("echo", "mocked flatpak install") + } + defer func() { shell.ExecCommand = exec.Command }() + + if err := fm.InstallPackage("test-package"); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} diff --git a/pkg/packagemanager/golang.go b/pkg/packagemanager/golang.go new file mode 100644 index 0000000..6bfb4cc --- /dev/null +++ b/pkg/packagemanager/golang.go @@ -0,0 +1,69 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "os" + "path/filepath" +) + +type GolangManager struct{} + +func (g *GolangManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(g, packages) + if err != nil { + return err + } + return nil +} + +func (g *GolangManager) Name() string { + return "Golang" +} + +func (g *GolangManager) InstallManager() error { + if _, err := shell.ExecLookPath("brew"); err != nil { + installHomebrew() + } + + if _, err := shell.ExecLookPath("go"); err == nil { + return nil + } + + cmd := shell.ExecCommand("brew", "install", "golang") + return cmd.Run() +} + +func (g *GolangManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("go", "install", pkg+"@latest") + return cmd.Run() +} + +func (g *GolangManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("go", "uninstall", pkg) + return cmd.Run() +} + +func (g *GolangManager) SearchPackage(pkg string) []string { + return []string{} +} + +func (g *GolangManager) UpdatePackage(pkg string) error { + return fmt.Errorf("update not supported") +} + +func (g *GolangManager) UpdateAllPackages() error { + return fmt.Errorf("update not supported") +} + +func (g *GolangManager) Check(pkg string) error { + home, err := os.UserHomeDir() + if err != nil { + fmt.Printf("error: %v\n", err) + } + cmd := shell.ExecCommand("ls", filepath.Join(home, "go", "bin", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/golang_test.go b/pkg/packagemanager/golang_test.go new file mode 100644 index 0000000..be7a19d --- /dev/null +++ b/pkg/packagemanager/golang_test.go @@ -0,0 +1,45 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestGolangManager_Name(t *testing.T) { + gm := &GolangManager{} + if name := gm.Name(); name != "Golang" { + t.Errorf("Expected name to be 'Golang', got %s", name) + } +} + +func TestGolangManager_InstallManager(t *testing.T) { + gm := &GolangManager{} + + // Mock exec.LookPath + shell.ExecLookPath = func(file string) (string, error) { + if file == "go" { + return "/usr/local/go/bin/go", nil + } + return "", exec.ErrNotFound + } + defer func() { shell.ExecLookPath = exec.LookPath }() + + if err := gm.InstallManager(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestGolangManager_InstallPackage(t *testing.T) { + gm := &GolangManager{} + + // Mock exec.Command + shell.ExecCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command("echo", "mocked go install") + } + defer func() { shell.ExecCommand = exec.Command }() + + if err := gm.InstallPackage("github.com/test/package"); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} diff --git a/pkg/packagemanager/homebrew.go b/pkg/packagemanager/homebrew.go new file mode 100644 index 0000000..3219ebd --- /dev/null +++ b/pkg/packagemanager/homebrew.go @@ -0,0 +1,76 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type HomebrewManager struct{} + +func (h *HomebrewManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(h, packages) + if err != nil { + return err + } + return nil +} + +func (h *HomebrewManager) Name() string { + return "Homebrew" +} + +func (h *HomebrewManager) InstallManager() error { + if _, err := shell.ExecLookPath("brew"); err == nil { + return nil + } + + return installHomebrew() +} + +func installHomebrew() error { + err := shell.ExecuteShellCommand("sh -c $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)", "NONINTERACTE=1") + if err != nil { + return fmt.Errorf("fehler bei der Installation von Homebrew: %v", err) + } + return nil +} + +func (h *HomebrewManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("brew", "install", pkg) + return cmd.Run() +} + +func (h *HomebrewManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("brew", "uninstall", pkg) + return cmd.Run() +} + +func (h *HomebrewManager) SearchPackage(pkg string) []string { + cmd := shell.ExecCommand("brew", "search", pkg) + packages, err := cmd.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", h.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (h *HomebrewManager) UpdatePackage(pkg string) error { + cmd := shell.ExecCommand("brew", "upgrade", pkg) + return cmd.Run() +} + +func (h *HomebrewManager) UpdateAllPackages() error { + cmd := shell.ExecCommand("brew", "upgrade") + return cmd.Run() +} + +func (h *HomebrewManager) Check(pkg string) error { + cmd := shell.ExecCommand("brew", "list", pkg) + return cmd.Run() +} diff --git a/pkg/packagemanager/homebrew_test.go b/pkg/packagemanager/homebrew_test.go new file mode 100644 index 0000000..983cd8a --- /dev/null +++ b/pkg/packagemanager/homebrew_test.go @@ -0,0 +1,45 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestHomebrewManager_Name(t *testing.T) { + hm := &HomebrewManager{} + if name := hm.Name(); name != "Homebrew" { + t.Errorf("Expected name to be 'Homebrew', got %s", name) + } +} + +func TestHomebrewManager_InstallManager(t *testing.T) { + hm := &HomebrewManager{} + + // Mock exec.LookPath and executeShellCommand + shell.ExecLookPath = func(file string) (string, error) { + if file == "brew" { + return "/usr/local/bin/brew", nil + } + return "", exec.ErrNotFound + } + defer func() { shell.ExecLookPath = exec.LookPath }() + + if err := hm.InstallManager(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestHomebrewManager_InstallPackage(t *testing.T) { + hm := &HomebrewManager{} + + // Mock exec.Command + shell.ExecCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command("echo", "mocked brew install") + } + defer func() { shell.ExecCommand = exec.Command }() + + if err := hm.InstallPackage("test-package"); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} diff --git a/pkg/packagemanager/os.go b/pkg/packagemanager/os.go new file mode 100644 index 0000000..d53f59a --- /dev/null +++ b/pkg/packagemanager/os.go @@ -0,0 +1,268 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "os" + "runtime" + "strings" +) + +type OSManager struct { + OS *OS + SudoPassword string + pm PackageManager +} + +func NewOSManager(sudoPassword string) *OSManager { + switch runtime.GOOS { + case "linux": + os, err := GetLinuxDistribution() + if err != nil { + log.Fatalf("error getting os information: %v", err) + } + var pm PackageManager + switch os.ID { + case "debian", "ubuntu": + pm = &AptManager{SudoPassword: sudoPassword} + case "arch": + pm = &PacmanManager{SudoPassword: sudoPassword} + case "fedora": + pm = &DnfManager{SudoPassword: sudoPassword} + default: + log.Fatalf("Unsupported Linux distribution: %s", os.ID) + } + return &OSManager{ + OS: os, + SudoPassword: sudoPassword, + pm: pm, + } + case "windows": + os, err := getPlatformInfo() + if err != nil { + return nil + } + + if _, err := shell.ExecLookPath("winget"); err == nil { + return &OSManager{ + OS: os, + SudoPassword: "", + pm: &WingetManager{}, + } + } else if _, err := shell.ExecLookPath("choco"); err == nil { + return &OSManager{ + OS: os, + SudoPassword: "", + pm: &ChocoManager{}, + } + } else { + log.Println("Neither Winget nor Chocolatey found on Windows. Package management functionality will be limited.") + } + case "darwin": + os, err := platformInfoMac() + if err != nil { + return nil + } + _, err = shell.ExecLookPath("brew") + if err == nil { + return &OSManager{ + OS: os, + SudoPassword: sudoPassword, + pm: &HomebrewManager{}, + } + } else { + manager := &HomebrewManager{} + manager.InstallManager() + return &OSManager{ + OS: os, + SudoPassword: sudoPassword, + pm: manager, + } + } + default: + log.Fatal("Operating system not supported") + } + return nil +} + +func (o *OSManager) Name() string { + return "OS Package Manager" +} + +func (o *OSManager) InstallManager() error { return nil } + +func (o *OSManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(o.pm, packages) + if err != nil { + return err + } + return nil +} + +func (o *OSManager) InstallPackage(pkg string) error { + return o.pm.InstallPackage(pkg) //(o.OS.InstallCommand, pkg, o.SudoPassword) +} + +func (o *OSManager) InstallBuildEssentials() error { + return InstallBuildEssentials(o.OS, o.SudoPassword) +} + +func (o *OSManager) RemovePackage(pkg string) error { + return o.pm.RemovePackage(pkg) +} + +func (o *OSManager) SearchPackage(pkg string) []string { + return o.pm.SearchPackage(pkg) +} + +func (o *OSManager) UpdatePackage(pkg string) error { + return o.pm.UpdatePackage(pkg) +} + +func (o *OSManager) UpdateAllPackages() error { + return o.pm.UpdateAllPackages() +} + +func (o *OSManager) Check(pkg string) error { + return o.pm.Check(pkg) +} + +type OS struct { + ID string + Name string + Version string + PackageManager string +} + +func parseOsRelease(osRelease string) *OS { + var result OS + result.ID = "Unknown" + result.Name = "Unknown" + result.Version = "Unknown" + + lines := strings.Split(osRelease, "\n") + + for _, line := range lines { + splitLine := strings.SplitN(line, "=", 2) + if len(splitLine) != 2 { + continue + } + switch splitLine[0] { + case "ID": + result.ID = strings.ToLower(strings.Trim(splitLine[1], "\"")) + case "NAME": + result.Name = strings.Trim(splitLine[1], "\"") + case "VERSION_ID": + result.Version = strings.Trim(splitLine[1], "\"") + } + } + err := result.getPackageManager() + if err != nil { + log.Fatal(err) + } + return &result +} + +func GetLinuxDistribution() (*OS, error) { + _, err := os.Stat("/etc/os-release") + if os.IsNotExist(err) { + return nil, fmt.Errorf("unable to read system information") + } + + osRelease, _ := os.ReadFile("/etc/os-release") + return parseOsRelease(string(osRelease)), nil +} + +func InstallBuildEssentials(os *OS, sudoPassword string) error { + var command string + switch os.PackageManager { + case "pacman": + command = "pacman -S --noconfirm --needed base-devel" + case "apt": + command = "apt install -y build-essential" + case "dnf": + command = "dnf install -y @development-tools" + default: + return fmt.Errorf("keine Build Essentials für OS %s definiert", os.ID) + } + + fmt.Printf("Installiere Build Essentials für %s...\n", os.Name) + if err := InstallPackage(command, "", sudoPassword); err != nil { + return fmt.Errorf("error installing Build Essentials: %v", err) + } + return nil +} + +func getSysctlValue(key string) (string, error) { + stdout, err := shell.ExecCommand(".", "sysctl", key).Output() + if err != nil { + return "", err + } + version := strings.TrimPrefix(string(stdout), key+": ") + return strings.TrimSpace(version), nil +} + +func platformInfoMac() (*OS, error) { + var result OS + result.ID = "Unknown" + result.Name = "MacOS" + result.Version = "Unknown" + + version, err := getSysctlValue("kern.osproductversion") + if err != nil { + return nil, err + } + result.Version = version + ID, err := getSysctlValue("kern.osversion") + if err != nil { + return nil, err + } + result.ID = ID + + return &result, nil +} + +func getPlatformInfo() (*OS, error) { + switch runtime.GOOS { + case "windows": + return platformInfoWin() + case "darwin": + return platformInfoMac() + case "linux": + return GetLinuxDistribution() + default: + return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } +} + +func (os *OS) getPackageManager() error { + switch os.ID { + case "debian", "ubuntu": + os.PackageManager = "apt" + return nil + case "arch": + os.PackageManager = "pacman" + return nil + case "fedora": + os.PackageManager = "dnf" + return nil + default: + pmcommands := []string{ + "apt", + "dnf", + "pacman", + } + for _, pmname := range pmcommands { + _, err := shell.ExecLookPath(pmname) + if err == nil { + os.PackageManager = pmname + return nil + } + } + return fmt.Errorf("no packagemanager found fos os: %s", os.Name) + } +} diff --git a/pkg/packagemanager/os_test.go b/pkg/packagemanager/os_test.go new file mode 100644 index 0000000..f4d76eb --- /dev/null +++ b/pkg/packagemanager/os_test.go @@ -0,0 +1,44 @@ +package packagemanager + +import ( + "testing" +) + +func TestNewOSManager(t *testing.T) { + sudoPassword := "testpassword" + + manager := NewOSManager(sudoPassword) + + if manager.OS == nil { + t.Error("Expected OS to be non-nil") + } + if manager.SudoPassword != sudoPassword { + t.Errorf("Expected SudoPassword to be %s, got %s", sudoPassword, manager.SudoPassword) + } +} + +func TestOSManagerName(t *testing.T) { + manager := &OSManager{} + if manager.Name() != "OS Package Manager" { + t.Errorf("Expected name to be 'OS Package Manager', got %s", manager.Name()) + } +} + +func TestParseOsRelease(t *testing.T) { + osReleaseContent := ` +ID=ubuntu +NAME="Ubuntu" +VERSION_ID="20.04" +` + os := parseOsRelease(osReleaseContent) + + if os.ID != "ubuntu" { + t.Errorf("Expected ID to be 'ubuntu', got %s", os.ID) + } + if os.Name != "Ubuntu" { + t.Errorf("Expected Name to be 'Ubuntu', got %s", os.Name) + } + if os.Version != "20.04" { + t.Errorf("Expected Version to be '20.04', got %s", os.Version) + } +} diff --git a/pkg/packagemanager/package.go b/pkg/packagemanager/package.go new file mode 100644 index 0000000..f45157e --- /dev/null +++ b/pkg/packagemanager/package.go @@ -0,0 +1,28 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type Packages struct { + Headless []string `mapstructure:"headless"` + NonHeadless []string `mapstructure:"non_headless"` +} + +func InstallPackage(cmd, pkg, sudoPassword string) error { + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(sudoPassword + "\n") + output, err := command.CombinedOutput() + if err != nil { + if strings.Contains(string(output), "not found") || strings.Contains(string(output), "no matching package") || strings.Contains(string(output), "Keine Übereinstimmung") || strings.Contains(string(output), "Ziel nicht gefunden") { + log.Printf("Package %s not available\n", pkg) + return nil + } + return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output)) + } + return nil +} diff --git a/pkg/packagemanager/packagemanager.go b/pkg/packagemanager/packagemanager.go new file mode 100644 index 0000000..55e5236 --- /dev/null +++ b/pkg/packagemanager/packagemanager.go @@ -0,0 +1,13 @@ +package packagemanager + +type PackageManager interface { + Install(packages []string) error + Check(pkg string) error + InstallPackage(pkg string) error + InstallManager() error + RemovePackage(pkg string) error + SearchPackage(pkg string) []string + UpdatePackage(pkg string) error + UpdateAllPackages() error + Name() string +} diff --git a/pkg/packagemanager/pacman.go b/pkg/packagemanager/pacman.go new file mode 100644 index 0000000..12a5211 --- /dev/null +++ b/pkg/packagemanager/pacman.go @@ -0,0 +1,81 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" + "log" + "strings" +) + +type PacmanManager struct { + SudoPassword string +} + +func (m *PacmanManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(m, packages) + if err != nil { + return err + } + return nil +} + +func (m *PacmanManager) Name() string { + return "OS Home Manager" +} + +func (m *PacmanManager) InstallManager() error { + return nil +} + +func (m *PacmanManager) InstallPackage(pkg string) error { + cmd := "pacman -S --noconfirm --needed" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(m.SudoPassword + "\n") + return command.Run() +} + +func (m *PacmanManager) RemovePackage(pkg string) error { + cmd := "pacman -R" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(m.SudoPassword + "\n") + return command.Run() +} + +func (p *PacmanManager) SearchPackage(pkg string) []string { + cmd := "pacman -Ss" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(p.SudoPassword + "\n") + packages, err := command.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", p.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (p *PacmanManager) UpdatePackage(pkg string) error { + cmd := "pacman -S" + fullCmd := fmt.Sprintf("%s %s", cmd, pkg) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(p.SudoPassword + "\n") + return command.Run() +} + +func (p *PacmanManager) UpdateAllPackages() error { + cmd := "apt -S" + fullCmd := fmt.Sprintf("%s", cmd) + command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd) + command.Stdin = strings.NewReader(p.SudoPassword + "\n") + return command.Run() +} + +func (p *PacmanManager) Check(pkg string) error { + cmd := shell.ExecCommand("/bin/sh", "-c", fmt.Sprintf("paxman -Q | grep %s", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/pipx.go b/pkg/packagemanager/pipx.go new file mode 100644 index 0000000..c2fd295 --- /dev/null +++ b/pkg/packagemanager/pipx.go @@ -0,0 +1,66 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "fmt" +) + +type PipxManager struct{} + +func (p *PipxManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + + err := InstallWithProgress(p, packages) + if err != nil { + return err + } + return nil +} + +func (p *PipxManager) Name() string { + return "Pipx" +} + +func (p *PipxManager) InstallManager() error { + if _, err := shell.ExecLookPath("brew"); err != nil { + installHomebrew() + } + + if _, err := shell.ExecLookPath("pipx"); err == nil { + return nil + } + + cmd := shell.ExecCommand("brew", "install", "pipx") + return cmd.Run() +} + +func (p *PipxManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("pipx", "install", pkg) + return cmd.Run() +} + +func (p *PipxManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("pipx", "uninstall", pkg) + return cmd.Run() +} + +func (p *PipxManager) SearchPackage(pkg string) []string { + return []string{} +} + +func (p *PipxManager) UpdatePackage(pkg string) error { + cmd := shell.ExecCommand("pipx", "upgrade", pkg) + return cmd.Run() +} + +func (p *PipxManager) UpdateAllPackages() error { + cmd := shell.ExecCommand("pipx", "upgrade-all") + return cmd.Run() +} + +func (p *PipxManager) Check(pkg string) error { + cmd := shell.ExecCommand("/bin/sh", "-c", fmt.Sprintf("pipx list | grep %s", pkg)) + return cmd.Run() +} diff --git a/pkg/packagemanager/pipx_test.go b/pkg/packagemanager/pipx_test.go new file mode 100644 index 0000000..870cce2 --- /dev/null +++ b/pkg/packagemanager/pipx_test.go @@ -0,0 +1,45 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "os/exec" + "testing" +) + +func TestPipxManager_Name(t *testing.T) { + pm := &PipxManager{} + if name := pm.Name(); name != "Pipx" { + t.Errorf("Expected name to be 'Pipx', got %s", name) + } +} + +func TestPipxManager_InstallManager(t *testing.T) { + pm := &PipxManager{} + + // Mock exec.LookPath + shell.ExecLookPath = func(file string) (string, error) { + if file == "pipx" { + return "/usr/bin/pipx", nil + } + return "", exec.ErrNotFound + } + defer func() { shell.ExecLookPath = exec.LookPath }() + + if err := pm.InstallManager(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestPipxManager_InstallPackage(t *testing.T) { + pm := &PipxManager{} + + // Mock exec.Command + shell.ExecCommand = func(name string, arg ...string) *exec.Cmd { + return exec.Command("echo", "mocked pipx install") + } + defer func() { shell.ExecCommand = exec.Command }() + + if err := pm.InstallPackage("test-package"); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} diff --git a/pkg/packagemanager/utils.go b/pkg/packagemanager/utils.go new file mode 100644 index 0000000..293679c --- /dev/null +++ b/pkg/packagemanager/utils.go @@ -0,0 +1,19 @@ +package packagemanager + +import ( + "log" + + "github.com/schollz/progressbar/v3" +) + +func InstallWithProgress(manager PackageManager, packages []string) error { + bar := progressbar.Default(int64(len(packages)), "Installiere "+manager.Name()+"-Pakete...") + for _, pkg := range packages { + err := manager.InstallPackage(pkg) + if err != nil { + log.Printf("\nError installing %s: %v\n", pkg, err) + } + bar.Add(1) + } + return nil +} diff --git a/pkg/packagemanager/win.go b/pkg/packagemanager/win.go new file mode 100644 index 0000000..4488b57 --- /dev/null +++ b/pkg/packagemanager/win.go @@ -0,0 +1,30 @@ +//go:build windows +// +build windows + +package packagemanager + +import ( + "fmt" + + "golang.org/x/sys/windows/registry" +) + +func platformInfoWin() (*OS, error) { + var result OS + result.ID = "Unknown" + result.Name = "Windows" + result.Version = "Unknown" + + key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + + productName, _, _ := key.GetStringValue("ProductName") + currentBuild, _, _ := key.GetStringValue("CurrentBuildNumber") + displayVersion, _, _ := key.GetStringValue("DisplayVersion") + releaseId, _, _ := key.GetStringValue("ReleaseId") + + result.Name = productName + result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild) + result.ID = displayVersion + + return &result, key.Close() +} diff --git a/pkg/packagemanager/win_dummy.go b/pkg/packagemanager/win_dummy.go new file mode 100644 index 0000000..b98c8d2 --- /dev/null +++ b/pkg/packagemanager/win_dummy.go @@ -0,0 +1,8 @@ +//go:build linux || darwin +// +build linux darwin + +package packagemanager + +func platformInfoWin() (*OS, error) { + return nil, nil +} diff --git a/pkg/packagemanager/winget.go b/pkg/packagemanager/winget.go new file mode 100644 index 0000000..777ec83 --- /dev/null +++ b/pkg/packagemanager/winget.go @@ -0,0 +1,71 @@ +package packagemanager + +import ( + "codeberg.org/Pata1704/system_setup_tool/internal/shell" + "log" + "strings" +) + +type WingetManager struct{} + +func (w *WingetManager) Install(packages []string) error { + if len(packages) == 0 { + return nil + } + err := InstallWithProgress(w, packages) + if err != nil { + return err + } + return nil +} + +func (w *WingetManager) Name() string { + return "Winget" +} + +func (w *WingetManager) InstallManager() error { + if _, err := shell.ExecLookPath("winget"); err == nil { + return nil + } + + return installHomebrew() +} + +func installWinget() error { + return nil +} + +func (w *WingetManager) InstallPackage(pkg string) error { + cmd := shell.ExecCommand("winget", "install", pkg) + return cmd.Run() +} + +func (w *WingetManager) RemovePackage(pkg string) error { + cmd := shell.ExecCommand("winget", "uninstall", pkg) + return cmd.Run() +} + +func (w *WingetManager) SearchPackage(pkg string) []string { + cmd := shell.ExecCommand("winget", "search", pkg) + packages, err := cmd.Output() + if err != nil { + log.Printf("error fetching %s packages: %v", w.Name(), err) + } + packageList := strings.Split(strings.TrimSpace(string(packages)), "\n") + return packageList +} + +func (w *WingetManager) UpdatePackage(pkg string) error { + cmd := shell.ExecCommand("winget", "update", pkg) + return cmd.Run() +} + +func (w *WingetManager) UpdateAllPackages() error { + cmd := shell.ExecCommand("winget", "update") + return cmd.Run() +} + +func (w *WingetManager) Check(pkg string) error { + cmd := shell.ExecCommand("wiget", "show", pkg) + return cmd.Run() +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} diff --git a/specialSoftware.go b/specialSoftware.go deleted file mode 100644 index 2087002..0000000 --- a/specialSoftware.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os/exec" - "runtime" - - "github.com/charmbracelet/bubbles/progress" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type specialSoftwareModel struct { - items []string - index int - spinner spinner.Model - progress progress.Model - done bool - sudoPassword string -} - -func newSpecialSoftwareModel(sudoPassword string) specialSoftwareModel { - items := []string{"oh-my-posh", "golang", "rust"} - - p := progress.New( - progress.WithDefaultGradient(), - progress.WithWidth(40), - progress.WithoutPercentage(), - ) - s := spinner.New() - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) - - return specialSoftwareModel{ - items: items, - spinner: s, - progress: p, - sudoPassword: sudoPassword, - } -} - -func (m specialSoftwareModel) Init() tea.Cmd { - return tea.Batch(m.installItemCmd(m.items[m.index]), m.spinner.Tick) -} - -func (m specialSoftwareModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "esc", "q": - return m, tea.Quit - } - case installedItemMsg: - if m.index >= len(m.items)-1 { - m.done = true - return m, tea.Quit - } - - m.index++ - progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.items))) - - return m, tea.Batch( - progressCmd, - tea.Printf("%s %s", checkMark, m.items[m.index-1]), - m.installItemCmd(m.items[m.index]), - ) - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - } - return m, nil -} - -func (m specialSoftwareModel) View() string { - if m.done { - return doneStyle.Render("Spezielle Software Installation abgeschlossen!\n") - } - - spin := m.spinner.View() + " " - prog := m.progress.View() - info := fmt.Sprintf("Installiere %s", m.items[m.index]) - - return spin + info + " " + prog -} - -func (m specialSoftwareModel) installItemCmd(item string) tea.Cmd { - return func() tea.Msg { - switch item { - case "oh-my-posh": - if _, err := exec.LookPath("oh-my-posh"); err == nil { - return installedItemMsg(item) - } - err := executeShellCommand("curl -s https://ohmyposh.dev/install.sh | bash -s") - if err != nil { - log.Printf("Fehler bei der Installation von oh-my-posh: %v\n", err) - } - case "golang": - if _, err := exec.LookPath("go"); err == nil { - return installedItemMsg(item) - } - golangVersion := "1.23.4" - if err := downloadGolang(golangVersion); err != nil { - log.Printf("Fehler beim Herunterladen von Go: %v", err) - } - golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH) - if err := installPackage(golangCommand, "", m.sudoPassword); err != nil { - log.Printf("Fehler beim Installieren von Go: %v", err) - } - case "rust": - if _, err := exec.LookPath("rustc"); err == nil { - if _, err := exec.LookPath("cargo"); err == nil { - return installedItemMsg(item) - } - } - err := executeShellCommand("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y") - if err != nil { - log.Printf("Fehler bei der Installation von rustup: %v\n", err) - } - } - return installedItemMsg(item) - } -} - -func executeShellCommand(command string) error { - cmd := exec.Command("bash", "-c", command) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("befehl fehlgeschlagen: %v\nAusgabe: %s", err, output) - } - return nil -} diff --git a/tui.go b/tui.go deleted file mode 100644 index f542b21..0000000 --- a/tui.go +++ /dev/null @@ -1,166 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os/exec" - - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) - doneStyle = lipgloss.NewStyle().Margin(1, 2) - checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") - unavailablePackages []string -) - -type installedItemMsg string - -type installedPkgMsg string - -func run(cmd *cobra.Command, args []string) { - os, err := getLinuxDistribution() - if err != nil { - log.Fatal(err) - } - - sudoPassword, err := getSudoPassword() - if err != nil { - log.Fatal(err) - } - - var cfg Config - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatalf("Fehler beim Lesen der Konfiguration: %v", err) - } - - form := huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Möchten Sie eine headless Installation durchführen?"). - Value(&cfg.Headless), - ), - ).WithTheme(huh.ThemeCatppuccin()) - - if err := form.Run(); err != nil { - log.Fatalf("Fehler bei der Benutzerabfrage: %v", err) - } - - var packages []string - packages = append(packages, cfg.Packages.Headless...) - if !cfg.Headless { - packages = append(packages, cfg.Packages.NonHeadless...) - } - - unavailablePackages = []string{} - - m := newModel(packages, sudoPassword, os) - p := tea.NewProgram(m) - if _, err := p.Run(); err != nil { - log.Fatal(err) - } - - printUnavailablePackages() - - var installSpecial bool - form = huh.NewForm( - huh.NewGroup( - huh.NewConfirm(). - Title("Möchten Sie die speziellen Pakete (Go, Rust, Pipx) installieren?"). - Value(&installSpecial), - ), - ).WithTheme(huh.ThemeCatppuccin()) - - err = form.Run() - if err != nil { - log.Fatal(err) - } - - if installSpecial { - if err := installBuildEssentials(os, sudoPassword); err != nil { - log.Printf("Warnung: %v", err) - } - sm := newSpecialSoftwareModel(sudoPassword) - p = tea.NewProgram(sm) - if _, err := p.Run(); err != nil { - log.Fatal(err) - } - } - - if len(cfg.SpecialPackages.Go) > 0 || len(cfg.SpecialPackages.Cargo) > 0 || len(cfg.SpecialPackages.Pipx) > 0 { - var allSpecialPkgs []struct { - typ string - name string - command string - } - - for _, pkg := range cfg.SpecialPackages.Go { - allSpecialPkgs = append(allSpecialPkgs, struct { - typ string - name string - command string - }{"go", pkg, "go install " + pkg + "@latest"}) - } - - for _, pkg := range cfg.SpecialPackages.Cargo { - allSpecialPkgs = append(allSpecialPkgs, struct { - typ string - name string - command string - }{"cargo", pkg, "cargo install " + pkg}) - } - - for _, pkg := range cfg.SpecialPackages.Pipx { - allSpecialPkgs = append(allSpecialPkgs, struct { - typ string - name string - command string - }{"pipx", pkg, "pipx install " + pkg}) - } - - if len(allSpecialPkgs) > 0 { - fmt.Println("\nInstalliere spezielle Pakete...") - p := progress.New( - progress.WithDefaultGradient(), - progress.WithWidth(40), - ) - - for i, pkg := range allSpecialPkgs { - p.SetPercent(float64(i) / float64(len(allSpecialPkgs))) - fmt.Printf("Installiere %s Paket: %s\n", pkg.typ, pkg.name) - - cmd := exec.Command("sh", "-c", pkg.command) - if err := cmd.Run(); err != nil { - log.Printf("Fehler bei der Installation von %s: %v", pkg.name, err) - continue - } - } - } - } - if cfg.Flatpak.Enable { - fmt.Println("\nKonfiguriere Flatpak...") - if err := installFlatpak(os, sudoPassword); err != nil { - log.Printf("Warnung bei Flatpak-Installation: %v", err) - } - - if err := addFlatpakRemotes(cfg.Flatpak.Remotes); err != nil { - log.Printf("Warnung bei Flatpak-Remotes: %v", err) - } - - if err := installFlatpakPackages(cfg.Flatpak.Packages); err != nil { - log.Printf("Warnung bei Flatpak-Paketen: %v", err) - } - } - if cfg.Dotfiles.Enable { - fmt.Println("\nKonfiguriere Dotfiles...") - if err := setupDotfiles(cfg.Dotfiles); err != nil { - log.Printf("Warnung bei Dotfiles-Setup: %v", err) - } - } -} diff --git a/utils.go b/utils.go deleted file mode 100644 index 8698bc4..0000000 --- a/utils.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "os" - "runtime" - - "github.com/charmbracelet/huh" -) - -func getSudoPassword() (string, error) { - var password string - form := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Bitte geben Sie Ihr sudo-Passwort ein"). - EchoMode(huh.EchoModePassword). - Value(&password), - ), - ).WithTheme(huh.ThemeCatppuccin()) - - err := form.Run() - if err != nil { - return "", fmt.Errorf("fehler bei der Passwortabfrage: %v", err) - } - return password, nil -} - -func downloadGolang(golangVersion string) error { - var link string - if runtime.GOARCH == "arm64" { - link = "https://go.dev/dl/go" + golangVersion + ".linux-arm64.tar.gz" - } else { - link = "https://go.dev/dl/go" + golangVersion + ".linux-amd64.tar.gz" - } - resp, err := http.Get(link) - if err != nil { - return fmt.Errorf("fehler beim Herunterladen von Go: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unerwarteter HTTP-Status: %s", resp.Status) - } - - outFile, err := os.Create("go" + golangVersion + ".linux-" + runtime.GOARCH + ".tar.gz") - if err != nil { - return fmt.Errorf("fehler beim Erstellen der Ausgabedatei: %v", err) - } - defer outFile.Close() - - _, err = io.Copy(outFile, resp.Body) - if err != nil { - return fmt.Errorf("fehler beim Schreiben der Ausgabedatei: %v", err) - } - - return nil -} - -func max(a, b int) int { - if a > b { - return a - } - return b -}