Merge branch 'development'

* development:
  docs: update changelog.md
  refactor: move main.go back to project root
  refactor: seperate files, function and structs into seperate packages
  fix(cli): add missing yes flag in flatpak remove cmd
  feat(cli): cleanup install command and add remove and search command
  feat(cli): add first implementation for install command
  feat(cli): implement search argument
This commit is contained in:
Patryk Hegenberg 2025-01-18 16:56:33 +01:00
commit cc3cc5c916
42 changed files with 939 additions and 760 deletions

View file

@ -1,20 +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 # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj # vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2 version: 2
env:
- GITHUB_TOKEN={{ .Env.CODEBERG_TOKEN }}
before: before:
hooks: hooks:
# You may remove this if you don't use go modules.
- go mod tidy - go mod tidy
# you may remove this if you don't need go generate
- go generate ./... - go generate ./...
- go test ./... - go test ./... -v
builds: builds:
- env: - env:
@ -26,7 +22,6 @@ builds:
archives: archives:
- format: tar.gz - format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >- name_template: >-
{{ .ProjectName }}_ {{ .ProjectName }}_
{{- title .Os }}_ {{- title .Os }}_
@ -34,7 +29,6 @@ archives:
{{- else if eq .Arch "386" }}i386 {{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }} {{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip format: zip

View file

@ -1,36 +0,0 @@
package main
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 := execLookPath("brew"); err != nil {
installHomebrew()
}
if _, err := execLookPath("cargo"); err == nil {
return nil
}
cmd := execCommand("brew", "install", "rust")
return cmd.Run()
}
func (c *CargoManager) InstallPackage(pkg string) error {
cmd := execCommand("cargo", "install", pkg)
return cmd.Run()
}

View file

@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [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
## [0.2.0] - 2025-01-16 ## [0.2.0] - 2025-01-16
### 🚀 Features ### 🚀 Features
@ -18,6 +35,7 @@ All notable changes to this project will be documented in this file.
### 📚 Documentation ### 📚 Documentation
- Update changelog.md
- Update changelog.md - Update changelog.md
### ⚙️ Miscellaneous Tasks ### ⚙️ Miscellaneous Tasks

View file

@ -1,23 +1,24 @@
package main package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"system_setup_tool/tui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var rootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "system_setup_tool", Use: "system_setup_tool",
Short: "Installs packages based on TOML configuration", Short: "Installs packages based on TOML configuration",
Run: run, Run: tui.Run,
} }
func init() { func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringP("config", "c", "", "Path to the configuration file") RootCmd.PersistentFlags().StringP("config", "c", "", "Path to the configuration file")
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config"))
addCmd.Flags().StringP("name", "n", "", "The name of the package you want to add") 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().StringP("manager", "m", "", "The package manager you want to add the package to (homebrew|cargo|flatpak|pipx|go)")
@ -31,8 +32,14 @@ func init() {
enableCmd.Flags().Bool("value", true, "Set to true to enable, false to disable") 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)")
packageCmd.AddCommand(addCmd, deleteCmd, showCmd, enableCmd) packageCmd.AddCommand(addCmd, deleteCmd, showCmd, enableCmd)
rootCmd.AddCommand(packageCmd) RootCmd.AddCommand(packageCmd, searchCmd, installCmd, removeCmd)
} }
func initConfig() { func initConfig() {

46
cmd/install_cmd.go Normal file
View file

@ -0,0 +1,46 @@
package cmd
import (
"fmt"
"log"
"system_setup_tool/utils"
pm "system_setup_tool/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),
Run: func(cmd *cobra.Command, args []string) {
packageName := args[0]
managerName, _ := cmd.Flags().GetString("manager")
var manager pm.PackageManager
switch managerName {
case "os":
sudoPassword, err := utils.GetSudoPassword()
if err != nil {
log.Fatal(err)
}
osManager := pm.NewOSManager(sudoPassword)
if err := osManager.Install([]string{packageName}); err != nil {
log.Printf("error: %v\n", err)
}
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.InstallPackage(packageName); err != nil {
log.Printf("error: %v\n", err)
}
},
}

View file

@ -1,4 +1,4 @@
package main package cmd
import ( import (
"fmt" "fmt"
@ -31,7 +31,6 @@ var addCmd = &cobra.Command{
packages = viper.GetStringSlice("packages.non_headless") packages = viper.GetStringSlice("packages.non_headless")
} }
// Add package to the appropriate list
packages = append(packages, name) packages = append(packages, name)
if isHeadless { if isHeadless {

44
cmd/remove_cmd.go Normal file
View file

@ -0,0 +1,44 @@
package cmd
import (
"log"
"system_setup_tool/utils"
pm "system_setup_tool/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":
sudoPassword, err := utils.GetSudoPassword()
if err != nil {
log.Fatal(err)
}
osManager := pm.NewOSManager(sudoPassword)
if err := osManager.Install([]string{packageName}); err != nil {
log.Printf("error: %v\n", err)
}
case "homebrew":
manager = &pm.HomebrewManager{}
case "cargo":
manager = &pm.CargoManager{}
case "pipx":
manager = &pm.PipxManager{}
case "flatpak":
manager = &pm.FlatpakManager{}
default:
}
if err := manager.RemovePackage(packageName); err != nil {
log.Printf("error: %v\n", err)
}
},
}

61
cmd/search_cmd.go Normal file
View file

@ -0,0 +1,61 @@
package cmd
import (
"fmt"
"strings"
pm "system_setup_tool/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),
Run: func(cmd *cobra.Command, args []string) {
packageName := args[0]
managerName, _ := cmd.Flags().GetString("manager")
if managerName == "os" {
managerName = "OS Package Manager"
}
managers := []pm.PackageManager{
pm.NewOSManager(""),
&pm.HomebrewManager{},
&pm.FlatpakManager{},
}
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
}
for _, manager := range managers {
searchAndDisplayResults(manager, 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()
}

View file

@ -12,20 +12,107 @@ packages = ['typst-cli']
[package_managers.flatpak] [package_managers.flatpak]
enable = true enable = true
packages = ['com.spotify.Client', 'us.zoom.Zoom', 'org.zotero.Zotero', 'com.google.AndroidStudio', 'io.freetubeapp.FreeTube', 'com.discordapp.Discord', 'com.nextcloud.desktopclient.nextcloud', 'com.github.tchx84.Flatseal', 'io.github.flattool.Warehouse', 'org.onlyoffice.desktopeditors'] packages = [
'com.spotify.Client',
'us.zoom.Zoom',
'org.zotero.Zotero',
'com.google.AndroidStudio',
'io.freetubeapp.FreeTube',
'com.discordapp.Discord',
'com.nextcloud.desktopclient.nextcloud',
'com.github.tchx84.Flatseal',
'io.github.flattool.Warehouse',
'org.onlyoffice.desktopeditors',
]
[package_managers.go] [package_managers.go]
enable = true enable = true
packages = ['github.com/stefanlogue/meteor', 'golang.org/x/tools/gopls', 'github.com/go-delve/delve/cmd/dlv', 'github.com/air-verse/air'] packages = [
'github.com/stefanlogue/meteor',
'golang.org/x/tools/gopls',
'github.com/go-delve/delve/cmd/dlv',
'github.com/air-verse/air',
]
[package_managers.homebrew] [package_managers.homebrew]
enable = true enable = true
packages = ['fd', 'fzf', 'ripgrep', 'neovim', 'helix', 'node', 'yazi', 'zk', 'bat', 'bottom', 'btop', 'git-cliff', 'glow', 'lazygit', 'goreleaser', 'harlequin', 'mage', 'posting', 'typst', 'wails', 'zoxide', 'lsd', 'jq', 'yq', 'timewarrior', 'tmux', 'fastfetch', 'stow', 'distrobox'] packages = [
'oh-my-posh',
'fd',
'fzf',
'ripgrep',
'neovim',
'helix',
'node',
'yazi',
'zk',
'bat',
'bottom',
'btop',
'git-cliff',
'glow',
'lazygit',
'goreleaser',
'harlequin',
'mage',
'posting',
'typst',
'wails',
'zoxide',
'lsd',
'jq',
'yq',
'timewarrior',
'tmux',
'fastfetch',
'stow',
'distrobox',
]
[package_managers.pipx] [package_managers.pipx]
enable = true enable = true
packages = ['euporie'] packages = ['euporie']
[packages] [package_managers.os]
headless = ['git', 'curl', 'wget', 'biber', 'bear', 'docker', 'docker-compose', 'zsh', 'npm', 'task', 'tree-sitter-cli', 'python3-pip', 'latexmk', 'luarocks'] enable = true
non_headless = ['flameshot', 'fuzzel', 'hyprcursor', 'hypridle', 'hyprland', 'hyprland-qtutils', 'hyprlock', 'hyprpaper', 'hyprutils', 'kitty', 'mako', 'SwayNotificationCenter', 'SwayNotificationCenter-zsh-completion', 'mpv', 'pidgin', 'remmina', 'thunderbird-i18n-de', 'virt-manager', 'vlc', 'waybar', 'nwg-panel', 'nwg-look', 'xdg-desktop-portal-hyprland', 'zathura', 'zathura-pdf-mupdf'] packages = [
'git',
'curl',
'wget',
'biber',
'bear',
'docker',
'docker-compose',
'zsh',
'npm',
'task',
'tree-sitter-cli',
'python3-pip',
'latexmk',
'luarocks',
'flameshot',
'fuzzel',
'hyprcursor',
'hypridle',
'hyprland',
'hyprland-qtutils',
'hyprlock',
'hyprpaper',
'hyprutils',
'kitty',
'mako',
'SwayNotificationCenter',
'SwayNotificationCenter-zsh-completion',
'mpv',
'pidgin',
'remmina',
'thunderbird-i18n-de',
'virt-manager',
'vlc',
'waybar',
'nwg-panel',
'nwg-look',
'xdg-desktop-portal-hyprland',
'zathura',
'zathura-pdf-mupdf',
]

View file

@ -1,4 +1,9 @@
package main package config
import (
"system_setup_tool/dotfiles"
pm "system_setup_tool/packagemanager"
)
type PackageManagerConfig struct { type PackageManagerConfig struct {
Enable bool `mapstructure:"enable"` Enable bool `mapstructure:"enable"`
@ -7,7 +12,7 @@ type PackageManagerConfig struct {
type Config struct { type Config struct {
Headless bool `mapstructure:"headless"` Headless bool `mapstructure:"headless"`
Packages Packages `mapstructure:"packages"` Packages pm.Packages `mapstructure:"packages"`
PackageManagers map[string]PackageManagerConfig `mapstructure:"package_managers"` PackageManagers map[string]PackageManagerConfig `mapstructure:"package_managers"`
Dotfiles DotfilesConfig `mapstructure:"dotfiles"` Dotfiles dotfiles.DotfilesConfig `mapstructure:"dotfiles"`
} }

View file

@ -1,42 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
)
type DotfilesConfig struct {
Enable bool `mapstructure:"enable"`
GitRepo string `mapstructure:"git_repo"`
}
func setupDotfiles(config DotfilesConfig) error {
if _, err := execLookPath("git"); err != nil {
return fmt.Errorf("git ist nicht installiert")
}
if _, err := execLookPath("stow"); err != nil {
return fmt.Errorf("gnu stow ist nicht installiert")
}
dotfilesDir := filepath.Join(os.Getenv("HOME"), "dotfiles")
cmd := execCommand("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 = execCommand("stow", ".", "--override='*'")
if err := cmd.Run(); err != nil {
log.Printf("Fehler beim Linken: %v", err)
}
fmt.Printf("Alles erfolgreich verlinkt\n")
return nil
}

43
dotfiles/dotfiles.go Normal file
View file

@ -0,0 +1,43 @@
package dotfiles
import (
"fmt"
"log"
"os"
"path/filepath"
"system_setup_tool/internal/shell"
)
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
}

7
go.mod
View file

@ -3,10 +3,7 @@ module system_setup_tool
go 1.23.4 go 1.23.4
require ( require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/huh v0.6.0 github.com/charmbracelet/huh v0.6.0
github.com/charmbracelet/lipgloss v1.0.0
github.com/magefile/mage v1.15.0 github.com/magefile/mage v1.15.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/schollz/progressbar/v3 v3.18.0 github.com/schollz/progressbar/v3 v3.18.0
@ -18,7 +15,9 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.2.0 // indirect github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.2.4 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // 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/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect

2
go.sum
View file

@ -10,8 +10,6 @@ github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQW
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 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 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 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 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= 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 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=

View file

@ -1,36 +0,0 @@
package main
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 := execLookPath("brew"); err != nil {
installHomebrew()
}
if _, err := execLookPath("go"); err == nil {
return nil
}
cmd := execCommand("brew", "install", "golang")
return cmd.Run()
}
func (g *GolangManager) InstallPackage(pkg string) error {
cmd := execCommand("go", "install", pkg+"@latest")
return cmd.Run()
}

View file

@ -1,43 +0,0 @@
package main
import (
"fmt"
)
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 := execLookPath("brew"); err == nil {
return nil
}
return installHomebrew()
}
func installHomebrew() error {
err := 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 := execCommand("brew", "install", pkg)
return cmd.Run()
}

23
internal/shell/shell.go Normal file
View file

@ -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
}

View file

@ -1,5 +1,4 @@
//go:build mage //go:build mage
// +build mage
package main package main

View file

@ -3,10 +3,11 @@ package main
import ( import (
"log" "log"
"os" "os"
"system_setup_tool/cmd"
) )
func main() { func main() {
if err := rootCmd.Execute(); err != nil { if err := cmd.RootCmd.Execute(); err != nil {
log.Println(err) log.Println(err)
os.Exit(1) os.Exit(1)
} }

164
model.go
View file

@ -1,164 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"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 := execLookPath("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
}
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("Fehler beim Ermitteln des Home-Verzeichnisses: %v", err)
}
ohMyZshDir := filepath.Join(homeDir, ".oh-my-zsh")
if _, err := os.Stat(ohMyZshDir); !os.IsNotExist(err) {
fmt.Println("Oh My Zsh ist bereits installiert.")
return nil
}
fmt.Println("Installiere Oh My Zsh...")
err = executeShellCommand(`sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"`, "")
if err != nil {
log.Printf("Fehler bei der Installation von Oh My Zsh: %v\n", err)
}
plugins := []string{
"git clone https://github.com/zsh-users/zsh-autosuggestions.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions",
"git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting",
"git clone https://github.com/zdharma-continuum/fast-syntax-highlighting.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/fast-syntax-highlighting",
"git clone --depth 1 -- https://github.com/marlonrichert/zsh-autocomplete.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autocomplete",
}
for _, plugin := range plugins {
err := executeShellCommand(plugin, "")
if err != nil {
log.Printf("Fehler bei der Installation des Plugins: %v\n", err)
}
}
fmt.Println("Oh My Zsh wurde erfolgreich installiert.")
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)
}
}

47
packagemanager/cargo.go Normal file
View file

@ -0,0 +1,47 @@
package packagemanager
import "system_setup_tool/internal/shell"
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{}
}

View file

@ -1,8 +1,9 @@
// cargo_test.go // cargo_test.go
package main package packagemanager
import ( import (
"os/exec" "os/exec"
"system_setup_tool/internal/shell"
"testing" "testing"
) )
@ -17,13 +18,13 @@ func TestCargoManager_InstallManager(t *testing.T) {
cm := &CargoManager{} cm := &CargoManager{}
// Mock exec.LookPath // Mock exec.LookPath
execLookPath = func(file string) (string, error) { shell.ExecLookPath = func(file string) (string, error) {
if file == "cargo" { if file == "cargo" {
return "/usr/bin/cargo", nil return "/usr/bin/cargo", nil
} }
return "", exec.ErrNotFound return "", exec.ErrNotFound
} }
defer func() { execLookPath = exec.LookPath }() defer func() { shell.ExecLookPath = exec.LookPath }()
if err := cm.InstallManager(); err != nil { if err := cm.InstallManager(); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)
@ -34,10 +35,10 @@ func TestCargoManager_InstallPackage(t *testing.T) {
cm := &CargoManager{} cm := &CargoManager{}
// Mock exec.Command // Mock exec.Command
execCommand = func(name string, arg ...string) *exec.Cmd { shell.ExecCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("echo", "mocked cargo install") return exec.Command("echo", "mocked cargo install")
} }
defer func() { execCommand = exec.Command }() defer func() { shell.ExecCommand = exec.Command }()
if err := cm.InstallPackage("test-package"); err != nil { if err := cm.InstallPackage("test-package"); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)

View file

@ -1,8 +1,10 @@
package main package packagemanager
import ( import (
"fmt" "fmt"
"os/exec" "log"
"strings"
"system_setup_tool/internal/shell"
) )
type FlatpakManager struct { type FlatpakManager struct {
@ -11,11 +13,23 @@ type FlatpakManager struct {
Config FlatpakConfig 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 { func (f *FlatpakManager) Install(packages []string) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
} }
err := installWithProgress(f, packages) err := InstallWithProgress(f, packages)
if err != nil { if err != nil {
return err return err
} }
@ -27,7 +41,7 @@ func (f *FlatpakManager) Name() string {
} }
func (f *FlatpakManager) InstallManager() error { func (f *FlatpakManager) InstallManager() error {
if _, err := exec.LookPath("flatpak"); err == nil { if _, err := shell.ExecLookPath("flatpak"); err == nil {
return nil return nil
} }
@ -67,7 +81,7 @@ func installFlatpak(os *OS, sudoPassword string) error {
return fmt.Errorf("keine Flatpak-Installation für OS %s definiert", os.ID) return fmt.Errorf("keine Flatpak-Installation für OS %s definiert", os.ID)
} }
if err := installPackage(command, "", sudoPassword); err != nil { if err := InstallPackage(command, "", sudoPassword); err != nil {
return fmt.Errorf("fehler bei der Flatpak-Installation: %v", err) return fmt.Errorf("fehler bei der Flatpak-Installation: %v", err)
} }
return nil return nil
@ -75,7 +89,7 @@ func installFlatpak(os *OS, sudoPassword string) error {
func addFlatpakRemotes(remotes []Remote) error { func addFlatpakRemotes(remotes []Remote) error {
for _, remote := range remotes { for _, remote := range remotes {
cmd := execCommand("flatpak", "remote-add", "--if-not-exists", remote.Name, remote.URL) cmd := shell.ExecCommand("flatpak", "remote-add", "--if-not-exists", remote.Name, remote.URL)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("fehler beim Hinzufügen des Remotes %s: %v", remote.Name, err) return fmt.Errorf("fehler beim Hinzufügen des Remotes %s: %v", remote.Name, err)
} }
@ -84,6 +98,21 @@ func addFlatpakRemotes(remotes []Remote) error {
} }
func (f *FlatpakManager) InstallPackage(pkg string) error { func (f *FlatpakManager) InstallPackage(pkg string) error {
cmd := execCommand("flatpak", "install", "-y", pkg) cmd := shell.ExecCommand("flatpak", "install", "-y", pkg)
return cmd.Run() 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
}

View file

@ -1,8 +1,8 @@
// flatpak_test.go package packagemanager
package main
import ( import (
"os/exec" "os/exec"
"system_setup_tool/internal/shell"
"testing" "testing"
) )
@ -33,10 +33,10 @@ func TestFlatpakManager_InstallPackage(t *testing.T) {
fm := &FlatpakManager{} fm := &FlatpakManager{}
// Mock exec.Command // Mock exec.Command
execCommand = func(name string, arg ...string) *exec.Cmd { shell.ExecCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("echo", "mocked flatpak install") return exec.Command("echo", "mocked flatpak install")
} }
defer func() { execCommand = exec.Command }() defer func() { shell.ExecCommand = exec.Command }()
if err := fm.InstallPackage("test-package"); err != nil { if err := fm.InstallPackage("test-package"); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)

47
packagemanager/golang.go Normal file
View file

@ -0,0 +1,47 @@
package packagemanager
import "system_setup_tool/internal/shell"
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{}
}

View file

@ -1,8 +1,8 @@
// golang_test.go package packagemanager
package main
import ( import (
"os/exec" "os/exec"
"system_setup_tool/internal/shell"
"testing" "testing"
) )
@ -17,13 +17,13 @@ func TestGolangManager_InstallManager(t *testing.T) {
gm := &GolangManager{} gm := &GolangManager{}
// Mock exec.LookPath // Mock exec.LookPath
execLookPath = func(file string) (string, error) { shell.ExecLookPath = func(file string) (string, error) {
if file == "go" { if file == "go" {
return "/usr/local/go/bin/go", nil return "/usr/local/go/bin/go", nil
} }
return "", exec.ErrNotFound return "", exec.ErrNotFound
} }
defer func() { execLookPath = exec.LookPath }() defer func() { shell.ExecLookPath = exec.LookPath }()
if err := gm.InstallManager(); err != nil { if err := gm.InstallManager(); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)
@ -34,10 +34,10 @@ func TestGolangManager_InstallPackage(t *testing.T) {
gm := &GolangManager{} gm := &GolangManager{}
// Mock exec.Command // Mock exec.Command
execCommand = func(name string, arg ...string) *exec.Cmd { shell.ExecCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("echo", "mocked go install") return exec.Command("echo", "mocked go install")
} }
defer func() { execCommand = exec.Command }() defer func() { shell.ExecCommand = exec.Command }()
if err := gm.InstallPackage("github.com/test/package"); err != nil { if err := gm.InstallPackage("github.com/test/package"); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)

View file

@ -0,0 +1,61 @@
package packagemanager
import (
"fmt"
"log"
"strings"
"system_setup_tool/internal/shell"
)
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
}

View file

@ -1,8 +1,8 @@
// homebrew_test.go package packagemanager
package main
import ( import (
"os/exec" "os/exec"
"system_setup_tool/internal/shell"
"testing" "testing"
) )
@ -17,13 +17,13 @@ func TestHomebrewManager_InstallManager(t *testing.T) {
hm := &HomebrewManager{} hm := &HomebrewManager{}
// Mock exec.LookPath and executeShellCommand // Mock exec.LookPath and executeShellCommand
execLookPath = func(file string) (string, error) { shell.ExecLookPath = func(file string) (string, error) {
if file == "brew" { if file == "brew" {
return "/usr/local/bin/brew", nil return "/usr/local/bin/brew", nil
} }
return "", exec.ErrNotFound return "", exec.ErrNotFound
} }
defer func() { execLookPath = exec.LookPath }() defer func() { shell.ExecLookPath = exec.LookPath }()
if err := hm.InstallManager(); err != nil { if err := hm.InstallManager(); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)
@ -34,10 +34,10 @@ func TestHomebrewManager_InstallPackage(t *testing.T) {
hm := &HomebrewManager{} hm := &HomebrewManager{}
// Mock exec.Command // Mock exec.Command
execCommand = func(name string, arg ...string) *exec.Cmd { shell.ExecCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("echo", "mocked brew install") return exec.Command("echo", "mocked brew install")
} }
defer func() { execCommand = exec.Command }() defer func() { shell.ExecCommand = exec.Command }()
if err := hm.InstallPackage("test-package"); err != nil { if err := hm.InstallPackage("test-package"); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)

View file

@ -1,27 +1,26 @@
package main package packagemanager
import ( import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings" "strings"
"system_setup_tool/internal/shell"
tea "github.com/charmbracelet/bubbletea"
) )
type OSManager struct { type OSManager struct {
OS *OS OS *OS
SudoPassword string SudoPassword string
Packages []string
Model model
} }
func NewOSManager(os *OS, sudoPassword string, packages []string) *OSManager { func NewOSManager(sudoPassword string) *OSManager {
os, err := GetLinuxDistribution()
if err != nil {
log.Fatalf("error geting os information: %v", err)
}
return &OSManager{ return &OSManager{
OS: os, OS: os,
SudoPassword: sudoPassword, SudoPassword: sudoPassword,
Packages: packages,
Model: newModel(packages, sudoPassword, os),
} }
} }
@ -32,20 +31,47 @@ func (o *OSManager) Name() string {
func (o *OSManager) InstallManager() error { return nil } func (o *OSManager) InstallManager() error { return nil }
func (o *OSManager) Install(packages []string) error { func (o *OSManager) Install(packages []string) error {
o.Packages = packages if len(packages) == 0 {
o.Model = newModel(packages, o.SudoPassword, o.OS) return nil
}
p := tea.NewProgram(o.Model) err := InstallWithProgress(o, packages)
_, err := p.Run() if err != nil {
return err return err
}
return nil
}
func (o *OSManager) InstallPackage(pkg string) error {
return InstallPackage(o.OS.InstallCommand, pkg, o.SudoPassword)
} }
func (o *OSManager) InstallBuildEssentials() error { func (o *OSManager) InstallBuildEssentials() error {
return installBuildEssentials(o.OS, o.SudoPassword) return InstallBuildEssentials(o.OS, o.SudoPassword)
} }
func (o *OSManager) InstallSpecialSoftware() error { func (o *OSManager) RemovePackage(pkg string) error {
return o.Model.installSpecialSoftware() fullCmd := fmt.Sprintf("%s %s", o.OS.RemoveCommand, pkg)
command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd)
command.Stdin = strings.NewReader(o.SudoPassword + "\n")
return command.Run()
}
func (o *OSManager) SearchPackage(pkg string) []string {
cmdParts := strings.Fields(o.OS.SearchCommand)
if len(cmdParts) == 0 {
log.Printf("Invalid search command for OS package manager")
return []string{}
}
cmd := shell.ExecCommand(cmdParts[0], append(cmdParts[1:], pkg)...)
packages, err := cmd.Output()
if err != nil {
log.Printf("Error fetching %s packages: %v", o.OS.PackageManager, err)
return []string{}
}
packageList := strings.Split(strings.TrimSpace(string(packages)), "\n")
return packageList
} }
type OS struct { type OS struct {
@ -54,6 +80,8 @@ type OS struct {
Version string Version string
PackageManager string PackageManager string
InstallCommand string InstallCommand string
SearchCommand string
RemoveCommand string
} }
func parseOsRelease(osRelease string) *OS { func parseOsRelease(osRelease string) *OS {
@ -88,10 +116,14 @@ func parseOsRelease(osRelease string) *OS {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = result.getSearchCommand()
if err != nil {
log.Fatal(err)
}
return &result return &result
} }
func getLinuxDistribution() (*OS, error) { func GetLinuxDistribution() (*OS, error) {
_, err := os.Stat("/etc/os-release") _, err := os.Stat("/etc/os-release")
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, fmt.Errorf("unable to read system information") return nil, fmt.Errorf("unable to read system information")
@ -119,7 +151,7 @@ func (os *OS) getPackageManager() error {
"pacman", "pacman",
} }
for _, pmname := range pmcommands { for _, pmname := range pmcommands {
_, err := execLookPath(pmname) _, err := shell.ExecLookPath(pmname)
if err == nil { if err == nil {
os.PackageManager = pmname os.PackageManager = pmname
return nil return nil
@ -145,7 +177,39 @@ func (os *OS) getInstallCommand() error {
} }
} }
func installBuildEssentials(os *OS, sudoPassword string) error { func (os *OS) getSearchCommand() error {
switch os.PackageManager {
case "apt":
os.SearchCommand = "apt search"
return nil
case "pacman":
os.SearchCommand = "pacman -Ss"
return nil
case "dnf":
os.SearchCommand = "dnf search"
return nil
default:
return fmt.Errorf("no install command found for package manager: %s", os.ID)
}
}
func (os *OS) getDeleteCommand() error {
switch os.PackageManager {
case "apt":
os.RemoveCommand = "apt remove"
return nil
case "pacman":
os.RemoveCommand = "pacman -R"
return nil
case "dnf":
os.RemoveCommand = "dnf remove"
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 var command string
switch os.PackageManager { switch os.PackageManager {
case "pacman": case "pacman":
@ -159,8 +223,8 @@ func installBuildEssentials(os *OS, sudoPassword string) error {
} }
fmt.Printf("Installiere Build Essentials für %s...\n", os.Name) fmt.Printf("Installiere Build Essentials für %s...\n", os.Name)
if err := installPackage(command, "", sudoPassword); err != nil { if err := InstallPackage(command, "", sudoPassword); err != nil {
return fmt.Errorf("fehler bei der Installation der Build Essentials: %v", err) return fmt.Errorf("error installing Build Essentials: %v", err)
} }
return nil return nil
} }

View file

@ -1,26 +1,20 @@
// osmanager_test.go package packagemanager
package main
import ( import (
"testing" "testing"
) )
func TestNewOSManager(t *testing.T) { func TestNewOSManager(t *testing.T) {
os := &OS{ID: "ubuntu", PackageManager: "apt"}
sudoPassword := "testpassword" sudoPassword := "testpassword"
packages := []string{"git", "curl"}
manager := NewOSManager(os, sudoPassword, packages) manager := NewOSManager(sudoPassword)
if manager.OS != os { if manager.OS == nil {
t.Errorf("Expected OS to be %v, got %v", os, manager.OS) t.Error("Expected OS to be non-nil")
} }
if manager.SudoPassword != sudoPassword { if manager.SudoPassword != sudoPassword {
t.Errorf("Expected SudoPassword to be %s, got %s", sudoPassword, manager.SudoPassword) t.Errorf("Expected SudoPassword to be %s, got %s", sudoPassword, manager.SudoPassword)
} }
if len(manager.Packages) != len(packages) {
t.Errorf("Expected Packages length to be %d, got %d", len(packages), len(manager.Packages))
}
} }
func TestOSManagerName(t *testing.T) { func TestOSManagerName(t *testing.T) {
@ -47,4 +41,13 @@ VERSION_ID="20.04"
if os.Version != "20.04" { if os.Version != "20.04" {
t.Errorf("Expected Version to be '20.04', got %s", os.Version) t.Errorf("Expected Version to be '20.04', got %s", os.Version)
} }
if os.PackageManager != "apt" {
t.Errorf("Expected PackageManager to be 'apt', got %s", os.PackageManager)
}
if os.InstallCommand != "apt install -y" {
t.Errorf("Expected InstallCommand to be 'apt install -y', got %s", os.InstallCommand)
}
if os.SearchCommand != "apt search" {
t.Errorf("Expected SearchCommand to be 'apt search', got %s", os.SearchCommand)
}
} }

View file

@ -1,8 +1,10 @@
package main package packagemanager
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"system_setup_tool/internal/shell"
) )
type Packages struct { type Packages struct {
@ -10,26 +12,17 @@ type Packages struct {
NonHeadless []string `mapstructure:"non_headless"` NonHeadless []string `mapstructure:"non_headless"`
} }
func installPackage(cmd, pkg, sudoPassword string) error { func InstallPackage(cmd, pkg, sudoPassword string) error {
fullCmd := fmt.Sprintf("%s %s", cmd, pkg) fullCmd := fmt.Sprintf("%s %s", cmd, pkg)
command := execCommand("sudo", "-S", "sh", "-c", fullCmd) command := shell.ExecCommand("sudo", "-S", "sh", "-c", fullCmd)
command.Stdin = strings.NewReader(sudoPassword + "\n") command.Stdin = strings.NewReader(sudoPassword + "\n")
output, err := command.CombinedOutput() output, err := command.CombinedOutput()
if err != nil { 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") { 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) log.Printf("Package %s not available\n", pkg)
return nil return nil
} }
return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output)) return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output))
} }
return nil 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)
}
}
}

View file

@ -1,8 +1,10 @@
package main package packagemanager
type PackageManager interface { type PackageManager interface {
Install(packages []string) error Install(packages []string) error
InstallPackage(pkg string) error InstallPackage(pkg string) error
InstallManager() error InstallManager() error
RemovePackage(pkg string) error
SearchPackage(pkg string) []string
Name() string Name() string
} }

48
packagemanager/pipx.go Normal file
View file

@ -0,0 +1,48 @@
package packagemanager
import "system_setup_tool/internal/shell"
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{}
}

View file

@ -1,8 +1,8 @@
// pipx_test.go package packagemanager
package main
import ( import (
"os/exec" "os/exec"
"system_setup_tool/internal/shell"
"testing" "testing"
) )
@ -17,13 +17,13 @@ func TestPipxManager_InstallManager(t *testing.T) {
pm := &PipxManager{} pm := &PipxManager{}
// Mock exec.LookPath // Mock exec.LookPath
execLookPath = func(file string) (string, error) { shell.ExecLookPath = func(file string) (string, error) {
if file == "pipx" { if file == "pipx" {
return "/usr/bin/pipx", nil return "/usr/bin/pipx", nil
} }
return "", exec.ErrNotFound return "", exec.ErrNotFound
} }
defer func() { execLookPath = exec.LookPath }() defer func() { shell.ExecLookPath = exec.LookPath }()
if err := pm.InstallManager(); err != nil { if err := pm.InstallManager(); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)
@ -34,10 +34,10 @@ func TestPipxManager_InstallPackage(t *testing.T) {
pm := &PipxManager{} pm := &PipxManager{}
// Mock exec.Command // Mock exec.Command
execCommand = func(name string, arg ...string) *exec.Cmd { shell.ExecCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("echo", "mocked pipx install") return exec.Command("echo", "mocked pipx install")
} }
defer func() { execCommand = exec.Command }() defer func() { shell.ExecCommand = exec.Command }()
if err := pm.InstallPackage("test-package"); err != nil { if err := pm.InstallPackage("test-package"); err != nil {
t.Errorf("Expected no error, got %v", err) t.Errorf("Expected no error, got %v", err)

19
packagemanager/utils.go Normal file
View file

@ -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
}

37
pipx.go
View file

@ -1,37 +0,0 @@
package main
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 := execLookPath("brew"); err != nil {
installHomebrew()
}
if _, err := execLookPath("pipx"); err == nil {
return nil
}
cmd := execCommand("brew", "install", "pipx")
return cmd.Run()
}
func (p *PipxManager) InstallPackage(pkg string) error {
cmd := execCommand("pipx", "install", pkg)
return cmd.Run()
}

146
tui.go
View file

@ -1,146 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/mitchellh/mapstructure"
"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 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)
}
unavailablePackages = []string{}
osManager := NewOSManager(os, sudoPassword, cfg.Packages.Headless)
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)
}
}
printUnavailablePackages()
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)
}
}
var installShellExtensions bool
form = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Möchten Sie die Shell-Extensions installieren?").
Value(&installShellExtensions),
),
).WithTheme(huh.ThemeCatppuccin())
err = form.Run()
if err != nil {
log.Fatal(err)
}
if installShellExtensions {
if err := osManager.InstallSpecialSoftware(); err != nil {
log.Printf("Warnung bei der Installation spezieller Software: %v", err)
}
}
for name, config := range cfg.PackageManagers {
if config.Enable {
var manager PackageManager
switch name {
case "homebrew":
manager = &HomebrewManager{}
case "go":
manager = &GolangManager{}
case "cargo":
manager = &CargoManager{}
case "pipx":
manager = &PipxManager{}
case "flatpak":
flatpakConfig := FlatpakConfig{}
if err := mapstructure.Decode(config, &flatpakConfig); err != nil {
log.Printf("Fehler beim Dekodieren der Flatpak-Konfiguration: %v", err)
continue
}
manager = &FlatpakManager{
OS: os,
SudoPassword: sudoPassword,
Config: flatpakConfig,
}
default:
log.Printf("Unbekannter Paketmanager: %s", name)
continue
}
if err := manager.InstallManager(); err != nil {
log.Printf("Warnung Packagemanager %s nicht vorhanden und konnte nicht installiert werden: %v", manager.Name(), err)
}
if err := manager.Install(config.Packages); err != nil {
log.Printf("Warnung bei %s-Paketen: %v", manager.Name(), err)
}
}
}
if cfg.Dotfiles.Enable {
fmt.Println("\nKonfiguriere Dotfiles...")
if err := setupDotfiles(cfg.Dotfiles); err != nil {
log.Printf("Warnung bei Dotfiles-Setup: %v", err)
}
}
}

