diff --git a/main.go b/main.go index 2937944..ad4d13f 100644 --- a/main.go +++ b/main.go @@ -19,21 +19,24 @@ const ( Green = "\033[0;32m" Blue = "\033[0;34m" Yellow = "\033[0;33m" + Purple = "\033[0;35m" // For elevated scripts NC = "\033[0m" ) type Config struct { - RunsDir string - LogsDir string - DryRun bool - Verbose bool + RunsDir string + LogsDir string + DryRun bool + Verbose bool + Interactive bool } type Script struct { - Path string // Full filesystem path - Name string // Just filename (e.g. "install.sh") - RelPath string // Relative path from runs/ (e.g. "tools/install.sh") - Desc string // Description from script comment + Path string // Full filesystem path + Name string // Just filename (e.g. "install.sh") + RelPath string // Relative path from runs/ (e.g. "tools/install.sh") + Desc string // Description from script comment + RequiresSudo bool // Whether script needs elevated privileges } var config Config @@ -50,6 +53,10 @@ func errorLog(msg string, args ...any) { fmt.Fprintf(os.Stderr, Red+"[ERROR]"+NC+" "+msg+"\n", args...) } +func sudoLog(msg string, args ...any) { + fmt.Printf(Purple+"[SUDO]"+NC+" "+msg+"\n", args...) +} + func commandExists(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil @@ -206,24 +213,39 @@ func handlePush() error { return nil } -func getDescription(scriptPath string) string { +func getScriptMetadata(scriptPath string) (string, bool) { file, err := os.Open(scriptPath) if err != nil { - return "No description" + return "No description", false } defer file.Close() + var desc string + var requiresSudo bool + scanner := bufio.NewScanner(file) for scanner.Scan() { - line := scanner.Text() + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "# NAME:") { - desc := strings.TrimSpace(strings.TrimPrefix(line, "# NAME:")) - if desc != "" { - return desc - } + desc = strings.TrimSpace(strings.TrimPrefix(line, "# NAME:")) + } + + if strings.HasPrefix(line, "# REQUIRES: sudo") || + strings.HasPrefix(line, "# REQUIRES:sudo") || + strings.HasPrefix(line, "# ELEVATED: true") || + strings.HasPrefix(line, "# ELEVATED:true") || + strings.HasPrefix(line, "# SUDO: true") || + strings.HasPrefix(line, "# SUDO:true") { + requiresSudo = true } } - return "No description" + + if desc == "" { + desc = "No description" + } + + return desc, requiresSudo } func findScripts(filters []string) ([]Script, error) { @@ -249,11 +271,14 @@ func findScripts(filters []string) ([]Script, error) { scriptName := filepath.Base(path) if len(filters) == 0 || matchesFilters(relPath, scriptName, filters) { + desc, requiresSudo := getScriptMetadata(path) + scripts = append(scripts, Script{ - Path: path, - Name: scriptName, - RelPath: relPath, - Desc: getDescription(path), + Path: path, + Name: scriptName, + RelPath: relPath, + Desc: desc, + RequiresSudo: requiresSudo, }) } } @@ -299,30 +324,40 @@ func matchesFilters(relPath, scriptName string, filters []string) bool { func executeScript(script Script, args []string, verbose bool) error { logFile := filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log") - // Create command with script path and arguments - cmd := exec.Command(script.Path, args...) - - if verbose { - logFileHandle, err := os.Create(logFile) - if err != nil { - return err + var cmd *exec.Cmd + if script.RequiresSudo { + sudoLog("Script requires elevated privileges: %s", script.RelPath) + if !commandExists("sudo") { + errorLog("sudo command not found - cannot run elevated script") + return fmt.Errorf("sudo not available") } - defer logFileHandle.Close() + fullArgs := append([]string{script.Path}, args...) + cmd = exec.Command("sudo", fullArgs...) + sudoLog("Running with sudo: %s", strings.Join(append([]string{script.Path}, args...), " ")) + } else { + cmd = exec.Command(script.Path, args...) + } + if config.Interactive { + cmd.Stdin = os.Stdin + } + + logFileHandle, err := os.Create(logFile) + if err != nil { + return err + } + defer logFileHandle.Close() + + showOutput := verbose || config.Interactive + if showOutput { cmd.Stdout = io.MultiWriter(os.Stdout, logFileHandle) cmd.Stderr = io.MultiWriter(os.Stderr, logFileHandle) - return cmd.Run() } else { - logFileHandle, err := os.Create(logFile) - if err != nil { - return err - } - defer logFileHandle.Close() - cmd.Stdout = logFileHandle cmd.Stderr = logFileHandle - return cmd.Run() } + + return cmd.Run() } func createNewScript(scriptName string) error { @@ -342,13 +377,12 @@ func createNewScript(scriptName string) error { template := fmt.Sprintf(`#!/usr/bin/env bash # NAME: %s script +# REQUIRES: sudo set -euo pipefail echo "Running %s script..." -# Add your commands here - echo "✅ %s completed successfully" `, strings.TrimSuffix(scriptName, ".sh"), scriptName, strings.TrimSuffix(scriptName, ".sh")) @@ -384,7 +418,31 @@ echo "Edit this script or add your own to runs/" return err } - log("✅ Created runs/ directory with example script") + sudoExampleScript := filepath.Join(config.RunsDir, "system-info.sh") + sudoContent := `#!/usr/bin/env bash +# NAME: System information collector +# REQUIRES: sudo + +set -euo pipefail + +echo "Collecting system information (requires elevated privileges)..." + +echo "=== Disk Usage ===" +df -h + +echo "=== Memory Info ===" +free -h + +echo "=== System Logs (last 10 lines) ===" +tail -n 10 /var/log/syslog 2>/dev/null || tail -n 10 /var/log/messages 2>/dev/null || echo "No accessible system logs" + +echo "✅ System info collection completed" +` + if err := os.WriteFile(sudoExampleScript, []byte(sudoContent), 0o755); err != nil { + return err + } + + log("✅ Created runs/ directory with example scripts") return nil } return nil @@ -405,10 +463,13 @@ var runCmd = &cobra.Command{ Short: "Run scripts matching filters (or all if no filters)", Long: `Run scripts matching filters. Use -- to pass arguments to scripts. +Scripts marked with "# REQUIRES: sudo" will automatically run with elevated privileges. + Examples: dev run tools/dev.sh # Run specific script dev run tools/dev.sh -- arg1 arg2 # Run with arguments - dev run --verbose install -- --force # Run with flags`, + dev run --verbose install -- --force # Run with flags + dev run system-info # Runs with sudo if marked as required`, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { scripts, err := findScripts(nil) if err != nil { @@ -417,10 +478,12 @@ Examples: var completions []string for _, script := range scripts { - // Add both the script name and relative path as completion options if strings.HasPrefix(script.Name, toComplete) || strings.HasPrefix(script.RelPath, toComplete) { - // Use relative path as the completion value with description - completions = append(completions, fmt.Sprintf("%s\t%s", script.RelPath, script.Desc)) + desc := script.Desc + if script.RequiresSudo { + desc = desc + " [SUDO]" + } + completions = append(completions, fmt.Sprintf("%s\t%s", script.RelPath, desc)) } } return completions, cobra.ShellCompDirectiveNoFileComp @@ -446,18 +509,14 @@ Examples: } if dashDashIndex >= 0 { - // Extract everything between "run" and "--" as filters (skip flags) for i := runIndex + 1; i < dashDashIndex; i++ { arg := rawArgs[i] - // Skip flags if !strings.HasPrefix(arg, "-") { filters = append(filters, arg) } } - // Everything after "--" are script args scriptArgs = rawArgs[dashDashIndex+1:] } else { - // No --, use cobra's args as filters filters = args scriptArgs = []string{} } @@ -479,19 +538,22 @@ Examples: if len(filters) > 0 { warn("No scripts match filters: %s", strings.Join(filters, ", ")) } else { - warn("No scripts found. Use 'dev new ' to create one") + warn("No scripts found. Use 'dev new ' to create one") } return nil } - // CLI execution for _, script := range scripts { if config.DryRun { argsStr := "" if len(scriptArgs) > 0 { argsStr = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " ")) } - fmt.Printf("[DRY] Would run: %s - %s%s\n", script.RelPath, script.Desc, argsStr) + sudoStr := "" + if script.RequiresSudo { + sudoStr = " [SUDO]" + } + fmt.Printf("[DRY] Would run: %s - %s%s%s\n", script.RelPath, script.Desc, sudoStr, argsStr) continue } @@ -499,7 +561,13 @@ Examples: if len(scriptArgs) > 0 { argsDisplay = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " ")) } - fmt.Printf("Running: %s%s\n", script.RelPath, argsDisplay) + + sudoDisplay := "" + if script.RequiresSudo { + sudoDisplay = " [ELEVATED]" + } + + fmt.Printf("Running: %s%s%s\n", script.RelPath, sudoDisplay, argsDisplay) if err := executeScript(script, scriptArgs, config.Verbose); err != nil { errorLog("❌ %s failed", script.RelPath) @@ -507,7 +575,11 @@ Examples: fmt.Printf(" Check log: %s\n", filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")) } } else { - log("✅ %s completed", script.RelPath) + if script.RequiresSudo { + sudoLog("✅ %s completed (elevated)", script.RelPath) + } else { + log("✅ %s completed", script.RelPath) + } } } return nil @@ -530,21 +602,39 @@ var listCmd = &cobra.Command{ if len(scripts) == 0 { warn("No scripts found in %s", config.RunsDir) - fmt.Println("Use 'dev new ' to create a new script") + fmt.Println("Use 'dev new ' to create a new script") return nil } fmt.Printf("Available scripts in %s:\n\n", config.RunsDir) for _, script := range scripts { - fmt.Printf(" %s%s%s - %s\n", Blue, script.RelPath, NC, script.Desc) + if script.RequiresSudo { + fmt.Printf(" %s%s%s - %s %s[SUDO]%s\n", + Blue, script.RelPath, NC, script.Desc, Purple, NC) + } else { + fmt.Printf(" %s%s%s - %s\n", + Blue, script.RelPath, NC, script.Desc) + } } - fmt.Printf("\nTotal: %d scripts\n", len(scripts)) + + sudoCount := 0 + for _, script := range scripts { + if script.RequiresSudo { + sudoCount++ + } + } + + fmt.Printf("\nTotal: %d scripts", len(scripts)) + if sudoCount > 0 { + fmt.Printf(" (%d require elevated privileges)", sudoCount) + } + fmt.Println() return nil }, } var newCmd = &cobra.Command{ - Use: "new ", + Use: "new ", Short: "Create a new script template", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -616,6 +706,7 @@ func main() { runCmd.Flags().BoolVar(&config.DryRun, "dry", false, "Show what would run without executing") runCmd.Flags().BoolVarP(&config.Verbose, "verbose", "v", false, "Show script output in terminal") + runCmd.Flags().BoolVarP(&config.Interactive, "interactive", "i", false, "Run script interactively (show output and allow input)") // This prevents Cobra from consuming -- and everything after it runCmd.Flags().SetInterspersed(false) diff --git a/runs/limine.sh b/runs/limine.sh new file mode 100755 index 0000000..1758904 --- /dev/null +++ b/runs/limine.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# NAME: Script to transition from systemd-boot to Limine bootloader +# REQUIRES: sudo + +set -euo pipefail + +echo "do you confirm this transition?" + +read -r -p "Are you sure? [y/N] " response + +if [[ "$response" != "y" ]]; then + exit 0 +fi + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_status() { echo -e "${BLUE}[INFO]${NC} $1"; } +print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +print_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +if [[ $EUID -ne 0 ]]; then + print_error "This script must be run as root (use sudo)" + exit 1 +fi + +if ! command -v pacman &>/dev/null; then + print_error "This script is designed for Arch Linux / CachyOS systems" + exit 1 +fi + +print_status "Starting transition from systemd-boot to Limine..." + +ESP_PATH="" +if command -v bootctl &>/dev/null; then + ESP_PATH=$(bootctl --print-esp-path 2>/dev/null || echo "") +fi + +if [[ -z "$ESP_PATH" ]]; then + if [[ -d "/boot/EFI" ]]; then + ESP_PATH="/boot" + elif [[ -d "/efi/EFI" ]]; then + ESP_PATH="/efi" + else + print_error "Could not detect ESP path" + exit 1 + fi +fi + +print_success "ESP detected at: $ESP_PATH" + +BACKUP_DIR="/root/bootloader-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$BACKUP_DIR" + +[[ -d "$ESP_PATH/loader" ]] && cp -r "$ESP_PATH/loader" "$BACKUP_DIR/" 2>/dev/null || true +[[ -d "$ESP_PATH/EFI/systemd" ]] && cp -r "$ESP_PATH/EFI/systemd" "$BACKUP_DIR/" 2>/dev/null || true +efibootmgr -v >"$BACKUP_DIR/efibootmgr-backup.txt" 2>/dev/null || true + +print_success "Backup created at: $BACKUP_DIR" + +CURRENT_CMDLINE="" +if [[ -f "/etc/kernel/cmdline" ]]; then + CURRENT_CMDLINE=$(cat /etc/kernel/cmdline) + print_status "Found existing /etc/kernel/cmdline: $CURRENT_CMDLINE" +else + CURRENT_CMDLINE=$(cat /proc/cmdline | sed 's/BOOT_IMAGE=[^ ]* //') + print_status "Using current /proc/cmdline: $CURRENT_CMDLINE" +fi + +print_status "Installing Limine and related packages..." +pacman -S --needed --noconfirm limine limine-snapper-sync limine-mkinitcpio-hook +print_success "Limine packages installed" + +print_status "Installing Limine bootloader to ESP..." +mkdir -p "$ESP_PATH/EFI/Limine" + +if [[ -f "/usr/share/limine/limine_x64.efi" ]]; then + cp /usr/share/limine/limine_x64.efi "$ESP_PATH/EFI/Limine/" +elif [[ -f "/usr/share/limine/BOOTX64.EFI" ]]; then + cp /usr/share/limine/BOOTX64.EFI "$ESP_PATH/EFI/Limine/limine_x64.efi" +else + print_error "Could not find Limine EFI files" + exit 1 +fi + +print_success "Limine files installed to ESP" + +print_status "Creating UEFI boot entry for Limine..." +ESP_DEVICE=$(df "$ESP_PATH" | tail -1 | awk '{print $1}') +ESP_DISK=$(echo "$ESP_DEVICE" | sed 's/[0-9]*$//') +ESP_PART_NUM=$(echo "$ESP_DEVICE" | sed 's/.*[^0-9]//') + +efibootmgr -c -d "$ESP_DISK" -p "$ESP_PART_NUM" -L "Limine" -l "\\EFI\\Limine\\limine_x64.efi" || { + print_warning "Could not create UEFI boot entry automatically" +} + +print_success "Limine UEFI boot entry created" + +print_status "Configuring kernel command line..." +echo "$CURRENT_CMDLINE" >/etc/kernel/cmdline +print_success "Kernel command line configured: $CURRENT_CMDLINE" + +print_status "Configuring Limine..." +[[ ! -f "/etc/default/limine" ]] && cat >/etc/default/limine <