115
tui/tui.go Normal file
View file

@ -0,0 +1,115 @@
package tui
import (
"fmt"
"log"
"system_setup_tool/config"
"system_setup_tool/dotfiles"
"system_setup_tool/utils"
pm "system_setup_tool/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)
}
}
}

View file

@ -1,64 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"github.com/charmbracelet/huh"
"github.com/schollz/progressbar/v3"
)
var (
execCommand = exec.Command
execLookPath = exec.LookPath
)
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 max(a, b int) int {
if a > b {
return a
}
return b
}
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
}
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
}

32
utils/utils.go Normal file
View file

@ -0,0 +1,32 @@
package utils
import (
"fmt"
"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 max(a, b int) int {
if a > b {
return a
}
return b
}

40
utils/utils_test.go Normal file
View file

@ -0,0 +1,40 @@
package utils
import (
"os/exec"
"system_setup_tool/internal/shell"
"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)
}
}

View file

@ -1,78 +0,0 @@
package main
import (
"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 := execCommand
defer func() { execCommand = oldExecCommand }()
execCommand = func(command string, args ...string) *exec.Cmd {
return exec.Command("echo", "mocked command")
}
err := executeShellCommand("test command", "TEST_ENV=value")
if err != nil {
t.Errorf("executeShellCommand() error = %v; want nil", err)
}
}
func TestInstallWithProgress(t *testing.T) {
mockManager := &MockPackageManager{
packages: []string{"pkg1", "pkg2"},
}
err := installWithProgress(mockManager, mockManager.packages)
if err != nil {
t.Errorf("installWithProgress() error = %v; want nil", err)
}
if len(mockManager.installedPackages) != len(mockManager.packages) {
t.Errorf("installWithProgress() installed %d packages; want %d", len(mockManager.installedPackages), len(mockManager.packages))
}
}
// MockPackageManager implementiert das PackageManager Interface für Tests
type MockPackageManager struct {
packages []string
installedPackages []string
}
func (m *MockPackageManager) Install(packages []string) error {
m.installedPackages = append(m.installedPackages, packages...)
return nil
}
func (m *MockPackageManager) InstallPackage(pkg string) error {
m.installedPackages = append(m.installedPackages, pkg)
return nil
}
func (m *MockPackageManager) InstallManager() error {
return nil
}
func (m *MockPackageManager) Name() string {
return "MockManager"
}